// Copyright 2026 printing-press-golden and contributors. Licensed under Apache-2.0. See LICENSE.
// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.

package cli

import (
	"bufio"
	"bytes"
	"encoding/json"
	"fmt"
	"github.com/gorilla/websocket"
	"github.com/spf13/cobra"
	"golden-api-cookie-auth-pp-cli/internal/client"
	"golden-api-cookie-auth-pp-cli/internal/cliutil"
	"golden-api-cookie-auth-pp-cli/internal/config"
	"io"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"sort"
	"strings"
	"time"
)

func newAuthCmd(flags *rootFlags) *cobra.Command {
	cmd := &cobra.Command{
		Use:   "auth",
		Short: "Manage authentication for Golden Api Cookie Auth",
		RunE:  parentNoSubcommandRunE(flags),
	}

	cmd.AddCommand(newAuthLoginCmd(flags))
	cmd.AddCommand(newAuthStatusCmd(flags))
	cmd.AddCommand(newAuthSetTokenCmd(flags))
	cmd.AddCommand(newAuthLogoutCmd(flags))

	return cmd
}

// chromeProfile holds info about a discovered Chrome profile.
type chromeProfile struct {
	Dir                 string   // directory name (e.g. "Default", "Profile 1")
	DisplayName         string   // human-readable name from Preferences
	CookieCount         int      // number of cookies matching the target domain
	RequiredCookieCount int      // number of required credential cookies present
	MissingCookies      []string // required credential cookies not present
}

func requiredAuthCookies() []string {
	raw := []string{"session_id", "csrf_token"}
	out := raw[:0]
	for _, name := range raw {
		name = strings.TrimSpace(name)
		if name != "" {
			out = append(out, name)
		}
	}
	return out
}

