package app

import (
	"bufio"
	"context"
	"flag"
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"strings"
	"sync"

	"golang.org/x/term"

	"github.com/ClareAI/astra-insight-mcp/internal/agent"
	"github.com/ClareAI/astra-insight-mcp/internal/config"
	"github.com/ClareAI/astra-insight-mcp/internal/gateway"
	"github.com/ClareAI/astra-insight-mcp/internal/llm"
	"github.com/ClareAI/astra-insight-mcp/internal/mcp"
	"github.com/ClareAI/astra-insight-mcp/internal/oauth"
	"github.com/ClareAI/astra-insight-mcp/internal/tools"
)

// REPL ANSI colors
const (
	replReset   = "\033[0m"
	replBold    = "\033[1m"
	replDim     = "\033[2m"
	replCyan    = "\033[36m"
	replGreen   = "\033[32m"
	replYellow  = "\033[33m"
	replMagenta = "\033[35m"
	replBlue    = "\033[34m"
)

// RunREPL starts the interactive agent optimizer chat loop.
func RunREPL(debug bool) {
	llmCfg := config.LoadLLMForCLI()

	needsSave := false

	if llmCfg.APIKey == "" {
		if !term.IsTerminal(int(os.Stdin.Fd())) || !term.IsTerminal(int(os.Stderr.Fd())) {
			fmt.Fprintf(os.Stderr, "%s✗ No LLM API key found (~/.astra/llm.json or LLM_API_KEY / OPENAI_API_KEY), and stdin is not a TTY.%s\n", replYellow, replReset)
			fmt.Fprintf(os.Stderr, "  Run: astra auth   (interactive key setup)\n")
			fmt.Fprintf(os.Stderr, "  Or set LLM_API_KEY, LLM_BASE_URL, LLM_MODEL in the environment.\n")
			os.Exit(1)
		}
		prov := promptLLMProviderUntilValid()
		key, err := readHiddenAPIKey("Enter LLM API key (input hidden):")
		if err != nil {
			fmt.Fprintf(os.Stderr, "%s✗ Failed to read input: %v%s\n", replYellow, err, replReset)
			os.Exit(1)
		}
		llmCfg.APIKey = key
		if llmCfg.APIKey == "" {
			fmt.Fprintf(os.Stderr, "%s✗ API key cannot be empty%s\n", replYellow, replReset)
			os.Exit(1)
		}
		llmCfg.BaseURL = prov.DefaultBaseURL()
		llmCfg.Model = prov.DefaultModel()
		llmCfg.FastModel = config.DeriveFastModel(llmCfg.Model)
		needsSave = true
	}

	if llmCfg.AstraAPIKey == "" {
		if astraKey := promptAstraPlatformKey(); astraKey != "" {
			llmCfg.AstraAPIKey = astraKey
			os.Setenv("ASTRA_API_KEY", astraKey)
			needsSave = true
		}
	}

	if needsSave {
		if err := config.SaveLLMFile(llmCfg); err != nil {
			fmt.Fprintf(os.Stderr, "%s⚠ Could not save config: %v%s\n", replDim, err, replReset)
		} else {
			path, _ := config.LLMFilePath()
			fmt.Fprintf(os.Stderr, "%s✓ Saved to %s%s\n", replGreen, path, replReset)
		}
	}

	cfg := config.Load()
	gwClient := gateway.NewClient(cfg.GatewayBaseURL)
	if debug {
		gwClient.SetDebug(true)
	}
	llmClient := llm.NewClient(llmCfg.APIKey, llmCfg.BaseURL, llmCfg.Model)
	var fastClient *llm.Client
	if llmCfg.FastModel != "" && llmCfg.FastModel != llmCfg.Model {
		fastClient = llmClient.WithModel(llmCfg.FastModel)
	}

	agent.SetRegisterFunc(func(srv *mcp.Server, gw *gateway.Client) agent.SkillPruner {
		return tools.RegisterCore(srv, gw)
	})

	printREPLBanner()

	tracker := newUsageTracker(llmCfg.Model, debug)

	makeCallbacks := func() agent.Callbacks {
		return agent.Callbacks{
			OnContent: func(s string) {
				fmt.Print(s)
			},
			OnToolCall: func(name string, args map[string]interface{}) {
				desc := formatToolCall(name, args)
				fmt.Printf("\n%s%s⚡ %s%s\n", replDim, replYellow, desc, replReset)
			},
			OnToolDone: func(name string, result string) {
				fmt.Printf("%s%s   ✓ done (%d chars)%s\n", replDim, replGreen, len(result), replReset)
			},
			OnUsage: tracker.onUsage,
		}
	}

	agentOpts := agent.Options{
		FastClient:  fastClient,
		RecipeStore: agent.NewFileRecipeStore(""),
	}
	a := agent.New(llmClient, gwClient, makeCallbacks(), agentOpts)

	reader := bufio.NewReader(os.Stdin)
	ctx := context.Background()

	for {
		fmt.Printf("\n%s%s❯%s ", replBold, replCyan, replReset)
		input, err := reader.ReadString('\n')
		if err != nil {
			break
		}
		input = strings.TrimSpace(input)
		if input == "" {
			continue
		}

		if input == `"""` || strings.HasPrefix(input, `"""`) {
			input = readMultiLine(reader, input)
		}

		lower := strings.ToLower(strings.TrimSpace(input))
		if lower == "exit" || lower == "quit" || lower == "q" {
			if debug {
				tracker.printSummary()
			}
			fmt.Printf("%s👋 Bye!%s\n", replDim, replReset)
			break
		}
		if lower == "clear" || lower == "reset" {
			a = agent.New(llmClient, gwClient, makeCallbacks(), agentOpts)
			fmt.Printf("%s🔄 Conversation reset%s\n", replDim, replReset)
			continue
		}
		if lower == "usage" {
			tracker.printSummary()
			continue
		}
		if lower == "help" || lower == "/help" {
			printREPLHelp()
			continue
		}

		input = expandFileRefs(input)

		fmt.Println()
		if err := a.Run(ctx, input); err != nil {
			fmt.Fprintf(os.Stderr, "\n%s%s✗ %s%s\n", replBold, replYellow, err.Error(), replReset)
		}
		fmt.Println()
	}
}

