package codegen

const mainTemplateSource = `// Code generated by clihub v{{.ClihubVersion}}. DO NOT EDIT.
package main

import (
	"bytes"
	"context"
{{- if .IsHTTP}}
	"crypto/rand"
	"crypto/sha256"
{{- end}}
	"encoding/base64"
{{- if .IsHTTP}}
	"encoding/hex"
{{- end}}
	"encoding/json"
	"fmt"
	"io"
{{- if .IsHTTP}}
	"net"
{{- end}}
	"net/http"
	"net/url"
	"os"
{{- if .IsHTTP}}
	"os/exec"
{{- end}}
	"path/filepath"
{{- if .IsHTTP}}
	"runtime"
{{- end}}
	"strings"
{{- if .IsHTTP}}
	"sync"
{{- end}}
	"time"

	mcpclient "github.com/mark3labs/mcp-go/client"
{{- if .IsHTTP}}
	"github.com/mark3labs/mcp-go/client/transport"
{{- end}}
	"github.com/mark3labs/mcp-go/mcp"
	"github.com/spf13/cobra"
	"golang.org/x/oauth2/google"
)

// --- Embedded server configuration ---
{{- if .IsHTTP}}
const serverURL = {{quote .ServerURL}}
{{- else}}
var stdioCommand = {{quote .StdioCommand}}
var stdioArgs = {{quoteSlice .StdioArgs}}
var envKeys = {{quoteSlice .EnvKeys}}
{{- end}}

// --- Global flags ---
var (
	globalTimeout        int
	globalOutput         string
	globalAuthToken      string
	globalAuthType       string
	globalAuthHeaderName string
	globalAuthUsername   string
	globalAuthPassword   string
	globalHelpAuth       bool
)

func main() {
	rootCmd := &cobra.Command{
		Use:   {{quote .CLIName}},
		Short: {{quote (printf "CLI for %s MCP server" .CLIName)}},
		SilenceUsage:  true,
		SilenceErrors: true,
	}

	rootCmd.PersistentFlags().IntVarP(&globalTimeout, "timeout", "t", 30000, "per-call timeout in milliseconds")
	rootCmd.PersistentFlags().StringVarP(&globalOutput, "output", "o", "text", "output format: text|json|markdown|raw")
	rootCmd.PersistentFlags().StringVar(&globalAuthToken, "auth-token", "", "bearer token for authenticated MCP servers")
	rootCmd.PersistentFlags().StringVar(&globalAuthType, "auth-type", "", "authentication type: bearer, api_key, basic, none")
	rootCmd.PersistentFlags().StringVar(&globalAuthHeaderName, "auth-header-name", "", "custom header name for api_key auth (default X-API-Key)")
	rootCmd.PersistentFlags().StringVar(&globalAuthUsername, "auth-username", "", "username for basic auth")
	rootCmd.PersistentFlags().StringVar(&globalAuthPassword, "auth-password", "", "password for basic auth")
	rootCmd.PersistentFlags().BoolVar(&globalHelpAuth, "help-auth", false, "show authentication flags and exit")
	hideAuthFlags(rootCmd)

{{- range .Tools}}
	rootCmd.AddCommand(cmd{{funcName .Name}}())
{{- end}}
{{- if .IsHTTP}}
	rootCmd.AddCommand(cmdAuth())
{{- end}}

	if wantsAuthHelp(os.Args[1:]) {
		printAuthFlagHelp(os.Stdout)
		return
	}

	if err := rootCmd.Execute(); err != nil {
		fmt.Fprintf(os.Stderr, "Error: %s\n", err)
		os.Exit(1)
	}
}

func wantsAuthHelp(args []string) bool {
	for _, arg := range args {
		if arg == "--help-auth" || arg == "--help-auth=true" {
			return true
		}
	}
	return false
}

func hideAuthFlags(rootCmd *cobra.Command) {
	for _, name := range []string{
		"auth-token",
		"auth-type",
		"auth-header-name",
		"auth-username",
		"auth-password",
	} {
		_ = rootCmd.PersistentFlags().MarkHidden(name)
	}
}

func printAuthFlagHelp(out io.Writer) {
	fmt.Fprintln(out, "Authentication flags:")
	fmt.Fprintln(out, "  --auth-token string         bearer token for authenticated MCP servers")
	fmt.Fprintln(out, "  --auth-type string          authentication type: bearer, api_key, basic, none")
	fmt.Fprintln(out, "  --auth-header-name string   custom header name for api_key auth (default X-API-Key)")
	fmt.Fprintln(out, "  --auth-username string      username for basic auth")
	fmt.Fprintln(out, "  --auth-password string      password for basic auth")
}

func chooseFromJSONFlagName(cmd *cobra.Command) string {
	for _, candidate := range []string{"from-json", "clihub-from-json", "from-json-input"} {
		if cmd.Flags().Lookup(candidate) == nil {
			return candidate
		}
	}
	return "clihub-from-json"
}

// --- Tool commands ---
{{range .Tools}}
func cmd{{funcName .Name}}() *cobra.Command {
{{- range .Options}}
	var {{varName .FlagName}} {{.GoType}}
{{- end}}
	var flagFromJSON string
	fromJSONFlagName := "from-json"

	cmd := &cobra.Command{
		Use:     {{quote .CommandName}},
		Aliases: []string{ {{quote .Name}} },
		Short:   {{quote .Description}},
		SilenceUsage:  true,
		SilenceErrors: true,
		RunE: func(cmd *cobra.Command, args []string) error {
			params := make(map[string]interface{})
			if flagFromJSON != "" {
				var conflictingFlag string
{{- range .Options}}
				if conflictingFlag == "" && cmd.Flags().Changed({{quote .FlagName}}) {
					conflictingFlag = {{quote .FlagName}}
				}
{{- end}}
				if conflictingFlag != "" {
					return fmt.Errorf("--%s cannot be combined with --%s", fromJSONFlagName, conflictingFlag)
				}
				if err := json.Unmarshal([]byte(flagFromJSON), &params); err != nil {
					return fmt.Errorf("invalid --%s JSON: %w", fromJSONFlagName, err)
				}
			} else {
{{- range .Options}}
{{- if eq .GoType "string"}}
					if {{varName .FlagName}} != "" {
					params[{{quote .PropertyName}}] = {{varName .FlagName}}
				}
{{- else if eq .GoType "int"}}
				if cmd.Flags().Changed({{quote .FlagName}}) {
					params[{{quote .PropertyName}}] = {{varName .FlagName}}
				}
{{- else if eq .GoType "float64"}}
				if cmd.Flags().Changed({{quote .FlagName}}) {
					params[{{quote .PropertyName}}] = {{varName .FlagName}}
				}
{{- else if eq .GoType "bool"}}
				if cmd.Flags().Changed({{quote .FlagName}}) {
					params[{{quote .PropertyName}}] = {{varName .FlagName}}
				}
{{- else if eq .GoType "[]string"}}
				if len({{varName .FlagName}}) > 0 {
					params[{{quote .PropertyName}}] = {{varName .FlagName}}
				}
{{- else if eq .GoType "[]int"}}
				if len({{varName .FlagName}}) > 0 {
					params[{{quote .PropertyName}}] = {{varName .FlagName}}
				}
{{- end}}
{{- end}}
			}
{{range .Options}}{{if .EnumValues}}
			// Validate enum for {{.FlagName}}
			if v, ok := params[{{quote .PropertyName}}]; ok {
				if s, ok := v.(string); ok {
					valid := {{quoteSlice .EnumValues}}
					found := false
					for _, e := range valid {
						if s == e {
							found = true
							break
						}
					}
					if !found {
						return fmt.Errorf("invalid value %q for --%s: must be one of: %s", s, {{quote .FlagName}}, strings.Join(valid, ", "))
					}
				}
			}
{{end}}{{end}}
			return callTool({{quote .Name}}, params)
		},
	}

{{- range .Options}}
	cmd.Flags().{{cobraFlag .GoType}}(&{{varName .FlagName}}, {{quote .FlagName}}, {{defaultLit .GoType .DefaultValue}}, {{quote (hasEnumDesc .Description .EnumValues)}})
{{- end}}
	fromJSONFlagName = chooseFromJSONFlagName(cmd)
	cmd.Flags().StringVar(&flagFromJSON, fromJSONFlagName, "", "tool input as JSON (bypasses typed flags)")

	return cmd
}
{{end}}

// --- MCP client via mcp-go SDK ---

func callTool(toolName string, params map[string]interface{}) error {
	provider := resolveAuthProvider()

	timeout := time.Duration(globalTimeout) * time.Millisecond
	ctx, cancel := context.WithTimeout(context.Background(), timeout)
	defer cancel()

	c, err := createClient(ctx, provider)
	if err != nil {
		return err
	}
	defer c.Close()

{{- if .IsHTTP}}
	// Start HTTP transport
	if err := c.Start(ctx); err != nil {
		if ctx.Err() != nil {
			return fmt.Errorf("tool call timed out after %dms", globalTimeout)
		}
		return fmt.Errorf("MCP connection failed: %w", err)
	}
{{- end}}

	// Initialize handshake
	initReq := mcp.InitializeRequest{}
	initReq.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION
	initReq.Params.ClientInfo = mcp.Implementation{Name: {{quote .CLIName}}, Version: "1.0.0"}
	initReq.Params.Capabilities = mcp.ClientCapabilities{}

	if _, err := c.Initialize(ctx, initReq); err != nil {
		if ctx.Err() != nil {
			return fmt.Errorf("tool call timed out after %dms", globalTimeout)
		}
{{- if not .IsHTTP}}
		// Capture stderr from crashed subprocess
		if r, ok := mcpclient.GetStderr(c); ok && r != nil {
			buf := make([]byte, 2048)
			if n, _ := r.Read(buf); n > 0 {
				return fmt.Errorf("MCP server crashed:\n  %s", strings.ReplaceAll(strings.TrimSpace(string(buf[:n])), "\n", "\n  "))
			}
		}
{{- end}}
		return fmt.Errorf("MCP handshake failed: %w", err)
	}

	// Call tool
	callReq := mcp.CallToolRequest{}
	callReq.Params.Name = toolName
	callReq.Params.Arguments = params

	result, err := c.CallTool(ctx, callReq)
	if err != nil {
		if ctx.Err() != nil {
			return fmt.Errorf("tool call timed out after %dms", globalTimeout)
		}
		return fmt.Errorf("tool call failed: %w", err)
	}
	if result.IsError {
		// Extract error text from content
		var errTexts []string
		for _, content := range result.Content {
			if tc, ok := content.(mcp.TextContent); ok {
				errTexts = append(errTexts, tc.Text)
			}
		}
		if len(errTexts) > 0 {
			return fmt.Errorf("tool error: %s", strings.Join(errTexts, "\n"))
		}
		return fmt.Errorf("tool returned an error")
	}

	return formatOutput(result, globalOutput)
}

func createClient(ctx context.Context, provider authProvider) (*mcpclient.Client, error) {
{{- if .IsHTTP}}
	var opts []transport.StreamableHTTPCOption
	headers := provider.getHeaders(ctx)
	if len(headers) > 0 {
		opts = append(opts, transport.WithHTTPHeaderFunc(func(ctx context.Context) map[string]string {
			return provider.getHeaders(ctx)
		}))
	}
	return mcpclient.NewStreamableHttpClient(serverURL, opts...)
{{- else}}
	// Build environment for subprocess
	env := os.Environ()
	for _, key := range envKeys {
		val := os.Getenv(key)
		if val == "" {
			return nil, fmt.Errorf("required environment variable %s is not set", key)
		}
	}
	return mcpclient.NewStdioMCPClient(stdioCommand, env, stdioArgs...)
{{- end}}
}

// --- Output formatting ---

func formatOutput(result *mcp.CallToolResult, format string) error {
	switch format {
	case "json":
		data, err := json.MarshalIndent(result, "", "  ")
		if err != nil {
			return err
		}
		fmt.Println(string(data))

	case "raw":
		data, err := json.Marshal(result)
		if err != nil {
			return err
		}
		fmt.Println(string(data))

	default: // "text", "markdown"
		text := extractText(result)
		fmt.Println(text)
	}
	return nil
}

func extractText(result *mcp.CallToolResult) string {
	var parts []string
	for _, content := range result.Content {
		switch c := content.(type) {
		case mcp.TextContent:
			parts = append(parts, c.Text)
		default:
			// For non-text content, marshal to JSON
			data, err := json.Marshal(c)
			if err == nil {
				var pretty bytes.Buffer
				if json.Indent(&pretty, data, "", "  ") == nil {
					parts = append(parts, pretty.String())
				} else {
					parts = append(parts, string(data))
				}
			}
		}
	}
	if len(parts) > 0 {
		return strings.Join(parts, "\n")
	}
	// Fallback: marshal the whole result
	data, err := json.MarshalIndent(result, "", "  ")
	if err != nil {
		return fmt.Sprintf("%+v", result)
	}
	return string(data)
}

// --- Auth provider dispatch ---

type authProvider interface {
	getHeaders(ctx context.Context) map[string]string
}

type noAuthProvider struct{}
func (p *noAuthProvider) getHeaders(_ context.Context) map[string]string { return nil }

type bearerTokenProvider struct{ token string }
func (p *bearerTokenProvider) getHeaders(_ context.Context) map[string]string {
	if p.token == "" { return nil }
	return map[string]string{"Authorization": "Bearer " + p.token}
}

type apiKeyProvider struct{ token, headerName string }
func (p *apiKeyProvider) getHeaders(_ context.Context) map[string]string {
	if p.token == "" { return nil }
	name := p.headerName
	if name == "" { name = "X-API-Key" }
	return map[string]string{name: p.token}
}

type basicAuthProvider struct{ username, password string }
func (p *basicAuthProvider) getHeaders(_ context.Context) map[string]string {
	if p.username == "" && p.password == "" { return nil }
	encoded := base64.StdEncoding.EncodeToString([]byte(p.username + ":" + p.password))
	return map[string]string{"Authorization": "Basic " + encoded}
}

type s2sOAuth2Provider struct {
	clientID, clientSecret, tokenEndpoint string
	cachedToken string
	expiresAt   time.Time
}
func (p *s2sOAuth2Provider) getHeaders(ctx context.Context) map[string]string {
	if p.cachedToken == "" || (!p.expiresAt.IsZero() && time.Now().After(p.expiresAt)) {
		token, expiresIn, err := s2sAuthenticate(ctx, p.tokenEndpoint, p.clientID, p.clientSecret)
		if err != nil {
			return nil
		}
		p.cachedToken = token
		if expiresIn > 0 {
			p.expiresAt = time.Now().Add(time.Duration(expiresIn) * time.Second)
		}
	}
	return map[string]string{"Authorization": "Bearer " + p.cachedToken}
}

type googleSAProvider struct {
	keyFile string
	scopes  []string
}
func (p *googleSAProvider) getHeaders(ctx context.Context) map[string]string {
	keyData, err := os.ReadFile(p.keyFile)
	if err != nil { return nil }
	scopes := p.scopes
	if len(scopes) == 0 {
		scopes = []string{"https://www.googleapis.com/auth/cloud-platform"}
	}
	creds, err := google.CredentialsFromJSON(ctx, keyData, scopes...)
	if err != nil { return nil }
	token, err := creds.TokenSource.Token()
	if err != nil { return nil }
	return map[string]string{"Authorization": "Bearer " + token.AccessToken}
}

// --- Credential store types ---

type credentialsFile struct {
	Version int                         ` + "`" + `json:"version,omitempty"` + "`" + `
	Servers map[string]serverCredential ` + "`" + `json:"servers"` + "`" + `
}

type serverCredential struct {
	AuthType      string   ` + "`" + `json:"auth_type,omitempty"` + "`" + `
	Type          string   ` + "`" + `json:"type,omitempty"` + "`" + `
	Token         string   ` + "`" + `json:"token,omitempty"` + "`" + `
	AccessToken   string   ` + "`" + `json:"access_token,omitempty"` + "`" + `
	RefreshToken  string   ` + "`" + `json:"refresh_token,omitempty"` + "`" + `
	ExpiresAt     string   ` + "`" + `json:"expires_at,omitempty"` + "`" + `
	ClientID      string   ` + "`" + `json:"client_id,omitempty"` + "`" + `
	ClientSecret  string   ` + "`" + `json:"client_secret,omitempty"` + "`" + `
	TokenEndpoint string   ` + "`" + `json:"token_endpoint,omitempty"` + "`" + `
	Scope         string   ` + "`" + `json:"scope,omitempty"` + "`" + `
	HeaderName    string   ` + "`" + `json:"header_name,omitempty"` + "`" + `
	Username      string   ` + "`" + `json:"username,omitempty"` + "`" + `
	Password      string   ` + "`" + `json:"password,omitempty"` + "`" + `
	KeyFile       string   ` + "`" + `json:"key_file,omitempty"` + "`" + `
	Scopes        []string ` + "`" + `json:"scopes,omitempty"` + "`" + `
}

func (s serverCredential) resolveAuthType() string {
	if s.AuthType != "" { return s.AuthType }
	switch s.Type {
	case "bearer": return "bearer_token"
	case "oauth": return "oauth2"
	default: return s.Type
	}
}

func (s serverCredential) isExpired() bool {
	if s.ExpiresAt == "" { return false }
	t, err := time.Parse(time.RFC3339, s.ExpiresAt)
	if err != nil { return false }
	return time.Now().After(t)
}

// --- Token refresh ---

func refreshOAuthToken(credPath, srvURL string, sc serverCredential) (string, bool, error) {
	if sc.RefreshToken == "" {
		return "", true, fmt.Errorf("access token expired and no refresh token available. Run ` + "`" + `%s auth` + "`" + ` to re-authenticate", os.Args[0])
	}
	tokenEndpoint := sc.TokenEndpoint
	if tokenEndpoint == "" {
		discovered, err := discoverTokenEndpointForServer(context.Background(), srvURL)
		if err != nil {
			return "", false, fmt.Errorf("access token expired and no token endpoint stored, and discovery failed: %w", err)
		}
		tokenEndpoint = discovered
	}

	form := url.Values{
		"grant_type":    {"refresh_token"},
		"client_id":     {sc.ClientID},
		"refresh_token": {sc.RefreshToken},
	}
	if sc.ClientSecret != "" {
		form.Set("client_secret", sc.ClientSecret)
	}

	httpClient := &http.Client{Timeout: 30 * time.Second}
	req, err := http.NewRequestWithContext(context.Background(), "POST", tokenEndpoint, strings.NewReader(form.Encode()))
	if err != nil {
		return "", false, fmt.Errorf("token refresh request: %w", err)
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	resp, err := httpClient.Do(req)
	if err != nil {
		return "", false, fmt.Errorf("token refresh request failed: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
		body, _ := io.ReadAll(resp.Body)
		permanent := isPermanentOAuthFailure(resp.StatusCode, body)
		return "", permanent, fmt.Errorf("token refresh failed (%d): %s. Run ` + "`" + `%s auth` + "`" + ` to re-authenticate", resp.StatusCode, string(body), os.Args[0])
	}

	var tokenResp struct {
		AccessToken  string ` + "`" + `json:"access_token"` + "`" + `
		RefreshToken string ` + "`" + `json:"refresh_token"` + "`" + `
		ExpiresIn    int    ` + "`" + `json:"expires_in"` + "`" + `
	}
	if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
		return "", false, fmt.Errorf("parse refresh response: %w", err)
	}

	if tokenResp.AccessToken == "" {
		return "", false, fmt.Errorf("refresh response missing access_token")
	}

	// Update credential store
	data, err := os.ReadFile(credPath)
	if err == nil {
		var creds credentialsFile
		if json.Unmarshal(data, &creds) == nil && creds.Servers != nil {
			entry := creds.Servers[srvURL]
			if entry.AuthType == "" {
				entry.AuthType = "oauth2"
				entry.Type = "oauth"
			}
			entry.AccessToken = tokenResp.AccessToken
			entry.TokenEndpoint = tokenEndpoint
			if tokenResp.RefreshToken != "" {
				entry.RefreshToken = tokenResp.RefreshToken
			}
			if tokenResp.ExpiresIn > 0 {
				entry.ExpiresAt = time.Now().Add(time.Duration(tokenResp.ExpiresIn) * time.Second).Format(time.RFC3339)
			}
			creds.Servers[srvURL] = entry
			if creds.Version < 2 {
				creds.Version = 2
			}
			if updated, err := json.MarshalIndent(creds, "", "  "); err == nil {
				os.WriteFile(credPath, updated, 0600)
			}
		}
	}

	return tokenResp.AccessToken, false, nil
}

func isPermanentOAuthFailure(status int, body []byte) bool {
	if status == http.StatusUnauthorized {
		return true
	}
	if status != http.StatusBadRequest {
		return false
	}

	var oauthErr struct {
		Error string ` + "`" + `json:"error"` + "`" + `
	}
	if err := json.Unmarshal(body, &oauthErr); err != nil {
		return false
	}
	switch oauthErr.Error {
	case "invalid_grant", "invalid_client", "unauthorized_client", "invalid_request":
		return true
	default:
		return false
	}
}

func discoverTokenEndpointForServer(ctx context.Context, srvURL string) (string, error) {
	httpClient := &http.Client{Timeout: 30 * time.Second}

	type resourceMeta struct {
		AuthorizationServers []string ` + "`" + `json:"authorization_servers"` + "`" + `
	}
	type authMeta struct {
		TokenEndpoint string ` + "`" + `json:"token_endpoint"` + "`" + `
	}

	authServers := []string{discoveryServerRoot(srvURL)}
	for _, wk := range oauthWellKnownURLsForDiscovery(srvURL, "oauth-protected-resource") {
		req, err := http.NewRequestWithContext(ctx, "GET", wk, nil)
		if err != nil {
			continue
		}
		resp, err := httpClient.Do(req)
		if err != nil {
			continue
		}
		if resp.StatusCode != http.StatusOK {
			resp.Body.Close()
			continue
		}
		var meta resourceMeta
		err = json.NewDecoder(resp.Body).Decode(&meta)
		resp.Body.Close()
		if err == nil && len(meta.AuthorizationServers) > 0 {
			authServers = meta.AuthorizationServers
			break
		}
	}

	for _, authServer := range authServers {
		for _, wk := range oauthWellKnownURLsForDiscovery(authServer, "oauth-authorization-server") {
			req, err := http.NewRequestWithContext(ctx, "GET", wk, nil)
			if err != nil {
				continue
			}
			resp, err := httpClient.Do(req)
			if err != nil {
				continue
			}
			if resp.StatusCode != http.StatusOK {
				resp.Body.Close()
				continue
			}
			var meta authMeta
			err = json.NewDecoder(resp.Body).Decode(&meta)
			resp.Body.Close()
			if err == nil && meta.TokenEndpoint != "" {
				return meta.TokenEndpoint, nil
			}
		}
	}

	return "", fmt.Errorf("could not discover token endpoint for %s", srvURL)
}

func discoveryServerRoot(rawURL string) string {
	u, err := url.Parse(rawURL)
	if err != nil || u.Scheme == "" || u.Host == "" {
		return rawURL
	}
	return u.Scheme + "://" + u.Host
}

func oauthWellKnownURLsForDiscovery(rawURL, suffix string) []string {
	u, err := url.Parse(rawURL)
	if err != nil {
		return nil
	}
	path := strings.TrimRight(u.Path, "/")
	base := fmt.Sprintf("%s://%s/.well-known/%s", u.Scheme, u.Host, suffix)
	if path == "" {
		return []string{base}
	}
	return []string{base + "/" + strings.TrimLeft(path, "/"), base}
}

// --- S2S OAuth2 ---

func s2sAuthenticate(ctx context.Context, tokenEndpoint, clientID, clientSecret string) (string, int, error) {
	form := url.Values{
		"grant_type":    {"client_credentials"},
		"client_id":     {clientID},
		"client_secret": {clientSecret},
	}

	req, err := http.NewRequestWithContext(ctx, "POST", tokenEndpoint, strings.NewReader(form.Encode()))
	if err != nil {
		return "", 0, err
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	resp, err := (&http.Client{Timeout: 30 * time.Second}).Do(req)
	if err != nil {
		return "", 0, fmt.Errorf("S2S token request: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
		body, _ := io.ReadAll(resp.Body)
		return "", 0, fmt.Errorf("S2S token endpoint returned %d: %s", resp.StatusCode, string(body))
	}

	var tokenResp struct {
		AccessToken string ` + "`" + `json:"access_token"` + "`" + `
		ExpiresIn   int    ` + "`" + `json:"expires_in"` + "`" + `
	}
	if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
		return "", 0, fmt.Errorf("parse S2S response: %w", err)
	}
	if tokenResp.AccessToken == "" {
		return "", 0, fmt.Errorf("S2S response missing access_token")
	}
	return tokenResp.AccessToken, tokenResp.ExpiresIn, nil
}

// resolveAuthProvider builds an auth provider using the following priority:
//  1. --auth-type + auth flags → explicit provider
//  2. --auth-token without --auth-type → bearer (backwards compat)
//  3. CLIHUB_AUTH_TOKEN env var → bearer
//  4. Credentials file → provider from stored auth_type (with token refresh)
//  5. No credentials → no auth
func resolveAuthProvider() authProvider {
	// 1. Explicit --auth-type
	if globalAuthType != "" {
		switch globalAuthType {
		case "bearer", "bearer_token":
			return &bearerTokenProvider{token: globalAuthToken}
		case "api_key":
			return &apiKeyProvider{token: globalAuthToken, headerName: globalAuthHeaderName}
		case "basic", "basic_auth":
			return &basicAuthProvider{username: globalAuthUsername, password: globalAuthPassword}
		default:
			return &noAuthProvider{}
		}
	}

	// 2. --auth-token without --auth-type → infer bearer
	if globalAuthToken != "" {
		return &bearerTokenProvider{token: globalAuthToken}
	}

	// 3. CLIHUB_AUTH_TOKEN env var
	if token := os.Getenv("CLIHUB_AUTH_TOKEN"); token != "" {
		return &bearerTokenProvider{token: token}
	}

	// 4. Credentials file
	credPath := os.Getenv("CLIHUB_CREDENTIALS_FILE")
	if credPath == "" {
		home, err := os.UserHomeDir()
		if err == nil {
			credPath = filepath.Join(home, ".clihub", "credentials.json")
		}
	}
	if credPath != "" {
		data, err := os.ReadFile(credPath)
		if err == nil {
			var creds credentialsFile
			if json.Unmarshal(data, &creds) == nil && creds.Servers != nil {
{{- if .IsHTTP}}
				if s, ok := creds.Servers[serverURL]; ok {
					authType := s.resolveAuthType()
					switch authType {
					case "bearer_token":
						return &bearerTokenProvider{token: s.Token}
					case "api_key":
						return &apiKeyProvider{token: s.Token, headerName: s.HeaderName}
					case "basic_auth":
						return &basicAuthProvider{username: s.Username, password: s.Password}
					case "oauth2":
						token := s.AccessToken
						if s.isExpired() {
							if s.RefreshToken != "" {
								refreshed, permanent, err := refreshOAuthToken(credPath, serverURL, s)
								if err != nil {
									fmt.Fprintf(os.Stderr, "Warning: token refresh failed: %s\n", err)
									if permanent {
										deleteCredential(credPath, serverURL)
										fmt.Fprintf(os.Stderr, "Stored credentials removed. Run ` + "`" + `%s auth` + "`" + ` to re-authenticate.\n", os.Args[0])
									} else {
										fmt.Fprintf(os.Stderr, "Stored credentials were kept (refresh error may be transient).\n")
									}
									return &noAuthProvider{}
								}
								token = refreshed
							} else {
								fmt.Fprintf(os.Stderr, "Warning: access token expired and no refresh token available.\n")
								deleteCredential(credPath, serverURL)
								fmt.Fprintf(os.Stderr, "Stored credentials removed. Run ` + "`" + `%s auth` + "`" + ` to re-authenticate.\n", os.Args[0])
								return &noAuthProvider{}
							}
						}
						return &bearerTokenProvider{token: token}
					case "s2s_oauth2":
						return &s2sOAuth2Provider{
							clientID: s.ClientID, clientSecret: s.ClientSecret,
							tokenEndpoint: s.TokenEndpoint,
						}
					case "google_sa":
						return &googleSAProvider{keyFile: s.KeyFile, scopes: s.Scopes}
					}
				}
{{- end}}
			}
		}
	}

	// 5. No auth
	return &noAuthProvider{}
}

// --- Credential helpers ---

func defaultCredPath() string {
	if p := os.Getenv("CLIHUB_CREDENTIALS_FILE"); p != "" {
		return p
	}
	home, err := os.UserHomeDir()
	if err != nil {
		return ""
	}
	return filepath.Join(home, ".clihub", "credentials.json")
}

func writeCredentials(path string, creds *credentialsFile) error {
	if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
		return err
	}
	if creds.Version < 2 {
		creds.Version = 2
	}
	data, err := json.MarshalIndent(creds, "", "  ")
	if err != nil {
		return err
	}
	return os.WriteFile(path, data, 0600)
}

func deleteCredential(credPath, srvURL string) {
	if credPath == "" {
		return
	}
	data, err := os.ReadFile(credPath)
	if err != nil {
		return
	}
	var creds credentialsFile
	if json.Unmarshal(data, &creds) != nil || creds.Servers == nil {
		return
	}
	delete(creds.Servers, srvURL)
	if updated, err := json.MarshalIndent(creds, "", "  "); err == nil {
		os.WriteFile(credPath, updated, 0600)
	}
}

// --- Auth command ---
{{- if .IsHTTP}}

func cmdAuth() *cobra.Command {
	var flagToken string
	var flagClientID string
	var flagClientSecret string

	cmd := &cobra.Command{
		Use:   "auth",
		Short: "Authenticate with the MCP server",
		Long:  "Run the OAuth browser flow or manually provide credentials for " + serverURL,
		SilenceUsage:  true,
		SilenceErrors: true,
		RunE: func(cmd *cobra.Command, args []string) error {
			if flagToken != "" {
				return saveManualToken(flagToken)
			}
			return runOAuthFlow(flagClientID, flagClientSecret)
		},
	}

	cmd.Flags().StringVar(&flagToken, "token", "", "manually save a bearer token")
	cmd.Flags().StringVar(&flagClientID, "client-id", "", "pre-registered OAuth client ID")
	cmd.Flags().StringVar(&flagClientSecret, "client-secret", "", "pre-registered OAuth client secret")

	cmd.AddCommand(&cobra.Command{
		Use:   "status",
		Short: "Show current authentication status",
		SilenceUsage:  true,
		SilenceErrors: true,
		RunE: func(cmd *cobra.Command, args []string) error {
			return showAuthStatus()
		},
	})

	cmd.AddCommand(&cobra.Command{
		Use:   "logout",
		Short: "Remove stored credentials",
		SilenceUsage:  true,
		SilenceErrors: true,
		RunE: func(cmd *cobra.Command, args []string) error {
			credPath := defaultCredPath()
			if credPath == "" {
				return fmt.Errorf("could not determine credentials path")
			}
			deleteCredential(credPath, serverURL)
			fmt.Printf("Credentials removed for %s\n", serverURL)
			return nil
		},
	})

	return cmd
}

func saveManualToken(token string) error {
	credPath := defaultCredPath()
	if credPath == "" {
		return fmt.Errorf("could not determine credentials path")
	}
	data, _ := os.ReadFile(credPath)
	var creds credentialsFile
	if json.Unmarshal(data, &creds) != nil || creds.Servers == nil {
		creds = credentialsFile{Version: 2, Servers: map[string]serverCredential{}}
	}
	creds.Servers[serverURL] = serverCredential{
		AuthType: "bearer_token",
		Type:     "bearer",
		Token:    token,
	}
	if err := writeCredentials(credPath, &creds); err != nil {
		return fmt.Errorf("save credentials: %w", err)
	}
	fmt.Printf("Token saved for %s\n", serverURL)
	return nil
}

func showAuthStatus() error {
	credPath := defaultCredPath()
	if credPath == "" {
		fmt.Println("Not authenticated (no credentials path)")
		return nil
	}
	data, err := os.ReadFile(credPath)
	if err != nil {
		fmt.Println("Not authenticated")
		return nil
	}
	var creds credentialsFile
	if json.Unmarshal(data, &creds) != nil || creds.Servers == nil {
		fmt.Println("Not authenticated")
		return nil
	}
	s, ok := creds.Servers[serverURL]
	if !ok {
		fmt.Println("Not authenticated")
		return nil
	}
	authType := s.resolveAuthType()
	fmt.Printf("Server:    %s\n", serverURL)
	fmt.Printf("Auth type: %s\n", authType)
	switch authType {
	case "oauth2":
		if s.isExpired() {
			fmt.Println("Status:    expired")
		} else {
			fmt.Println("Status:    authenticated")
		}
		if s.ExpiresAt != "" {
			fmt.Printf("Expires:   %s\n", s.ExpiresAt)
		}
	default:
		fmt.Println("Status:    authenticated")
	}
	return nil
}

func runOAuthFlow(clientID, clientSecret string) error {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
	defer cancel()
	httpClient := &http.Client{Timeout: 30 * time.Second}

	fmt.Println("Discovering OAuth endpoints...")

	// Step 1: Discover protected resource metadata
	var authServerURL string
	var scopeFromResource string
	resMeta, err := fetchResourceMeta(ctx, httpClient, serverURL)
	if err == nil && len(resMeta.AuthorizationServers) > 0 {
		authServerURL = resMeta.AuthorizationServers[0]
		if len(resMeta.ScopesSupported) > 0 {
			scopeFromResource = strings.Join(resMeta.ScopesSupported, " ")
		}
	} else {
		// Fallback: use server root as auth server
		u, _ := url.Parse(serverURL)
		if u != nil {
			authServerURL = u.Scheme + "://" + u.Host
		} else {
			authServerURL = serverURL
		}
	}

	// Step 2: Discover auth server metadata
	authMeta, err := fetchAuthMeta(ctx, httpClient, authServerURL)
	if err != nil {
		return fmt.Errorf("OAuth discovery failed: %w\n\nIf this server doesn't support OAuth, provide a token manually:\n  %s auth --token <YOUR_TOKEN>", err, os.Args[0])
	}
	fmt.Printf("Found authorization server: %s\n", authMeta.Issuer)

	// Determine scope
	scope := "mcp:tools"
	if scopeFromResource != "" {
		scope = scopeFromResource
	} else if len(authMeta.ScopesSupported) > 0 {
		scope = strings.Join(authMeta.ScopesSupported, " ")
	}

	// Step 3: Start callback server
	listener, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		return fmt.Errorf("could not start callback server: %w", err)
	}
	port := listener.Addr().(*net.TCPAddr).Port
	redirectURI := fmt.Sprintf("http://127.0.0.1:%d/callback", port)
	resultCh := make(chan oauthCallbackResult, 1)
	var cbOnce sync.Once

	mux := http.NewServeMux()
	mux.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) {
		code := r.URL.Query().Get("code")
		oauthErr := r.URL.Query().Get("error")
		state := r.URL.Query().Get("state")
		var res oauthCallbackResult
		if oauthErr != "" {
			desc := r.URL.Query().Get("error_description")
			w.WriteHeader(http.StatusBadRequest)
			fmt.Fprintf(w, "<html><body><h1>Authorization failed</h1><p>%s: %s</p></body></html>", oauthErr, desc)
			res = oauthCallbackResult{err: fmt.Errorf("%s: %s", oauthErr, desc)}
		} else if code == "" {
			w.WriteHeader(http.StatusBadRequest)
			fmt.Fprint(w, "<html><body><h1>Missing authorization code</h1></body></html>")
			res = oauthCallbackResult{err: fmt.Errorf("missing authorization code")}
		} else {
			w.WriteHeader(http.StatusOK)
			fmt.Fprint(w, "<html><body><h1>Authentication successful!</h1><p>You can close this window.</p></body></html>")
			res = oauthCallbackResult{code: code, state: state}
		}
		cbOnce.Do(func() {
			resultCh <- res
		})
	})
	srv := &http.Server{Handler: mux}
	go srv.Serve(listener)
	defer srv.Close()

	// Step 4: Client credentials (DCR or pre-registered)
	if clientID == "" && authMeta.RegistrationEndpoint != "" {
		fmt.Println("Registering OAuth client...")
		regID, regSecret, err := registerOAuthClient(ctx, httpClient, authMeta.RegistrationEndpoint, redirectURI, scope)
		if err != nil {
			return fmt.Errorf("client registration failed: %w\n\n"+
				"This server requires a pre-registered OAuth app.\n"+
				"  1. Register at the provider's portal (%s)\n"+
				"  2. Set redirect URI to: http://127.0.0.1/callback\n"+
				"  3. Run: %s auth --client-id <ID> --client-secret <SECRET>",
				err, authMeta.Issuer, os.Args[0])
		}
		clientID = regID
		clientSecret = regSecret
	} else if clientID == "" {
		return fmt.Errorf("this server requires a pre-registered OAuth app\n\n"+
			"  1. Register at the provider's portal (%s)\n"+
			"  2. Set redirect URI to: http://127.0.0.1/callback\n"+
			"  3. Run: %s auth --client-id <ID> --client-secret <SECRET>",
			authMeta.Issuer, os.Args[0])
	}

	// Step 5: PKCE
	verifier, err := generatePKCEVerifier()
	if err != nil {
		return fmt.Errorf("PKCE generation failed: %w", err)
	}
	challenge := generatePKCEChallenge(verifier)

	// Step 6: State
	state, err := generateOAuthState()
	if err != nil {
		return fmt.Errorf("state generation failed: %w", err)
	}

	// Step 7: Build authorization URL
	authURL, err := url.Parse(authMeta.AuthorizationEndpoint)
	if err != nil {
		return fmt.Errorf("invalid authorization endpoint: %w", err)
	}
	q := authURL.Query()
	q.Set("client_id", clientID)
	q.Set("redirect_uri", redirectURI)
	q.Set("response_type", "code")
	q.Set("code_challenge", challenge)
	q.Set("code_challenge_method", "S256")
	q.Set("state", state)
	q.Set("scope", scope)
	authURL.RawQuery = q.Encode()

	// Step 8: Open browser
	fmt.Println("Opening browser for authentication...")
	openDefaultBrowser(authURL.String())
	fmt.Printf("If the browser doesn't open, visit:\n%s\n\nWaiting for authorization...\n", authURL.String())

	// Step 9: Wait for callback
	select {
	case <-ctx.Done():
		return fmt.Errorf("timed out waiting for authorization")
	case result := <-resultCh:
		if result.err != nil {
			return fmt.Errorf("authorization failed: %w", result.err)
		}
		if result.state != state {
			return fmt.Errorf("OAuth state mismatch (possible CSRF)")
		}

		// Step 10: Exchange code for tokens
		fmt.Println("Exchanging authorization code for tokens...")
		form := url.Values{
			"grant_type":    {"authorization_code"},
			"code":          {result.code},
			"redirect_uri":  {redirectURI},
			"client_id":     {clientID},
			"code_verifier": {verifier},
		}
		if clientSecret != "" {
			form.Set("client_secret", clientSecret)
		}

		tokenReq, err := http.NewRequestWithContext(ctx, "POST", authMeta.TokenEndpoint, strings.NewReader(form.Encode()))
		if err != nil {
			return fmt.Errorf("token exchange: %w", err)
		}
		tokenReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")

		tokenResp, err := httpClient.Do(tokenReq)
		if err != nil {
			return fmt.Errorf("token exchange request: %w", err)
		}
		defer tokenResp.Body.Close()

		if tokenResp.StatusCode != http.StatusOK && tokenResp.StatusCode != http.StatusCreated {
			body, _ := io.ReadAll(tokenResp.Body)
			return fmt.Errorf("token exchange failed (%d): %s", tokenResp.StatusCode, string(body))
		}

		var tokens struct {
			AccessToken  string ` + "`" + `json:"access_token"` + "`" + `
			RefreshToken string ` + "`" + `json:"refresh_token"` + "`" + `
			ExpiresIn    int    ` + "`" + `json:"expires_in"` + "`" + `
			Scope        string ` + "`" + `json:"scope"` + "`" + `
		}
		if err := json.NewDecoder(tokenResp.Body).Decode(&tokens); err != nil {
			return fmt.Errorf("parse token response: %w", err)
		}
		if tokens.AccessToken == "" {
			return fmt.Errorf("token response missing access_token")
		}

		// Save to credential store
		credPath := defaultCredPath()
		if credPath == "" {
			return fmt.Errorf("could not determine credentials path")
		}
		data, _ := os.ReadFile(credPath)
		var creds credentialsFile
		if json.Unmarshal(data, &creds) != nil || creds.Servers == nil {
			creds = credentialsFile{Version: 2, Servers: map[string]serverCredential{}}
		}
		entry := serverCredential{
			AuthType:      "oauth2",
			Type:          "oauth",
			AccessToken:   tokens.AccessToken,
			RefreshToken:  tokens.RefreshToken,
			ClientID:      clientID,
			ClientSecret:  clientSecret,
			TokenEndpoint: authMeta.TokenEndpoint,
			Scope:         tokens.Scope,
		}
		if tokens.ExpiresIn > 0 {
			entry.ExpiresAt = time.Now().Add(time.Duration(tokens.ExpiresIn) * time.Second).Format(time.RFC3339)
		}
		creds.Servers[serverURL] = entry
		if err := writeCredentials(credPath, &creds); err != nil {
			return fmt.Errorf("save credentials: %w", err)
		}

		fmt.Println("Authentication successful! Credentials saved.")
		return nil
	}
}

type oauthCallbackResult struct {
	code, state string
	err         error
}

type oauthServerMeta struct {
	Issuer                string   ` + "`" + `json:"issuer"` + "`" + `
	AuthorizationEndpoint string   ` + "`" + `json:"authorization_endpoint"` + "`" + `
	TokenEndpoint         string   ` + "`" + `json:"token_endpoint"` + "`" + `
	RegistrationEndpoint  string   ` + "`" + `json:"registration_endpoint"` + "`" + `
	ScopesSupported       []string ` + "`" + `json:"scopes_supported"` + "`" + `
}

type oauthResourceMeta struct {
	AuthorizationServers []string ` + "`" + `json:"authorization_servers"` + "`" + `
	ScopesSupported      []string ` + "`" + `json:"scopes_supported"` + "`" + `
}

func fetchResourceMeta(ctx context.Context, client *http.Client, srvURL string) (*oauthResourceMeta, error) {
	for _, wk := range oauthWellKnownURLs(srvURL, "oauth-protected-resource") {
		req, err := http.NewRequestWithContext(ctx, "GET", wk, nil)
		if err != nil {
			continue
		}
		resp, err := client.Do(req)
		if err != nil {
			continue
		}
		if resp.StatusCode != http.StatusOK {
			resp.Body.Close()
			continue
		}
		var meta oauthResourceMeta
		err = json.NewDecoder(resp.Body).Decode(&meta)
		resp.Body.Close()
		if err != nil {
			continue
		}
		if len(meta.AuthorizationServers) > 0 {
			return &meta, nil
		}
	}
	return nil, fmt.Errorf("no resource metadata found")
}

func fetchAuthMeta(ctx context.Context, client *http.Client, authSrvURL string) (*oauthServerMeta, error) {
	for _, wk := range oauthWellKnownURLs(authSrvURL, "oauth-authorization-server") {
		req, err := http.NewRequestWithContext(ctx, "GET", wk, nil)
		if err != nil {
			continue
		}
		resp, err := client.Do(req)
		if err != nil {
			continue
		}
		if resp.StatusCode != http.StatusOK {
			resp.Body.Close()
			continue
		}
		var meta oauthServerMeta
		err = json.NewDecoder(resp.Body).Decode(&meta)
		resp.Body.Close()
		if err != nil {
			continue
		}
		if meta.AuthorizationEndpoint != "" && meta.TokenEndpoint != "" {
			return &meta, nil
		}
	}
	return nil, fmt.Errorf("no auth server metadata found at %s", authSrvURL)
}

func oauthWellKnownURLs(rawURL, suffix string) []string {
	u, err := url.Parse(rawURL)
	if err != nil {
		return nil
	}
	path := strings.TrimRight(u.Path, "/")
	base := fmt.Sprintf("%s://%s/.well-known/%s", u.Scheme, u.Host, suffix)
	if path == "" {
		return []string{base}
	}
	return []string{base + path, base}
}

func registerOAuthClient(ctx context.Context, client *http.Client, endpoint, redirectURI, scope string) (string, string, error) {
	body, _ := json.Marshal(map[string]interface{}{
		"client_name":                {{quote .CLIName}},
		"redirect_uris":             []string{redirectURI},
		"grant_types":               []string{"authorization_code", "refresh_token"},
		"response_types":            []string{"code"},
		"token_endpoint_auth_method": "none",
		"scope":                      scope,
	})
	req, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewReader(body))
	if err != nil {
		return "", "", err
	}
	req.Header.Set("Content-Type", "application/json")
	resp, err := client.Do(req)
	if err != nil {
		return "", "", err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
		b, _ := io.ReadAll(resp.Body)
		return "", "", fmt.Errorf("registration failed (%d): %s", resp.StatusCode, string(b))
	}
	var reg struct {
		ClientID     string ` + "`" + `json:"client_id"` + "`" + `
		ClientSecret string ` + "`" + `json:"client_secret"` + "`" + `
	}
	if err := json.NewDecoder(resp.Body).Decode(&reg); err != nil {
		return "", "", err
	}
	if reg.ClientID == "" {
		return "", "", fmt.Errorf("server returned empty client_id")
	}
	return reg.ClientID, reg.ClientSecret, nil
}

func generatePKCEVerifier() (string, error) {
	const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"
	b := make([]byte, 64)
	if _, err := rand.Read(b); err != nil {
		return "", err
	}
	for i := range b {
		b[i] = chars[int(b[i])%len(chars)]
	}
	return string(b), nil
}

func generatePKCEChallenge(verifier string) string {
	h := sha256.Sum256([]byte(verifier))
	return base64.RawURLEncoding.EncodeToString(h[:])
}

func generateOAuthState() (string, error) {
	b := make([]byte, 32)
	if _, err := rand.Read(b); err != nil {
		return "", err
	}
	return hex.EncodeToString(b), nil
}

func openDefaultBrowser(url string) {
	switch runtime.GOOS {
	case "darwin":
		exec.Command("open", url).Start()
	case "linux":
		exec.Command("xdg-open", url).Start()
	case "windows":
		exec.Command("cmd", "/c", "start", "", url).Start()
	}
}
{{- end}}
`

const goModTemplateSource = `module {{.CLIName}}

go 1.24

require (
	github.com/mark3labs/mcp-go v0.44.0
	github.com/spf13/cobra v1.10.2
	golang.org/x/oauth2 v0.35.0
)
`