func newAuthLoginCmd(flags *rootFlags) *cobra.Command {
	var browserFlag bool
	var profileFlag string
	var cookiesFile string

	cmd := &cobra.Command{
		Use:   "login",
		Short: "Authenticate with the API",
		Long: `Authenticate using your browser session.

Use --chrome to read cookies from Chrome for .cookie-auth.example.
Use --browser as an alias for --chrome.
Use --cookies-file to import Playwright storage-state JSON or a raw Cookie header file.
Requires a cookie extraction tool (pycookiecheat, cookies, or cookie-scoop-cli).

If you have multiple Chrome profiles, pycookiecheat and cookie-scoop-cli can
auto-detect which profile is logged in. Use --profile to select a specific
profile by name when the installed backend supports it.`,
		Example: `  golden-api-cookie-auth-pp-cli auth login --chrome
  golden-api-cookie-auth-pp-cli auth login --browser
  golden-api-cookie-auth-pp-cli auth login --cookies-file storage-state.json
  golden-api-cookie-auth-pp-cli auth login --chrome --profile "Work"`,
		RunE: func(cmd *cobra.Command, args []string) error {
			w := cmd.OutOrStdout()
			domain := ".cookie-auth.example"
			if !browserFlag && cookiesFile == "" {
				fmt.Fprintln(w, "Use --chrome, --browser, or --cookies-file to authenticate from your browser session:")
				fmt.Fprintf(cmd.OutOrStdout(), "  golden-api-cookie-auth-pp-cli auth login --chrome\n")
				fmt.Fprintf(cmd.OutOrStdout(), "  golden-api-cookie-auth-pp-cli auth login --browser\n")
				fmt.Fprintf(cmd.OutOrStdout(), "  golden-api-cookie-auth-pp-cli auth login --cookies-file storage-state.json\n")
				return nil
			}
			if browserFlag && cookiesFile != "" {
				return authErr(fmt.Errorf("choose either --chrome/--browser or --cookies-file, not both"))
			}
			// Check if already authenticated via env var
			if v := os.Getenv("COOKIE_AUTH_SESSION"); v != "" {
				fmt.Fprintf(cmd.OutOrStdout(), "Already authenticated via %s env var.\n", "COOKIE_AUTH_SESSION")
				return nil
			}

			// Step 0: Try press-auth, the dedicated cookie capture companion.
			// press-auth spawns its own controlled Chrome window so users do
			// not need to relaunch their daily Chrome with
			// --remote-debugging-port. If press-auth is not installed, fall
			// through silently to the legacy extraction chain. If press-auth
			// is installed but errored (e.g. ExitNotCaptured because the user
			// has not logged in yet), surface its message and still fall
			// through — the legacy chain may succeed if persistent cookies
			// are already on disk.
			var cookies string
			fromPressAuth := false
			fromCookiesFile := false
			if cookiesFile != "" {
				imported, err := loadCookiesFromFile(cookiesFile, domain)
				if err != nil {
					return authErr(err)
				}
				cookies = imported.Header
				fromCookiesFile = true
				fmt.Fprintf(w, "Loaded cookies from %s for %s.\n", cookiesFile, domain)
			}
			if !fromCookiesFile {
				if pressAuthPath, err := exec.LookPath("press-auth"); err == nil {
					paCookies, paErr := tryPressAuth(pressAuthPath, domain)
					if paErr == nil && paCookies != "" {
						fmt.Fprintf(w, "Loaded cookies via press-auth for %s.\n", domain)
						cookies = paCookies
						fromPressAuth = true
					} else if paErr != nil {
						fmt.Fprintf(w, "press-auth returned an error: %v\n", paErr)
						fmt.Fprintln(w, "Falling back to other extraction methods.")
						fmt.Fprintln(w, "If you haven't logged in yet, run:")
						fmt.Fprintf(w, "\n  press-auth login %s --login-url <login-url>\n\n", domain)
					}
				}
			}

			if !fromPressAuth && !fromCookiesFile {
				// Step 1: Detect cookie extraction tool
				tool, err := detectCookieTool()
				if err != nil {
					fmt.Fprintln(w, red("No cookie extraction tool found."))
					fmt.Fprintln(w, "")
					if runtime.GOOS == "windows" {
						fmt.Fprintln(w, "pycookiecheat does not support Windows. Read cookies from a live Chrome session instead:")
						fmt.Fprintf(w, "\n  golden-api-cookie-auth-pp-cli auth login --browser\n\n")
						fmt.Fprintln(w, "Or install a cross-platform reader:")
						fmt.Fprintln(w, "  cargo install cookie-scoop-cli     # Rust")
					} else {
						fmt.Fprintln(w, "Install one of:")
						fmt.Fprintln(w, "  pip install pycookiecheat          # Python (recommended)")
						fmt.Fprintln(w, "  brew install barnardb/cookies/cookies  # Homebrew")
						fmt.Fprintln(w, "  cargo install cookie-scoop-cli     # Rust")
					}
					return authErr(fmt.Errorf("no cookie tool found"))
				}

				if profileFlag != "" && !cookieToolSupportsProfiles(tool.name) {
					return authErr(fmt.Errorf("%s does not support --profile; install pycookiecheat or cookie-scoop-cli", tool.name))
				}

				// Step 2: Resolve which Chrome profile to use when the backend can honor it
				profileDir := ""
				if cookieToolSupportsProfiles(tool.name) {
					profileDir, err = resolveChromeProfile(w, cmd.InOrStdin(), domain, profileFlag, requiredAuthCookies())
					if err != nil {
						loginURL := "https://" + strings.TrimPrefix(domain, ".")
						fmt.Fprintln(w, "")
						fmt.Fprintln(w, red("No Chrome profile has cookies for "+domain+"."))
						fmt.Fprintln(w, "")
						fmt.Fprintln(w, "Log in to your account:")
						fmt.Fprintf(w, "\n  %s\n\n", loginURL)
						fmt.Fprintln(w, "Then run this command again:")
						fmt.Fprintf(w, "\n  golden-api-cookie-auth-pp-cli auth login --chrome\n")
						return authErr(fmt.Errorf("not logged in to %s", domain))
					}
				}

				if profileDir != "" {
					fmt.Fprintf(w, "Reading cookies from Chrome profile %q for %s...\n", profileDir, domain)
				} else {
					fmt.Fprintf(w, "Reading cookies for %s...\n", domain)
				}

				// Step 3: Extract cookies from the resolved profile
				cookies, err = extractCookies(tool, domain, profileDir)
				if err != nil {
					return authErr(fmt.Errorf("extracting cookies: %w", err))
				}

				if cookies == "" {
					return authErr(fmt.Errorf("cookie tool returned no cookies for %s", domain))
				}
			} // end if !fromPressAuth
			// Unfiltered, the persisted blob carries every cookie the target
			// domain has set (CSRF, WAF, anti-bot fingerprints, etc.) into the
			// Cookie header on every request, which routinely trips upstream
			// WAFs and shadows the real credential.
			cookieMap := parseCookieString(cookies)
			requiredCookies := []string{"session_id", "csrf_token"}
			kept := make([]string, 0, len(requiredCookies))
			for _, name := range requiredCookies {
				v, ok := cookieMap[name]
				if !ok {
					loginURL := "https://" + strings.TrimPrefix(domain, ".")
					fmt.Fprintf(w, "\n%s Cookie %q not found for %s.\n", red("ERROR"), name, domain)
					fmt.Fprintln(w, "")
					fmt.Fprintln(w, "Log in to your account:")
					fmt.Fprintf(w, "\n  %s\n\n", loginURL)
					fmt.Fprintln(w, "Then run this command again:")
					fmt.Fprintf(w, "\n  golden-api-cookie-auth-pp-cli auth login --chrome\n")
					return authErr(fmt.Errorf("cookie %q not found for %s", name, domain))
				}
				kept = append(kept, name+"="+v)
			}
			cookies = strings.Join(kept, "; ")

			// Step 5: Save to config
			cfg, err := config.Load(flags.configPath)
			if err != nil {
				return configErr(err)
			}

			if err := cfg.SaveTokens("", "", cookies, "", time.Time{}); err != nil {
				return configErr(fmt.Errorf("saving cookies: %w", err))
			}
			// Persist the required cookies so client.LoadCookieJar() carries
			// them on subsequent invocations; the SaveTokens blob above feeds
			// the Authorization header path but is invisible to the HTTP
			// client's cookie jar.
			jarCookies := make(map[string]string, len(requiredCookies))
			for _, name := range requiredCookies {
				if v, ok := cookieMap[name]; ok {
					jarCookies[name] = v
				}
			}
			if err := client.WriteCookieJarFromMap(domain, jarCookies); err != nil {
				fmt.Fprintf(w, "warning: persisting cookie jar: %v (credentials still saved)\n", err)
			}

			count := len(strings.Split(cookies, ";"))
			fmt.Fprintf(w, "%s Found %d cookies for %s\n", green("OK"), count, domain)
			fmt.Fprintf(w, "Session saved to %s\n", credentialSavePath(cfg))
			return nil
		},
	}

	cmd.Flags().BoolVar(&browserFlag, "chrome", false, "Read cookies from Chrome")
	cmd.Flags().BoolVar(&browserFlag, "browser", false, "Alias for --chrome")
	cmd.Flags().StringVar(&profileFlag, "profile", "", "Chrome profile name (e.g. \"Work\", \"Personal\")")
	cmd.Flags().StringVar(&cookiesFile, "cookies-file", "", "Import cookies from a Playwright storage-state JSON file or raw Cookie header file")
	return cmd
}
func cookieToolSupportsProfiles(tool string) bool {
	switch tool {
	case "pycookiecheat", "pycookiecheat-cli", "cookie-scoop":
		return true
	default:
		return false
	}
}

func newAuthStatusCmd(flags *rootFlags) *cobra.Command {
	return &cobra.Command{
		Use:     "status",
		Short:   "Show authentication status",
		Example: "  golden-api-cookie-auth-pp-cli auth status",
		RunE: func(cmd *cobra.Command, args []string) error {
			cfg, err := config.Load(flags.configPath)
			if err != nil {
				return configErr(err)
			}

			w := cmd.OutOrStdout()
			header := cfg.AuthHeader()
			if header == "" {
				if v := os.Getenv("COOKIE_AUTH_SESSION"); v != "" {
					fmt.Fprintln(w, green("Authenticated"))
					fmt.Fprintf(w, "  Source: %s env var\n", "COOKIE_AUTH_SESSION")
					return nil
				}
				fmt.Fprintln(w, red("Not authenticated"))
				fmt.Fprintln(w, "")
				fmt.Fprintln(w, "Log in from your browser session:")
				fmt.Fprintf(w, "  golden-api-cookie-auth-pp-cli auth login --chrome\n")
				return authErr(fmt.Errorf("no credentials configured"))
			}

			fmt.Fprintln(w, green("Authenticated"))
			fmt.Fprintf(w, "  Source: %s\n", cfg.AuthSource)
			fmt.Fprintf(w, "  Domain: .cookie-auth.example\n")
			fmt.Fprintf(w, "  Config: %s\n", cfg.Path)
			return nil
		},
	}
}