var fileRefPattern = regexp.MustCompile(`@((?:[~./])?[^\s,;]+)`)

func expandFileRefs(input string) string {
	matches := fileRefPattern.FindAllStringSubmatchIndex(input, -1)
	if len(matches) == 0 {
		return input
	}

	var result strings.Builder
	lastEnd := 0

	for _, loc := range matches {
		fullStart, fullEnd := loc[0], loc[1]
		pathStart, pathEnd := loc[2], loc[3]
		path := input[pathStart:pathEnd]

		if strings.HasPrefix(path, "~") {
			if home, err := os.UserHomeDir(); err == nil {
				path = filepath.Join(home, path[1:])
			}
		}

		absPath, err := filepath.Abs(path)
		if err != nil {
			result.WriteString(input[lastEnd:fullEnd])
			lastEnd = fullEnd
			continue
		}

		content, err := os.ReadFile(absPath)
		if err != nil {
			fmt.Fprintf(os.Stderr, "%s⚠ cannot read %s: %s%s\n", replYellow, path, err.Error(), replReset)
			result.WriteString(input[lastEnd:fullEnd])
			lastEnd = fullEnd
			continue
		}

		sizeKB := float64(len(content)) / 1024.0

		fmt.Fprintf(os.Stderr, "%s📎 %s (%.1f KB, %d lines)%s\n",
			replDim, absPath, sizeKB, strings.Count(string(content), "\n")+1, replReset)

		result.WriteString(input[lastEnd:fullStart])
		result.WriteString(fmt.Sprintf("\n\n<file path=%q>\n%s\n</file>\n\n", absPath, strings.TrimRight(string(content), "\n")))
		lastEnd = fullEnd
	}

	result.WriteString(input[lastEnd:])
	return result.String()
}

func readMultiLine(reader *bufio.Reader, firstLine string) string {
	fmt.Fprintf(os.Stderr, "%s(multi-line mode — type \"\"\" on a new line to finish)%s\n", replDim, replReset)

	var lines []string

	after := strings.TrimPrefix(firstLine, `"""`)
	after = strings.TrimSpace(after)
	if after != "" {
		lines = append(lines, after)
	}

	for {
		fmt.Printf("%s%s…%s ", replDim, replCyan, replReset)
		line, err := reader.ReadString('\n')
		if err != nil {
			break
		}
		trimmed := strings.TrimRight(line, "\n\r")

		if strings.TrimSpace(trimmed) == `"""` {
			break
		}
		lines = append(lines, trimmed)
	}

	return strings.Join(lines, "\n")
}