// newAuthSetTokenCmd is the escape hatch for sites whose access token can't
// be reached by the cookie-jar extractor in `auth login --chrome` — most
// commonly Auth0 SPA SDK v2+ deployments using the default in-memory cache,
// where the JWT lives in JS heap and never touches cookies or localStorage.
// Users (or agents) grab the bearer from DevTools → Network → Authorization
// and paste it here instead of hand-editing config.toml.
//
// The cliutil.LooksLikeJWT length floor is the cookie-vs-JWT-shape gate from
// retro #1598 WU-2: short tracking cookies like Cloudflare's `__cf_bm`
// (~31 chars) happen to have three base64url segments and would otherwise
// silently land as the access token.
func newAuthSetTokenCmd(flags *rootFlags) *cobra.Command {
	return &cobra.Command{
		Use:     "set-token <jwt>",
		Short:   "Save a bearer JWT pasted from DevTools (escape hatch for in-memory tokens)",
		Example: "  golden-api-cookie-auth-pp-cli auth set-token eyJhbGciOi...",
		Long: "Save a bearer JWT pasted from DevTools (escape hatch for in-memory tokens).\n\n" +
			"Use this when `auth login --chrome` can't reach the access token — most\n" +
			"commonly Auth0 SPA SDK deployments that keep the JWT in JS heap memory.\n" +
			"Open DevTools → Network → any authenticated request → copy the\n" +
			"`Authorization: Bearer ...` value and paste the JWT portion here.\n\n" +
			"The token is validated for JWT shape (three base64url segments, length\n" +
			"floor) so short tracking cookies don't get saved as credentials.",
		Args: cobra.ExactArgs(1),
		RunE: func(cmd *cobra.Command, args []string) error {
			cfg, err := config.Load(flags.configPath)
			if err != nil {
				return configErr(err)
			}

			token := strings.TrimSpace(args[0])
			token = strings.TrimPrefix(token, "Bearer ")
			if !cliutil.LooksLikeJWT(token) {
				return authErr(fmt.Errorf("argument is not JWT-shaped (three base64url segments, ≥150 chars total). " +
					"This is the cookie-vs-JWT-shape gate — short tracking cookies like Cloudflare's __cf_bm " +
					"share the segment shape but aren't credentials. Paste the `Authorization: Bearer ...` value " +
					"from DevTools → Network, not a cookie jar"))
			}

			// Clear any legacy auth_header so AuthHeader() falls through to
			// the newly-saved bearer. Mirrors auth_simple's set-token.
			cfg.AuthHeaderVal = ""
			// Pass a zero expiry. We can't decode the JWT to learn its real
			// lifetime, and preserving a stale cfg.TokenExpiry from a prior
			// session (the exact case escape-hatch users reach this from)
			// would later mislead any expiry-aware status check into treating
			// this fresh token as already expired. Actual expiry surfaces via
			// 401 on the next API call, same as the rest of the browser-auth
			// flow. Matches ClearTokens / SaveBearerToken's zero-on-write
			// invariant in config.go.tmpl.
			if err := cfg.SaveTokens("", "", token, "", time.Time{}); err != nil {
				return configErr(fmt.Errorf("saving token: %w", err))
			}

			savePath := credentialSavePath(cfg)
			// JSON envelope: {saved, config_path, credentials_path}.
			if flags.asJSON {
				out := map[string]any{
					"saved":       true,
					"config_path": cfg.Path,
				}
				if !cfg.AgentcookieManagedByExternalStore() {
					out["credentials_path"] = savePath
				}
				return printJSONFiltered(cmd.OutOrStdout(), out, flags)
			}
			fmt.Fprintf(cmd.OutOrStdout(), "Token saved to %s\n", savePath)
			return nil
		},
	}
}

func credentialSavePath(cfg *config.Config) string {
	if cfg != nil && cfg.AgentcookieManagedByExternalStore() {
		return cfg.Path
	}
	if path, err := cliutil.CredentialsFilePath(); err == nil {
		return path
	}
	if cfg != nil {
		return cfg.Path
	}
	return ""
}

func newAuthLogoutCmd(flags *rootFlags) *cobra.Command {
	return &cobra.Command{
		Use:     "logout",
		Short:   "Clear stored credentials",
		Example: "  golden-api-cookie-auth-pp-cli auth logout",
		RunE: func(cmd *cobra.Command, args []string) error {
			cfg, err := config.Load(flags.configPath)
			if err != nil {
				return configErr(err)
			}

			if err := cfg.ClearTokens(); err != nil {
				return configErr(fmt.Errorf("clearing tokens: %w", err))
			}
			if os.Getenv("COOKIE_AUTH_SESSION") != "" {
				fmt.Fprintf(cmd.OutOrStdout(), "Config cleared. Note: %s env var is still set.\n", "COOKIE_AUTH_SESSION")
				return nil
			}
			fmt.Fprintln(cmd.OutOrStdout(), "Logged out. Credentials cleared.")
			return nil
		},
	}
}

// --- Chrome profile discovery ---
// chromeDataDir returns the Chrome user data directory for the current OS.
func chromeDataDir() (string, error) {
	home, err := os.UserHomeDir()
	if err != nil {
		return "", err
	}

	switch runtime.GOOS {
	case "darwin":
		return filepath.Join(home, "Library", "Application Support", "Google", "Chrome"), nil
	case "linux":
		return filepath.Join(home, ".config", "google-chrome"), nil
	case "windows":
		localAppData := os.Getenv("LOCALAPPDATA")
		if localAppData == "" {
			localAppData = filepath.Join(home, "AppData", "Local")
		}
		return filepath.Join(localAppData, "Google", "Chrome", "User Data"), nil
	default:
		return "", fmt.Errorf("unsupported OS: %s", runtime.GOOS)
	}
}

// discoverChromeProfiles finds Chrome profiles and counts cookies matching the domain.
// When requiredCookies is non-empty, profiles with those credential cookies rank
// ahead of profiles that only have guest, analytics, or challenge cookies.
func discoverChromeProfiles(domain string, requiredCookies []string) ([]chromeProfile, error) {
	dataDir, err := chromeDataDir()
	if err != nil {
		return nil, err
	}
	if _, err := exec.LookPath("sqlite3"); err != nil {
		return nil, fmt.Errorf("sqlite3 not found")
	}

	entries, err := os.ReadDir(dataDir)
	if err != nil {
		return nil, fmt.Errorf("cannot read Chrome data directory: %w", err)
	}

	// Match domain for SQLite query — host_key uses leading dot (e.g. ".notion.so")
	domainPattern := "%" + strings.TrimPrefix(domain, ".") + "%"

	var profiles []chromeProfile
	for _, entry := range entries {
		if !entry.IsDir() {
			continue
		}
		name := entry.Name()
		if name != "Default" && !strings.HasPrefix(name, "Profile ") {
			continue
		}

		profilePath := filepath.Join(dataDir, name)
		cookiesDB := filepath.Join(profilePath, "Cookies")
		if _, err := os.Stat(cookiesDB); err != nil {
			continue
		}

		displayName := readProfileDisplayName(filepath.Join(profilePath, "Preferences"))
		if displayName == "" {
			displayName = name
		}

		count, requiredCount, missing := inspectCookiesForDomain(cookiesDB, domainPattern, requiredCookies)

		profiles = append(profiles, chromeProfile{
			Dir:                 name,
			DisplayName:         displayName,
			CookieCount:         count,
			RequiredCookieCount: requiredCount,
			MissingCookies:      missing,
		})
	}

	// Sort: profiles with credential cookies first, then by total cookie count,
	// then by directory name for deterministic prompts.
	sort.Slice(profiles, func(i, j int) bool {
		if profiles[i].RequiredCookieCount != profiles[j].RequiredCookieCount {
			return profiles[i].RequiredCookieCount > profiles[j].RequiredCookieCount
		}
		if profiles[i].CookieCount != profiles[j].CookieCount {
			return profiles[i].CookieCount > profiles[j].CookieCount
		}
		return profiles[i].Dir < profiles[j].Dir
	})

	return profiles, nil
}

// readProfileDisplayName extracts the human-readable name from Chrome's Preferences JSON.
func readProfileDisplayName(prefsPath string) string {
	data, err := os.ReadFile(prefsPath)
	if err != nil {
		return ""
	}
	var prefs struct {
		Profile struct {
			Name string `json:"name"`
		} `json:"profile"`
	}
	if err := json.Unmarshal(data, &prefs); err != nil {
		return ""
	}
	return prefs.Profile.Name
}

// inspectCookiesForDomain copies the Cookies DB (plus WAL/SHM) to temp and counts matching rows.
// Uses sqlite3 when available; host_key is plaintext so no decryption is needed.
func inspectCookiesForDomain(cookiesDB, domainPattern string, requiredCookies []string) (count int, requiredCount int, missing []string) {
	tmpFile, err := os.CreateTemp("", "cookies-probe-*.db")
	if err != nil {
		return 0, 0, append([]string{}, requiredCookies...)
	}
	tmpPath := tmpFile.Name()
	tmpFile.Close()
	defer os.Remove(tmpPath)
	defer os.Remove(tmpPath + "-wal")
	defer os.Remove(tmpPath + "-shm")

	// Copy the database file plus WAL/SHM to avoid Chrome's WAL lock
	// and to include uncommitted cookie writes that are still in the WAL.
	if err := copyFileIfExists(cookiesDB, tmpPath); err != nil {
		return 0, 0, append([]string{}, requiredCookies...)
	}
	_ = copyFileIfExists(cookiesDB+"-wal", tmpPath+"-wal")
	_ = copyFileIfExists(cookiesDB+"-shm", tmpPath+"-shm")

	query := fmt.Sprintf("SELECT COUNT(*) FROM cookies WHERE host_key LIKE '%s'", sqlQuoteLiteral(domainPattern))
	out, err := exec.Command("sqlite3", tmpPath, query).Output()
	if err != nil {
		return 0, 0, append([]string{}, requiredCookies...)
	}

	fmt.Sscanf(strings.TrimSpace(string(out)), "%d", &count)

	if len(requiredCookies) == 0 || count == 0 {
		return count, 0, append([]string{}, requiredCookies...)
	}

	quotedNames := make([]string, 0, len(requiredCookies))
	for _, name := range requiredCookies {
		name = strings.TrimSpace(name)
		if name == "" {
			continue
		}
		quotedNames = append(quotedNames, "'"+sqlQuoteLiteral(name)+"'")
	}
	if len(quotedNames) == 0 {
		return count, 0, nil
	}

	query = fmt.Sprintf(
		"SELECT DISTINCT name FROM cookies WHERE host_key LIKE '%s' AND name IN (%s)",
		sqlQuoteLiteral(domainPattern),
		strings.Join(quotedNames, ","),
	)
	out, err = exec.Command("sqlite3", tmpPath, query).Output()
	if err != nil {
		return count, 0, append([]string{}, requiredCookies...)
	}

	present := map[string]bool{}
	for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
		name := strings.TrimSpace(line)
		if name != "" && !present[name] {
			present[name] = true
			requiredCount++
		}
	}
	for _, name := range requiredCookies {
		name = strings.TrimSpace(name)
		if name != "" && !present[name] {
			missing = append(missing, name)
		}
	}
	return count, requiredCount, missing
}

func sqlQuoteLiteral(s string) string {
	return strings.ReplaceAll(s, "'", "''")
}

// copyFileIfExists copies src to dst. Returns nil if src does not exist.
func copyFileIfExists(src, dst string) error {
	in, err := os.Open(src)
	if err != nil {
		if os.IsNotExist(err) {
			return nil
		}
		return err
	}
	defer in.Close()
	out, err := os.Create(dst)
	if err != nil {
		return err
	}
	defer out.Close()
	_, err = io.Copy(out, in)
	return err
}