func printREPLHelp() {
	fmt.Printf(`
%s%sCommands:%s
  %squit%s / %sexit%s       Exit the CLI
  %sclear%s / %sreset%s     Reset conversation history
  %susage%s              Show token usage summary
  %shelp%s               Show this help

%s%sSubcommands (run from shell):%s
  %sastra auth%s               Set up or change LLM + Astra keys
  %sastra auth clear%s         Remove all saved keys and start fresh
  %sastra auth status%s        Show what's configured

%s%sSyntax:%s
  %s@path/to/file%s     Attach file content to your message
                     Supports relative, absolute, and ~ paths
                     Multiple files: %s@a.txt describe @b.json%s

  %s"""%s                Start multi-line input mode
                     Type %s"""%s on a new line to finish and send

%s%sExamples:%s
  %s❯%s Analyze this agent's instruction @instruction.txt
  %s❯%s Compare @v1.txt and @v2.txt, which is better?
  %s❯%s """
  %s…%s Here is a long prompt
  %s…%s that spans multiple lines
  %s…%s """
`,
		replBold, replCyan, replReset,
		replGreen, replReset, replGreen, replReset,
		replGreen, replReset, replGreen, replReset,
		replGreen, replReset,
		replGreen, replReset,
		replBold, replCyan, replReset,
		replGreen, replReset,
		replGreen, replReset,
		replGreen, replReset,
		replBold, replCyan, replReset,
		replYellow, replReset,
		replYellow, replReset,
		replYellow, replReset,
		replYellow, replReset,
		replBold, replCyan, replReset,
		replCyan, replReset,
		replCyan, replReset,
		replCyan, replReset,
		replDim, replReset,
		replDim, replReset,
		replDim, replReset,
	)
}

var modelPricing = map[string][2]float64{
	"gpt-4o":                     {2.50, 10.00},
	"gpt-4o-mini":                {0.15, 0.60},
	"gpt-4.1":                    {2.00, 8.00},
	"gpt-4.1-mini":               {0.40, 1.60},
	"gpt-4.1-nano":               {0.10, 0.40},
	"o3":                         {2.00, 8.00},
	"o3-mini":                    {1.10, 4.40},
	"o4-mini":                    {1.10, 4.40},
	"claude-sonnet-4-20250514":   {3.00, 15.00},
	"claude-opus-4-20250514":     {15.00, 75.00},
	"anthropic/claude-sonnet-4":  {3.00, 15.00},
	"anthropic/claude-opus-4":    {15.00, 75.00},
	"anthropic/claude-opus-4.6":  {15.00, 75.00},
	"anthropic/claude-3.5-haiku": {0.80, 4.00},
	"claude-3.5-haiku":           {0.80, 4.00},
	"gemini-2.5-pro":             {1.25, 10.00},
	"gemini-2.5-flash":           {0.15, 0.60},
	"google/gemini-2.5-pro":      {1.25, 10.00},
	"google/gemini-2.5-flash":    {0.15, 0.60},
	"deepseek/deepseek-chat-v3":  {0.27, 1.10},
	"deepseek/deepseek-r1":       {0.55, 2.19},
}

type usageTracker struct {
	mu              sync.Mutex
	model           string
	debug           bool
	calls           int
	totalPrompt     int
	totalCompl      int
	totalTokens     int
	totalCached     int
	totalCacheWrite int
	totalCost       float64
}

func newUsageTracker(model string, debug bool) *usageTracker {
	return &usageTracker{model: model, debug: debug}
}

func (t *usageTracker) onUsage(u llm.Usage) {
	t.mu.Lock()
	defer t.mu.Unlock()

	t.calls++
	t.totalPrompt += u.PromptTokens
	t.totalCompl += u.CompletionTokens
	t.totalTokens += u.TotalTokens

	cached, cacheWrite := 0, 0
	if u.PromptTokensDetail != nil {
		cached = u.PromptTokensDetail.CachedTokens
		cacheWrite = u.PromptTokensDetail.CacheWriteTokens
		t.totalCached += cached
		t.totalCacheWrite += cacheWrite
	}

	model := u.Model
	if model == "" {
		model = t.model
	}
	cost := t.estimateCostForModel(model, u.PromptTokens, u.CompletionTokens, cached, cacheWrite)
	t.totalCost += cost

	if t.debug {
		fmt.Fprintf(os.Stderr, "\n%s───── TOKEN USAGE ─────%s\n", replDim, replReset)
		fmt.Fprintf(os.Stderr, "%s  prompt: %d  completion: %d  total: %d%s\n",
			replDim, u.PromptTokens, u.CompletionTokens, u.TotalTokens, replReset)
		if cached > 0 || cacheWrite > 0 {
			fmt.Fprintf(os.Stderr, "%s  cache:  %d read (90%% off) | %d write%s\n",
				replDim, cached, cacheWrite, replReset)
		}
		if cost > 0 {
			fmt.Fprintf(os.Stderr, "%s  cost:   $%.4f  (session: $%.4f)%s\n", replDim, cost, t.totalCost, replReset)
		}
		fmt.Fprintf(os.Stderr, "%s───────────────────────%s\n", replDim, replReset)
	}
}