// resolveChromeProfile determines which Chrome profile to read cookies from.
// Priority: --profile flag > auto-detect (single match) > interactive prompt.
func resolveChromeProfile(w io.Writer, r io.Reader, domain, profileFlag string, requiredCookies []string) (string, error) {
	if profileFlag != "" {
		return resolveProfileByName(profileFlag)
	}

	profiles, err := discoverChromeProfiles(domain, requiredCookies)
	if err != nil {
		// Profile probing is optional; cookie extraction can still succeed without it.
		fmt.Fprintf(w, "Could not inspect Chrome profiles (%v); continuing without auto-detection.\n", err)
		return "", nil
	}

	// Prefer profiles that have all required credential cookies. A profile with
	// only guest/challenge cookies can make the user look "not logged in" even
	// though another Chrome profile has the active session.
	if len(requiredCookies) > 0 {
		var withRequiredCookies []chromeProfile
		for _, p := range profiles {
			if p.CookieCount > 0 && p.RequiredCookieCount == len(requiredCookies) {
				withRequiredCookies = append(withRequiredCookies, p)
			}
		}
		if len(withRequiredCookies) > 0 {
			return chooseChromeProfile(w, r, domain, withRequiredCookies, true)
		}
		printMissingCookieHint(w, profiles, requiredCookies)
	}

	// Fall back to profiles that have any cookie for this domain. This preserves
	// behavior for specs that do not enumerate cookie names.
	var withCookies []chromeProfile
	for _, p := range profiles {
		if p.CookieCount > 0 {
			withCookies = append(withCookies, p)
		}
	}

	if len(withCookies) == 0 {
		return "", fmt.Errorf("no Chrome profile has cookies for %s", domain)
	}
	return chooseChromeProfile(w, r, domain, withCookies, false)
}

func printMissingCookieHint(w io.Writer, profiles []chromeProfile, requiredCookies []string) {
	if len(requiredCookies) == 0 {
		return
	}
	for _, p := range profiles {
		if p.CookieCount == 0 || len(p.MissingCookies) == 0 {
			continue
		}
		fmt.Fprintf(w, "Chrome profile %s (%s) is missing required cookies: %s\n", p.DisplayName, p.Dir, strings.Join(p.MissingCookies, ", "))
		return
	}
}

func chooseChromeProfile(w io.Writer, r io.Reader, domain string, profiles []chromeProfile, matchedRequired bool) (string, error) {
	switch len(profiles) {
	case 1:
		if matchedRequired {
			fmt.Fprintf(w, "Auto-detected Chrome profile: %s (%s, required cookies present)\n", profiles[0].DisplayName, profiles[0].Dir)
		} else {
			fmt.Fprintf(w, "Auto-detected Chrome profile: %s (%s)\n", profiles[0].DisplayName, profiles[0].Dir)
		}
		return profiles[0].Dir, nil
	default:
		// Multiple profiles have cookies.
		// Non-interactive: pick the profile with the most cookies (already sorted).
		// Interactive: ask the user.
		if !isTerminal(w) {
			// Non-interactive — auto-select the best profile (most cookies, first in sorted list)
			selected := profiles[0]
			if matchedRequired {
				fmt.Fprintf(w, "Auto-selected Chrome profile: %s (%s, required cookies present, %d cookies)\n", selected.DisplayName, selected.Dir, selected.CookieCount)
			} else {
				fmt.Fprintf(w, "Auto-selected Chrome profile: %s (%s, %d cookies)\n", selected.DisplayName, selected.Dir, selected.CookieCount)
			}
			fmt.Fprintf(w, "Use --profile to select a different profile.\n")
			return selected.Dir, nil
		}

		fmt.Fprintf(w, "Multiple Chrome profiles have cookies for %s:\n", domain)
		for i, p := range profiles {
			if matchedRequired {
				fmt.Fprintf(w, "  %d. %s (%s, required cookies present, %d cookies)\n", i+1, p.DisplayName, p.Dir, p.CookieCount)
			} else {
				fmt.Fprintf(w, "  %d. %s (%s, %d cookies)\n", i+1, p.DisplayName, p.Dir, p.CookieCount)
			}
		}
		fmt.Fprintf(w, "Which profile? [1]: ")

		scanner := bufio.NewScanner(r)
		choice := 1
		if scanner.Scan() {
			text := strings.TrimSpace(scanner.Text())
			if text != "" {
				fmt.Sscanf(text, "%d", &choice)
			}
		}
		if choice < 1 || choice > len(profiles) {
			choice = 1
		}
		selected := profiles[choice-1]
		fmt.Fprintf(w, "Using profile: %s (%s)\n", selected.DisplayName, selected.Dir)
		return selected.Dir, nil
	}
}

// resolveProfileByName finds a Chrome profile directory by its display name.
func resolveProfileByName(name string) (string, error) {
	dataDir, err := chromeDataDir()
	if err != nil {
		return "", err
	}

	entries, err := os.ReadDir(dataDir)
	if err != nil {
		return "", err
	}

	lowerName := strings.ToLower(name)
	for _, entry := range entries {
		if !entry.IsDir() {
			continue
		}
		dirName := entry.Name()
		if dirName != "Default" && !strings.HasPrefix(dirName, "Profile ") {
			continue
		}
		displayName := readProfileDisplayName(filepath.Join(dataDir, dirName, "Preferences"))
		if strings.ToLower(displayName) == lowerName || strings.ToLower(dirName) == lowerName {
			return dirName, nil
		}
	}

	return "", fmt.Errorf("Chrome profile %q not found", name)
}

// --- Cookie extraction tools ---

// resolvePythonBinary finds a usable Python 3 interpreter, returning the
// executable name and any leading args (e.g. `py -3` on Windows). pycookiecheat
// is the only tool the CLI shells into Python for, and Windows ships
// `python.exe` or the `py` launcher rather than `python3`.
func resolvePythonBinary() (string, []string, bool) {
	candidates := []struct {
		bin  string
		args []string
	}{
		{"python3", nil},
		{"python", nil},
		{"py", []string{"-3"}},
	}
	for _, c := range candidates {
		if _, err := exec.LookPath(c.bin); err == nil {
			return c.bin, c.args, true
		}
	}
	return "", nil, false
}