func (t *usageTracker) cacheSavings() float64 {
	prices, ok := modelPricing[t.model]
	if !ok {
		for prefix, p := range modelPricing {
			if strings.Contains(t.model, prefix) || strings.Contains(prefix, t.model) {
				prices = p
				ok = true
				break
			}
		}
	}
	if !ok {
		return 0
	}
	return (float64(t.totalCached) / 1_000_000) * prices[0] * 0.9
}

func (t *usageTracker) estimateCostForModel(model string, prompt, completion, cached, cacheWrite int) float64 {
	prices, ok := modelPricing[model]
	if !ok {
		for prefix, p := range modelPricing {
			if strings.Contains(model, prefix) || strings.Contains(prefix, model) {
				prices = p
				ok = true
				break
			}
		}
	}
	if !ok {
		return 0
	}
	inputPrice, outputPrice := prices[0], prices[1]
	uncachedPrompt := prompt - cached - cacheWrite
	cost := (float64(uncachedPrompt) / 1_000_000) * inputPrice
	cost += (float64(cached) / 1_000_000) * inputPrice * 0.1
	cost += (float64(cacheWrite) / 1_000_000) * inputPrice * 1.25
	cost += (float64(completion) / 1_000_000) * outputPrice
	return cost
}

func (t *usageTracker) printSummary() {
	t.mu.Lock()
	defer t.mu.Unlock()

	fmt.Fprintf(os.Stderr, "\n%s%s═══ Session Usage Summary ═══%s\n", replBold, replCyan, replReset)
	fmt.Fprintf(os.Stderr, "%s  calls:      %d%s\n", replDim, t.calls, replReset)
	fmt.Fprintf(os.Stderr, "%s  prompt:     %d tokens%s\n", replDim, t.totalPrompt, replReset)
	fmt.Fprintf(os.Stderr, "%s  completion: %d tokens%s\n", replDim, t.totalCompl, replReset)
	fmt.Fprintf(os.Stderr, "%s  total:      %d tokens%s\n", replDim, t.totalTokens, replReset)
	if t.totalCached > 0 || t.totalCacheWrite > 0 {
		fmt.Fprintf(os.Stderr, "%s  cache read: %d tokens (saved ~$%.4f)%s\n",
			replDim, t.totalCached, t.cacheSavings(), replReset)
		fmt.Fprintf(os.Stderr, "%s  cache write:%d tokens%s\n", replDim, t.totalCacheWrite, replReset)
	}
	if t.totalCost > 0 {
		fmt.Fprintf(os.Stderr, "%s  est. cost:  $%.4f%s\n", replDim, t.totalCost, replReset)
	} else {
		fmt.Fprintf(os.Stderr, "%s  est. cost:  (unknown pricing for current model)%s\n", replDim, replReset)
	}
	fmt.Fprintf(os.Stderr, "%s%s════════════════════════════%s\n", replBold, replCyan, replReset)
}

func printREPLBanner() {
	fmt.Printf("\n%s%s╭──────────────────────────────────────╮%s\n", replBold, replMagenta, replReset)
	fmt.Printf("%s%s│   Astra — Agent Optimizer (REPL)     │%s\n", replBold, replMagenta, replReset)
	fmt.Printf("%s%s╰──────────────────────────────────────╯%s\n", replBold, replMagenta, replReset)
	fmt.Printf("%s  core tools + skills on demand | @file to attach | \"\"\" for multi-line | 'help' for more%s\n", replDim, replReset)
}

func formatToolCall(name string, args map[string]interface{}) string {
	var parts []string
	for k, v := range args {
		if k == "api_key" {
			continue
		}
		s := fmt.Sprintf("%v", v)
		if len(s) > 60 {
			s = s[:60] + "…"
		}
		parts = append(parts, fmt.Sprintf("%s%s%s=%s", replBlue, k, replReset+replDim+replYellow, s))
	}
	if len(parts) == 0 {
		return name
	}
	return fmt.Sprintf("%s(%s)", name, strings.Join(parts, ", "))
}