// cookieTool bundles the detected tool name with any auxiliary invocation
// data the extraction call needs. For pycookiecheat we carry the Python
// binary and its leading args so detection and extraction cannot disagree
// if PATH mutates between calls.
type cookieTool struct {
	name   string
	pyBin  string
	pyArgs []string
}

// tryPressAuth shells out to `press-auth cookies <domain>` and returns the
// resulting Cookie header value. Stderr from press-auth (if any) is surfaced
// as the error message so callers can include the recovery hint.
func tryPressAuth(pressAuthPath, domain string) (string, error) {
	cmd := exec.Command(pressAuthPath, "cookies", domain)
	var out, errBuf bytes.Buffer
	cmd.Stdout = &out
	cmd.Stderr = &errBuf
	if err := cmd.Run(); err != nil {
		msg := strings.TrimSpace(errBuf.String())
		if msg != "" {
			return "", fmt.Errorf("%s", msg)
		}
		return "", err
	}
	return strings.TrimSpace(out.String()), nil
}

// detectCookieTool checks for available cookie extraction tools in preference order.
// pycookiecheat is skipped on Windows because upstream raises
// `OSError("This script only works on MacOS or Linux.")` regardless of whether
// the import probe succeeds.
func detectCookieTool() (cookieTool, error) {
	if runtime.GOOS != "windows" {
		if bin, args, ok := resolvePythonBinary(); ok {
			probeArgs := append(append([]string{}, args...), "-c", "import pycookiecheat")
			if err := exec.Command(bin, probeArgs...).Run(); err == nil {
				return cookieTool{name: "pycookiecheat", pyBin: bin, pyArgs: args}, nil
			}
		}
		if path, err := exec.LookPath("pycookiecheat"); err == nil {
			if err := exec.Command(path, "--help").Run(); err == nil {
				return cookieTool{name: "pycookiecheat-cli", pyBin: path}, nil
			}
		}
	}
	if err := exec.Command("cookies", "--help").Run(); err == nil {
		return cookieTool{name: "cookies"}, nil
	}
	if err := exec.Command("cookie-scoop", "--help").Run(); err == nil {
		return cookieTool{name: "cookie-scoop"}, nil
	}
	if runtime.GOOS == "windows" {
		return cookieTool{}, fmt.Errorf("no cookie extraction tool found; pycookiecheat does not support Windows. Use `auth login --browser` (live Chrome via CDP) or install cookie-scoop-cli")
	}
	return cookieTool{}, fmt.Errorf("no cookie extraction tool found; install one: pip install pycookiecheat")
}

// extractCookies shells out to the detected tool and returns a Cookie header value string.
// profileDir selects which Chrome profile to read from (e.g. "Default", "Profile 1").
func extractCookies(tool cookieTool, domain, profileDir string) (string, error) {
	switch tool.name {
	case "pycookiecheat":
		return extractViaPycookiecheat(tool, domain, profileDir)
	case "pycookiecheat-cli":
		return extractViaPycookiecheatCLI(tool, domain, profileDir)
	case "cookies":
		return extractViaCookiesCLI(domain)
	case "cookie-scoop":
		return extractViaCookieScoop(domain, profileDir)
	default:
		return "", fmt.Errorf("unknown cookie tool: %s", tool.name)
	}
}

func extractViaPycookiecheat(tool cookieTool, domain, profileDir string) (string, error) {
	cleanDomain := strings.TrimPrefix(domain, ".")
	cookiePath := ""
	if profileDir != "" {
		dataDir, err := chromeDataDir()
		if err == nil {
			cookiePath = filepath.Join(dataDir, profileDir, "Cookies")
		}
	}

	var script string
	if cookiePath != "" {
		// Use forward slashes so Python doesn't interpret backslashes as escapes on Windows
		safePath := filepath.ToSlash(cookiePath)
		script = fmt.Sprintf(
			`import json; from pycookiecheat import chrome_cookies; print(json.dumps(chrome_cookies("https://%s", cookie_file="%s")))`,
			cleanDomain, safePath,
		)
	} else {
		script = fmt.Sprintf(
			`import json; from pycookiecheat import chrome_cookies; print(json.dumps(chrome_cookies("https://%s")))`,
			cleanDomain,
		)
	}

	var out bytes.Buffer
	cmd := exec.Command(tool.pyBin, append(append([]string{}, tool.pyArgs...), "-c", script)...)
	cmd.Stdout = &out
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
		return "", fmt.Errorf("pycookiecheat failed: %w", err)
	}

	var cookies map[string]string
	if err := json.Unmarshal(out.Bytes(), &cookies); err != nil {
		return "", fmt.Errorf("parsing pycookiecheat output: %w", err)
	}

	var parts []string
	for name, value := range cookies {
		parts = append(parts, name+"="+value)
	}
	return strings.Join(parts, "; "), nil
}

func extractViaPycookiecheatCLI(tool cookieTool, domain, profileDir string) (string, error) {
	cleanDomain := strings.TrimPrefix(domain, ".")
	var args []string
	if profileDir != "" {
		dataDir, err := chromeDataDir()
		if err != nil {
			return "", err
		}
		args = append(args, "-c", filepath.Join(dataDir, profileDir, "Cookies"))
	}
	args = append(args, "https://"+cleanDomain)

	var out bytes.Buffer
	cmd := exec.Command(tool.pyBin, args...)
	cmd.Stdout = &out
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
		return "", fmt.Errorf("pycookiecheat cli failed: %w", err)
	}

	var cookies map[string]string
	if err := json.Unmarshal(out.Bytes(), &cookies); err != nil {
		return "", fmt.Errorf("parsing pycookiecheat output: %w", err)
	}

	var parts []string
	for name, value := range cookies {
		parts = append(parts, name+"="+value)
	}
	return strings.Join(parts, "; "), nil
}