// RunAuth handles `astra auth [subcommand]`.
//
//	astra auth              Interactive setup (LLM provider + Astra platform key)
//	astra auth clear        Remove all saved credentials
//	astra auth status       Show current auth configuration
//	astra auth --oauth ...  Browser OAuth PKCE (advanced)
//	astra auth --token X    Manually save an OAuth access token (advanced)
func RunAuth(args []string) {
	if len(args) > 0 && args[0] == "clear" {
		runAuthClear()
		return
	}
	if len(args) > 0 && args[0] == "status" {
		runAuthStatus()
		return
	}

	if hasFlags(args) {
		runAuthLegacyFlags(args)
		return
	}

	runAuthInteractive()
}

// runAuthInteractive is the simplified setup: provider → LLM key → Astra key → save.
// No base URL or model prompts — those are auto-derived from the provider.
func runAuthInteractive() {
	if !term.IsTerminal(int(os.Stdin.Fd())) || !term.IsTerminal(int(os.Stderr.Fd())) {
		fmt.Fprintf(os.Stderr, "%s✗ stdin is not a TTY. Use astra configure --api-key ... or set env vars.%s\n", replYellow, replReset)
		os.Exit(1)
	}

	prov := promptLLMProviderUntilValid()

	llmKey, err := readHiddenAPIKey("Enter LLM API key (input hidden):")
	if err != nil {
		fmt.Fprintf(os.Stderr, "%s✗ Failed to read input: %v%s\n", replYellow, err, replReset)
		os.Exit(1)
	}
	if llmKey == "" {
		fmt.Fprintf(os.Stderr, "%s✗ API key cannot be empty%s\n", replYellow, replReset)
		os.Exit(1)
	}

	astraKey := promptAstraPlatformKey()

	cfg := config.LLMEnv{
		APIKey:      llmKey,
		BaseURL:     prov.DefaultBaseURL(),
		Model:       prov.DefaultModel(),
		FastModel:   config.DeriveFastModel(prov.DefaultModel()),
		AstraAPIKey: astraKey,
	}

	if err := config.SaveLLMFile(cfg); err != nil {
		fmt.Fprintf(os.Stderr, "%s✗ Failed to save: %v%s\n", replYellow, err, replReset)
		os.Exit(1)
	}

	path, _ := config.LLMFilePath()
	fmt.Fprintf(os.Stderr, "%s✓ Saved to %s%s\n", replGreen, path, replReset)
	fmt.Fprintf(os.Stderr, "  Run %sastra%s to start the REPL.\n", replGreen, replReset)
}

func hasFlags(args []string) bool {
	for _, a := range args {
		if strings.HasPrefix(a, "-") {
			return true
		}
	}
	return false
}

func runAuthClear() {
	cleared := 0

	llmPath, err := config.LLMFilePath()
	if err == nil {
		if err := os.Remove(llmPath); err == nil {
			fmt.Fprintf(os.Stderr, "%s✓ Removed %s%s\n", replGreen, llmPath, replReset)
			cleared++
		}
	}

	oauthPath := oauth.TokenFilePath()
	if oauthPath != "" {
		if err := os.Remove(oauthPath); err == nil {
			fmt.Fprintf(os.Stderr, "%s✓ Removed %s%s\n", replGreen, oauthPath, replReset)
			cleared++
		}
	}

	for _, key := range []string{"LLM_API_KEY", "ASTRA_API_KEY", "ASTRA_ACCESS_TOKEN"} {
		os.Unsetenv(key)
	}

	if cleared == 0 {
		fmt.Fprintf(os.Stderr, "%sNo saved credentials found.%s\n", replDim, replReset)
	} else {
		fmt.Fprintf(os.Stderr, "\nRun %sastra auth%s to set up new credentials.\n", replGreen, replReset)
	}
}