func extractViaCookiesCLI(domain string) (string, error) {
	// barnardb/cookies doesn't support profile selection — reads Default
	var out bytes.Buffer
	cmd := exec.Command("cookies", "https://"+strings.TrimPrefix(domain, "."))
	cmd.Stdout = &out
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
		return "", fmt.Errorf("cookies cli failed: %w", err)
	}

	result := strings.TrimSpace(out.String())
	result = strings.TrimPrefix(result, "Cookie: ")
	return result, nil
}

type importedCookieFile struct {
	Header  string
	Cookies []*http.Cookie
}

func loadCookiesFromFile(path string, domain string) (importedCookieFile, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return importedCookieFile{}, fmt.Errorf("reading cookies file: %w", err)
	}
	var state struct {
		Cookies []struct {
			Name     string  `json:"name"`
			Value    string  `json:"value"`
			Domain   string  `json:"domain"`
			Path     string  `json:"path"`
			Expires  float64 `json:"expires"`
			Secure   bool    `json:"secure"`
			HTTPOnly bool    `json:"httpOnly"`
		} `json:"cookies"`
	}
	if err := json.Unmarshal(data, &state); err == nil {
		if len(state.Cookies) == 0 {
			return importedCookieFile{}, fmt.Errorf("cookies file contains an empty cookies array")
		}
		parts := make([]string, 0, len(state.Cookies))
		cookies := make([]*http.Cookie, 0, len(state.Cookies))
		for _, c := range state.Cookies {
			if !cookieDomainMatches(c.Domain, domain) {
				continue
			}
			name := strings.TrimSpace(c.Name)
			if name == "" {
				continue
			}
			path := c.Path
			if path == "" {
				path = "/"
			}
			ck := &http.Cookie{
				Name:     name,
				Value:    c.Value,
				Domain:   c.Domain,
				Path:     path,
				Secure:   c.Secure,
				HttpOnly: c.HTTPOnly,
			}
			if c.Expires > 0 {
				ck.Expires = time.Unix(int64(c.Expires), 0)
			}
			cookies = append(cookies, ck)
			parts = append(parts, name+"="+c.Value)
		}
		if len(parts) == 0 {
			return importedCookieFile{}, fmt.Errorf("cookies file contains no cookies for %s", domain)
		}
		return importedCookieFile{Header: strings.Join(parts, "; "), Cookies: cookies}, nil
	}
	header := trimCookieHeaderPrefix(string(data))
	if header == "" {
		return importedCookieFile{}, fmt.Errorf("cookies file is empty")
	}
	return importedCookieFile{Header: header, Cookies: parseCookieHeaderCookies(header)}, nil
}

func trimCookieHeaderPrefix(s string) string {
	s = strings.TrimSpace(s)
	if len(s) >= len("Cookie:") && strings.EqualFold(s[:len("Cookie:")], "Cookie:") {
		return strings.TrimSpace(s[len("Cookie:"):])
	}
	return s
}

func parseCookieHeaderCookies(header string) []*http.Cookie {
	parts := strings.Split(header, ";")
	cookies := make([]*http.Cookie, 0, len(parts))
	for _, part := range parts {
		name, value, ok := strings.Cut(strings.TrimSpace(part), "=")
		if !ok || strings.TrimSpace(name) == "" {
			continue
		}
		cookies = append(cookies, &http.Cookie{Name: strings.TrimSpace(name), Value: strings.TrimSpace(value), Path: "/"})
	}
	return cookies
}

func cookieDomainMatches(cookieDomain string, targetDomain string) bool {
	target := strings.TrimPrefix(strings.ToLower(strings.TrimSpace(targetDomain)), ".")
	if target == "" {
		return true
	}
	candidate := strings.TrimPrefix(strings.ToLower(strings.TrimSpace(cookieDomain)), ".")
	return candidate == target || strings.HasSuffix(candidate, "."+target) || (strings.Contains(candidate, ".") && strings.HasSuffix(target, "."+candidate))
}

// parseCookieString splits a "name1=value1; name2=value2" string into a map.
func parseCookieString(cookies string) map[string]string {
	m := make(map[string]string)
	for _, pair := range strings.Split(cookies, "; ") {
		pair = strings.TrimSpace(pair)
		if pair == "" {
			continue
		}
		idx := strings.IndexByte(pair, '=')
		if idx < 0 {
			continue
		}
		m[pair[:idx]] = pair[idx+1:]
	}
	return m
}

func extractViaCookieScoop(domain, profileDir string) (string, error) {
	cleanDomain := strings.TrimPrefix(domain, ".")
	args := []string{"--url", "https://" + cleanDomain, "--header"}
	if profileDir != "" {
		args = append(args, "--chrome-profile", profileDir)
	}

	var out bytes.Buffer
	cmd := exec.Command("cookie-scoop", args...)
	cmd.Stdout = &out
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
		return "", fmt.Errorf("cookie-scoop failed: %w", err)
	}

	result := strings.TrimSpace(out.String())
	result = strings.TrimPrefix(result, "Cookie: ")
	return result, nil
}

// --- Live browser cookie extraction (session cookies fallback) ---

// extractLiveCookies reads document.cookie from a live Chrome session.
// Tries: agent-browser → browser-use → raw CDP, in that order.
func extractLiveCookies(domain string) (string, error) {
	targetURL := "https://" + strings.TrimPrefix(domain, ".")

	// 1. Try browser-use --connect (preferred — has full DOM/cookie access)
	if _, err := exec.LookPath("browser-use"); err == nil {
		cookies, err := extractViaBrowserUse(targetURL)
		if err == nil && cookies != "" {
			return cookies, nil
		}
	}

	// 2. Try agent-browser --auto-connect (fallback — limited eval in auto-connect mode)
	if _, err := exec.LookPath("agent-browser"); err == nil {
		cookies, err := extractViaAgentBrowser(targetURL)
		if err == nil && cookies != "" {
			return cookies, nil
		}
	}

	// 3. Try raw CDP on common debug ports
	for _, port := range []string{"9222", "9229"} {
		cookies, err := extractViaCDP(targetURL, port)
		if err == nil && cookies != "" {
			return cookies, nil
		}
	}

	return "", fmt.Errorf("no live browser connection available (tried agent-browser, browser-use, CDP)")
}