func runAuthStatus() {
	llmCfg, _ := config.LoadLLMFile()

	fmt.Fprintf(os.Stderr, "%s%s═══ Auth Status ═══%s\n", replBold, replCyan, replReset)

	printField := func(label, value string) {
		if value == "" {
			fmt.Fprintf(os.Stderr, "  %-20s %s✗ not set%s\n", label, replYellow, replReset)
		} else {
			masked := value
			if len(masked) > 12 {
				masked = masked[:12] + "..."
			}
			fmt.Fprintf(os.Stderr, "  %-20s %s✓ %s%s\n", label, replGreen, masked, replReset)
		}
	}

	printField("LLM API Key", llmCfg.APIKey)
	printField("LLM Base URL", llmCfg.BaseURL)
	printField("LLM Model", llmCfg.Model)
	printField("Astra API Key", llmCfg.AstraAPIKey)

	oauthTok, err := oauth.LoadStoredToken()
	if err == nil && oauthTok.AccessToken != "" {
		printField("OAuth Token", oauthTok.AccessToken)
	} else {
		printField("OAuth Token", "")
	}

	if path, err := config.LLMFilePath(); err == nil {
		fmt.Fprintf(os.Stderr, "\n%s  Config file: %s%s\n", replDim, path, replReset)
	}
	fmt.Fprintf(os.Stderr, "%s%s═══════════════════%s\n", replBold, replCyan, replReset)
}

func runAuthLegacyFlags(args []string) {
	fs := flag.NewFlagSet("auth", flag.ExitOnError)
	oauthFlag := fs.Bool("oauth", false, "Run browser-based OAuth PKCE login flow")
	tokenFlag := fs.String("token", "", "Manually save an access token")
	envFlag := fs.String("env", "dev", "Environment: prod, stage, dev, qa")
	scopeFlag := fs.String("scope", "", "OAuth scopes (default: chat,agents:read,agents:write)")
	listenHost := fs.String("listen-host", "127.0.0.1", "Callback server host")
	listenPort := fs.Int("listen-port", 8787, "Callback server port")
	timeoutFlag := fs.Int("timeout", 120, "Callback timeout in seconds")
	noOpenFlag := fs.Bool("no-open", false, "Don't auto-open browser; print URL only")

	fs.Usage = func() {
		fmt.Fprintf(os.Stderr, "Usage: astra auth [command | flags]\n\n")
		fmt.Fprintf(os.Stderr, "Commands:\n")
		fmt.Fprintf(os.Stderr, "  (none)     Interactive setup — configure LLM provider + Astra platform key\n")
		fmt.Fprintf(os.Stderr, "  clear      Remove all saved credentials (~/.astra/llm.json + oauth_token.json)\n")
		fmt.Fprintf(os.Stderr, "  status     Show current auth configuration\n\n")
		fmt.Fprintf(os.Stderr, "Advanced flags:\n")
		fmt.Fprintf(os.Stderr, "  --oauth    Browser OAuth PKCE login\n")
		fmt.Fprintf(os.Stderr, "  --token X  Manually save an access token\n\n")
		fmt.Fprintf(os.Stderr, "Examples:\n")
		fmt.Fprintf(os.Stderr, "  astra auth                          # interactive setup\n")
		fmt.Fprintf(os.Stderr, "  astra auth clear                    # remove all saved keys\n")
		fmt.Fprintf(os.Stderr, "  astra auth status                   # show what's configured\n")
		fmt.Fprintf(os.Stderr, "  astra auth --oauth --env dev        # browser OAuth login\n\n")
		fmt.Fprintf(os.Stderr, "Flags:\n")
		fs.PrintDefaults()
	}

	if err := fs.Parse(args); err != nil {
		os.Exit(1)
	}

	if *oauthFlag {
		cfg := oauth.Config{
			Env:        *envFlag,
			Scope:      *scopeFlag,
			ListenHost: *listenHost,
			ListenPort: *listenPort,
			Timeout:    *timeoutFlag,
			NoOpen:     *noOpenFlag,
		}
		if err := oauth.RunBrowserAuth(cfg); err != nil {
			fmt.Fprintf(os.Stderr, "[auth] ERROR: %v\n", err)
			os.Exit(1)
		}
		return
	}

	if *tokenFlag != "" {
		tok := &oauth.TokenData{AccessToken: *tokenFlag}
		if existing, err := oauth.LoadStoredToken(); err == nil {
			existing.AccessToken = *tokenFlag
			tok = existing
		}
		if err := oauth.SaveToken(tok); err != nil {
			fmt.Fprintf(os.Stderr, "[auth] ERROR saving token: %v\n", err)
			os.Exit(1)
		}
		truncated := *tokenFlag
		if len(truncated) > 8 {
			truncated = truncated[:8] + "..."
		}
		fmt.Fprintf(os.Stderr, "[auth] Saved access_token (%s) to %s\n", truncated, oauth.TokenFilePath())
		return
	}

	fs.Usage()
	os.Exit(1)
}