func extractViaAgentBrowser(targetURL string) (string, error) {
	// Open the target URL (attaches to running Chrome)
	openCmd := exec.Command("agent-browser", "--auto-connect", "open", targetURL)
	openCmd.Stderr = os.Stderr
	if err := openCmd.Run(); err != nil {
		return "", fmt.Errorf("agent-browser open: %w", err)
	}

	// Read document.cookie via eval
	var out bytes.Buffer
	evalCmd := exec.Command("agent-browser", "eval", "document.cookie")
	evalCmd.Stdout = &out
	evalCmd.Stderr = os.Stderr
	if err := evalCmd.Run(); err != nil {
		closeCmd := exec.Command("agent-browser", "close")
		_ = closeCmd.Run()
		return "", fmt.Errorf("agent-browser eval: %w", err)
	}

	// Close the daemon
	closeCmd := exec.Command("agent-browser", "close")
	_ = closeCmd.Run()

	return strings.TrimSpace(out.String()), nil
}

func extractViaBrowserUse(targetURL string) (string, error) {
	// Try connecting to running Chrome
	openCmd := exec.Command("browser-use", "--connect", "open", targetURL)
	openCmd.Stderr = os.Stderr
	if err := openCmd.Run(); err != nil {
		return "", fmt.Errorf("browser-use open: %w", err)
	}

	var out bytes.Buffer
	evalCmd := exec.Command("browser-use", "eval", "document.cookie")
	evalCmd.Stdout = &out
	evalCmd.Stderr = os.Stderr
	if err := evalCmd.Run(); err != nil {
		closeCmd := exec.Command("browser-use", "close")
		_ = closeCmd.Run()
		return "", fmt.Errorf("browser-use eval: %w", err)
	}

	closeCmd := exec.Command("browser-use", "close")
	_ = closeCmd.Run()

	result := strings.TrimSpace(out.String())
	// browser-use eval prefixes with "result: "
	result = strings.TrimPrefix(result, "result: ")
	return result, nil
}

func extractViaCDP(targetURL, port string) (string, error) {
	// Discover available tabs via CDP JSON endpoint
	listURL := fmt.Sprintf("http://localhost:%s/json", port)
	client := &http.Client{Timeout: 2 * time.Second}
	resp, err := client.Get(listURL)
	if err != nil {
		return "", fmt.Errorf("CDP not available on port %s", port)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", err
	}

	// Find a tab matching the target domain
	domain := strings.TrimPrefix(strings.TrimPrefix(targetURL, "https://"), "http://")
	var tabs []struct {
		URL                  string `json:"url"`
		WebSocketDebuggerURL string `json:"webSocketDebuggerUrl"`
	}
	if err := json.Unmarshal(body, &tabs); err != nil {
		return "", fmt.Errorf("parsing CDP tabs: %w", err)
	}

	var wsURL string
	for _, tab := range tabs {
		if strings.Contains(tab.URL, domain) {
			wsURL = tab.WebSocketDebuggerURL
			break
		}
	}
	if wsURL == "" {
		return "", fmt.Errorf("no Chrome tab found for %s", domain)
	}

	return evalDocumentCookieViaCDP(wsURL, domain)
}

// evalDocumentCookieViaCDP opens the per-tab DevTools WebSocket and runs a
// single Runtime.evaluate of `document.cookie`. Bounded by a single 5-second
// wall-clock deadline shared across the dial, write, and all subsequent frame
// reads — net.Conn.SetReadDeadline is a single deadline covering all reads on
// the connection, not a per-message reset, so heavy CDP event traffic chips
// away at the same budget. Returns the cookie string verbatim (no parsing) so
// the existing extractLiveCookies pipeline can hand it to refreshStoredBrowserCookies.
func evalDocumentCookieViaCDP(wsURL, domain string) (string, error) {
	dialer := websocket.Dialer{HandshakeTimeout: 2 * time.Second}
	conn, _, err := dialer.Dial(wsURL, nil)
	if err != nil {
		return "", fmt.Errorf("CDP WebSocket dial failed for %s: %w", domain, err)
	}
	defer conn.Close()

	deadline := time.Now().Add(5 * time.Second)
	_ = conn.SetWriteDeadline(deadline)
	_ = conn.SetReadDeadline(deadline)

	const evalID = 1
	evalReq := map[string]any{
		"id":     evalID,
		"method": "Runtime.evaluate",
		"params": map[string]any{
			"expression":    "document.cookie",
			"returnByValue": true,
			"awaitPromise":  true,
		},
	}
	if err := conn.WriteJSON(evalReq); err != nil {
		return "", fmt.Errorf("CDP Runtime.evaluate write failed for %s: %w", domain, err)
	}

	// Drain frames until we see our response. CDP can interleave events from
	// other domains (Network, Page) on the same socket if those domains were
	// enabled in this tab by another client; ignore them and keep reading
	// until the shared 5-second deadline above fires. Heavy event traffic can
	// exhaust the budget before our response arrives — that's by design (a
	// stuck tab fails fast) rather than a per-read timeout.
	for {
		var msg struct {
			ID     int             `json:"id"`
			Result json.RawMessage `json:"result"`
			Error  *struct {
				Code    int    `json:"code"`
				Message string `json:"message"`
			} `json:"error"`
		}
		if err := conn.ReadJSON(&msg); err != nil {
			return "", fmt.Errorf("CDP Runtime.evaluate read failed for %s: %w", domain, err)
		}
		if msg.ID != evalID {
			continue
		}
		if msg.Error != nil {
			return "", fmt.Errorf("CDP Runtime.evaluate error for %s: %s", domain, msg.Error.Message)
		}
		var result struct {
			Result struct {
				Type  string `json:"type"`
				Value string `json:"value"`
			} `json:"result"`
			ExceptionDetails *struct {
				Text string `json:"text"`
			} `json:"exceptionDetails"`
		}
		if err := json.Unmarshal(msg.Result, &result); err != nil {
			return "", fmt.Errorf("CDP Runtime.evaluate result parse failed for %s: %w", domain, err)
		}
		if result.ExceptionDetails != nil {
			return "", fmt.Errorf("CDP page threw evaluating document.cookie for %s: %s", domain, result.ExceptionDetails.Text)
		}
		return result.Result.Value, nil
	}
}
