// Copyright 2026 printing-press-golden. 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 (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"io"
	"os"
	"strings"
	"time"

	"github.com/spf13/cobra"
	"printing-press-golden-pp-cli/internal/client"
	"printing-press-golden-pp-cli/internal/cliutil"
	"printing-press-golden-pp-cli/internal/config"
	"printing-press-golden-pp-cli/internal/store"
)

// looksLikeDoctorInterstitial reports whether the response body matches a known
// bot-detection challenge page (Cloudflare, Akamai, Vercel, AWS WAF, DataDome,
// PerimeterX). Only fires on the doctor probe — used to distinguish "transport
// reached the wall" from "transport failed entirely." Returns the vendor name
// when matched, or empty string when no match.
//
// Markers are anchored to <title> or vendor-specific strings to avoid
// false-positives on benign content. For example, a recipe titled "Just A
// Moment of Pause Cookies" must NOT match the Cloudflare challenge marker;
// only "<title>just a moment" (the actual interstitial title) does.
func looksLikeDoctorInterstitial(body []byte) string {
	if len(body) == 0 {
		return ""
	}
	limit := len(body)
	if limit > 8192 {
		limit = 8192
	}
	prefix := strings.ToLower(string(body[:limit]))
	if !strings.Contains(prefix, "<title") {
		// Every bot interstitial we recognize sets a <title>; bodies without
		// one are body-only API responses, not challenge pages.
		return ""
	}
	switch {
	case strings.Contains(prefix, "<title>just a moment") || // CF JS challenge
		strings.Contains(prefix, "challenges.cloudflare.com") || // CF Turnstile
		(strings.Contains(prefix, "attention required") && strings.Contains(prefix, "cloudflare")):
		return "Cloudflare"
	case strings.Contains(prefix, "akamai") && (strings.Contains(prefix, "request unsuccessful") || strings.Contains(prefix, "access denied")):
		return "Akamai"
	case strings.Contains(prefix, "x-vercel-mitigated") || strings.Contains(prefix, "x-vercel-challenge-token") ||
		(strings.Contains(prefix, "vercel") && strings.Contains(prefix, "challenge")):
		return "Vercel"
	case strings.Contains(prefix, "request blocked") && strings.Contains(prefix, "aws waf"):
		return "AWS WAF"
	case strings.Contains(prefix, "datadome") && (strings.Contains(prefix, "blocked") || strings.Contains(prefix, "captcha") || strings.Contains(prefix, "challenge")):
		return "DataDome"
	case strings.Contains(prefix, "perimeterx") || strings.Contains(prefix, "px-captcha"):
		return "PerimeterX"
	}
	return ""
}

// suggestReadCommand walks the Cobra tree to find an endpoint-mirror command
// an operator can run to confirm credentials work end-to-end. Picks the
// first leaf that (a) carries the `pp:endpoint` annotation, so it actually
// dials the API rather than reading a local file like `feedback list` or
// `profile list`; (b) has a list/get verb; and (c) takes no positional
// arguments, so the suggestion is copy-paste runnable. Returns the dotted
// command path (e.g. "issues list") or "" when no such command exists —
// common in mutation-only CLIs and in CLIs where every read command has
// required positional arguments.
func suggestReadCommand(root *cobra.Command) string {
	if root == nil {
		return ""
	}
	var found string
	var walk func(*cobra.Command, []string)
	walk = func(cmd *cobra.Command, path []string) {
		if found != "" {
			return
		}
		for _, child := range cmd.Commands() {
			childPath := append(append([]string{}, path...), child.Name())
			if isSuggestableReadLeaf(child) {
				found = strings.Join(childPath, " ")
				return
			}
			// Recurse even into Hidden parents: printed CLIs mark raw
			// resource parents Hidden to keep --help curated, but their
			// endpoint leaves remain runnable (`<cli> projects list`
			// works). Skipping hidden subtrees would make this return ""
			// in nearly every CLI. isSuggestableReadLeaf still rejects a
			// leaf that is itself Hidden.
			walk(child, childPath)
			if found != "" {
				return
			}
		}
	}
	walk(root, nil)
	return found
}

func isSuggestableReadLeaf(cmd *cobra.Command) bool {
	if cmd == nil || cmd.Hidden || cmd.HasSubCommands() || !cmd.Runnable() {
		return false
	}
	// Only endpoint-mirror commands count; framework commands like
	// `feedback list` and `profile list` read local files and would
	// recreate the false-confidence failure mode the suggestion is
	// supposed to avoid.
	if cmd.Annotations["pp:endpoint"] == "" {
		return false
	}
	verb := strings.ToLower(strings.SplitN(cmd.Use, " ", 2)[0])
	if verb != "list" && verb != "get" {
		return false
	}
	// Endpoint commands with positional path params advertise them in
	// Use as `<id>` (required) or `[id]` (optional). The runtime body
	// rejects empty args by printing help, so suggesting one would not
	// actually exercise the token — reject before the Args probe below.
	if strings.ContainsAny(cmd.Use, "<[") {
		return false
	}
	// Probe the Args validator with an empty positional-arg list. A nil
	// validator accepts anything (including zero args); a non-nil validator
	// that returns nil for [] accepts zero args. Either qualifies — the
	// suggestion `<cli> list` is then a complete command.
	if cmd.Args == nil {
		return true
	}
	return cmd.Args(cmd, []string{}) == nil
}

func newDoctorCmd(flags *rootFlags) *cobra.Command {
	var failOn string
	cmd := &cobra.Command{
		Use:   "doctor",
		Short: "Check CLI health",
		Example: `  printing-press-golden-pp-cli doctor
  printing-press-golden-pp-cli doctor --json
  printing-press-golden-pp-cli doctor --fail-on warn`,
		RunE: func(cmd *cobra.Command, args []string) error {
			report := map[string]any{}

			// Check config
			cfg, err := config.Load(flags.configPath)
			if err != nil {
				report["config"] = fmt.Sprintf("error: %s", err)
			} else {
				report["config"] = "ok"
				report["config_path"] = cfg.Path
				report["base_url"] = cfg.BaseURL
			}

			// Check auth
			authConfigured := false
			if cfg != nil {
				header := cfg.AuthHeader()
				if header == "" {
					report["auth"] = "not configured"
					report["auth_hint"] = "export PRINTING_PRESS_GOLDEN_API_KEY=<your-key>"
				} else {
					authConfigured = true
					report["auth"] = "configured"
					report["auth_source"] = cfg.AuthSource
				}
			}

			// Check auth environment variables
			authEnvSet := []string{}
			authEnvRequiredMissing := []string{}
			authEnvInfo := []string{}
			authEnvOptionalNames := []string{}
			// Validation rejects multi-OR-group specs upstream, so the single optional-satisfied state is sufficient at runtime.
			authEnvOptionalSatisfied := false
			if os.Getenv("PRINTING_PRESS_GOLDEN_API_KEY") != "" {
				authEnvSet = append(authEnvSet, "PRINTING_PRESS_GOLDEN_API_KEY")
			} else if authConfigured {
				authSource, _ := report["auth_source"].(string)
				if authSource == "" {
					authSource = "config"
				}
				authEnvInfo = append(authEnvInfo, "credentials available from "+authSource)
			} else {
				authEnvRequiredMissing = append(authEnvRequiredMissing, "PRINTING_PRESS_GOLDEN_API_KEY")
			}
			switch {
			case len(authEnvRequiredMissing) > 0:
				report["env_vars"] = "ERROR missing required: " + strings.Join(authEnvRequiredMissing, ", ")
			case len(authEnvOptionalNames) > 1 && !authEnvOptionalSatisfied:
				report["env_vars"] = "INFO set one of: " + strings.Join(authEnvOptionalNames, " or ")
			case len(authEnvInfo) > 0 && authConfigured:
				report["env_vars"] = "OK " + strings.Join(authEnvInfo, "; ")
			case len(authEnvInfo) > 0:
				report["env_vars"] = "INFO " + strings.Join(authEnvInfo, "; ")
			default:
				report["env_vars"] = fmt.Sprintf("OK %d/%d available", len(authEnvSet), 1)
			}

			// Check API connectivity and validate credentials.
			//
			// The doctor uses the same client every other command uses --
			// flags.newClient() returns a *client.Client wrapping whatever
			// transport the spec declared (Surf for browser-chrome, stdlib
			// for standard). A separate stdlib http.Client would silently
			// bypass that choice and report false negatives against
			// Cloudflare-fronted, Akamai-fronted, or otherwise bot-detected
			// sites. By going through flags.newClient(), the doctor's
			// reachability verdict matches what real commands experience.
			if cfg != nil && cfg.BaseURL != "" {
				c, clientErr := flags.newClient()
				if clientErr != nil {
					report["api"] = fmt.Sprintf("client init error: %s", clientErr)
				} else {
					// Step 1: Basic reachability via the configured transport.
					reachBody, reachErr := c.Get("/", nil)
					var reachAPIErr *client.APIError
					switch {
					case reachErr == nil:
						// 2xx response — clearly reachable. Still inspect the
						// body for a known interstitial; some bot walls return
						// 200 with a JS challenge page.
						if vendor := looksLikeDoctorInterstitial(reachBody); vendor != "" {
							report["api"] = fmt.Sprintf("blocked by %s interstitial — the configured transport reached the wall. Try a different network, wait for the IP-level rate limit to clear, or check that the browser-chrome transport is bound correctly.", vendor)
						} else {
							report["api"] = "reachable"
						}
					case errors.As(reachErr, &reachAPIErr):
						// Non-2xx from the server. The network reached, the
						// server responded — that's "reachable" for our
						// purposes. Inspect the response body for a known
						// interstitial first; otherwise note the status.
						status := reachAPIErr.StatusCode
						if vendor := looksLikeDoctorInterstitial([]byte(reachAPIErr.Body)); vendor != "" {
							report["api"] = fmt.Sprintf("blocked by %s interstitial (HTTP %d) — the configured transport reached the wall.", vendor, status)
						} else {
							report["api"] = fmt.Sprintf("reachable (HTTP %d at /)", status)
						}
					default:
						// Network-level failure: DNS, connection refused, TLS,
						// transport init, etc. The transport itself didn't
						// connect.
						report["api"] = fmt.Sprintf("unreachable: %s", reachErr)
					}

					// Step 2: Validate credentials with an authenticated probe.
					authHeader := cfg.AuthHeader()
					if authHeader == "" {
						// No auth configured — skip credential validation
					} else if reachErr != nil && !errors.As(reachErr, &reachAPIErr) {
						report["credentials"] = "skipped (API unreachable)"
					} else {
						suggestion := suggestReadCommand(cmd.Root())
						if suggestion != "" {
							report["credentials"] = fmt.Sprintf("present, not verified. Run `%s %s` to confirm the token works end-to-end.", "printing-press-golden-pp-cli", suggestion)
						} else {
							report["credentials"] = "present, not verified. Run any read command to confirm the token works end-to-end."
						}
					}
				}
			} else if cfg != nil && cfg.BaseURL == "" {
				report["api"] = "not configured (set base_url in config file)"
			}
			// Cache health: only reported when this CLI has a local store.
			// Surfaces rows + last_synced_at per resource, schema version,
			// and a fresh/stale/unknown verdict so agents can introspect
			// whether to trust the cached data before issuing queries.
			report["cache"] = collectCacheReport(cmd.Context(), "")

			// Verify mode state. Surfaced so an operator who unintentionally
			// inherits PRINTING_PRESS_VERIFY=1 (parent shell, CI runner, container
			// image) detects the foot-gun without inspecting a response body.
			// Pairs with the synthetic envelope's verify_noop / reason literals
			// as a second diagnosis anchor.
			if cliutil.IsVerifyEnv() {
				if cliutil.IsVerifyLiveHTTPEnv() {
					report["verify_mode"] = "INFO ACTIVE — live HTTP opt-in (mutating verbs dial out)"
				} else {
					report["verify_mode"] = "INFO ACTIVE — mutating HTTP verbs short-circuit (PRINTING_PRESS_VERIFY=1; no network calls for DELETE/POST/PUT/PATCH)"
				}
			} else {
				report["verify_mode"] = "normal operation"
			}

			report["version"] = version

			if flags.asJSON {
				if err := printJSONFiltered(cmd.OutOrStdout(), report, flags); err != nil {
					return err
				}
				return doctorExitForFailOn(failOn, report)
			}

			// Human-readable output with color
			w := cmd.OutOrStdout()
			checkKeys := []struct{ key, label string }{
				{"config", "Config"},
				{"auth", "Auth"},
				{"env_vars", "Env Vars"},
				{"verify_mode", "Verify Mode"},
				{"api", "API"},
				{"credentials", "Credentials"},
			}
			for _, ck := range checkKeys {
				v, ok := report[ck.key]
				if !ok {
					continue
				}
				s := fmt.Sprintf("%v", v)
				indicator := green("OK")
				switch {
				case strings.HasPrefix(s, "INFO"):
					indicator = yellow("INFO")
				case strings.HasPrefix(s, "ERROR"):
					indicator = red("FAIL")
				case strings.HasPrefix(s, "optional"):
					// Optional-auth CLI with no key set — informational, not a failure.
					indicator = yellow("INFO")
				case strings.Contains(s, "scope-limited"):
					indicator = yellow("WARN")
				case strings.Contains(s, "not verified"):
					// "present, not verified" — credentials are loaded but no
					// probe ran. Informational, not a warning; a clean config
					// shouldn't render yellow WARN in CI dashboards.
					indicator = yellow("INFO")
				case strings.Contains(s, "error") || strings.Contains(s, "not configured") || strings.Contains(s, "unreachable") || strings.Contains(s, "invalid") || strings.Contains(s, "missing"):
					indicator = red("FAIL")
				case s == "not required":
					// Public APIs: no auth needed is a healthy state, not a warning.
					indicator = green("OK")
				case strings.Contains(s, "not ") || strings.Contains(s, "skipped") || strings.Contains(s, "inferred"):
					indicator = yellow("WARN")
				}
				fmt.Fprintf(w, "  %s %s: %s\n", indicator, ck.label, s)
			}
			// Print info keys without status indicator
			for _, key := range []string{"config_path", "base_url", "auth_source", "version"} {
				if v, ok := report[key]; ok {
					fmt.Fprintf(w, "  %s: %v\n", key, v)
				}
			}
			// Print auth setup hints (indented under Auth line)
			if hint, ok := report["auth_hint"]; ok {
				fmt.Fprintf(w, "  hint: %v\n", hint)
			}
			// Cache section: render after the primary health block so it
			// sits next to version info, mirroring the JSON report layout.
			if cacheAny, ok := report["cache"]; ok {
				if cacheRep, ok := cacheAny.(map[string]any); ok {
					renderCacheReport(w, cacheRep)
				}
			}
			return doctorExitForFailOn(failOn, report)
		},
	}
	cmd.Flags().StringVar(&failOn, "fail-on", "", "Exit non-zero when a health level is reached: stale, error. Default is never.")
	return cmd
}

// doctorExitForFailOn returns a non-nil error when the report's worst
// status meets or exceeds the --fail-on threshold. "error" always trips
// when any section reports an error; "stale" also trips when the cache
// section is stale. The default empty string means never fail on status.
func doctorExitForFailOn(failOn string, report map[string]any) error {
	if failOn == "" {
		return nil
	}
	worstError := false
	worstStale := false
	for _, v := range report {
		s, ok := v.(string)
		if ok {
			if strings.Contains(s, "error") || strings.Contains(s, "unreachable") || strings.Contains(s, "invalid") || strings.Contains(s, "missing") {
				worstError = true
			}
		}
		if m, ok := v.(map[string]any); ok {
			if st, _ := m["status"].(string); st == "error" {
				worstError = true
			} else if st == "stale" {
				worstStale = true
			}
		}
	}
	switch failOn {
	case "error":
		if worstError {
			return fmt.Errorf("doctor: --fail-on=error triggered")
		}
	case "stale":
		if worstError || worstStale {
			return fmt.Errorf("doctor: --fail-on=stale triggered")
		}
	default:
		return fmt.Errorf("doctor: unknown --fail-on value %q (valid: stale, error)", failOn)
	}
	return nil
}

// collectCacheReport opens the local store, reads per-resource sync state,
// and returns a map summarising cache health. Never panics on missing DB
// or open failure; returns a map with status=unknown or status=error so the
// caller can render and agents can interpret.
//
// staleAfterSpec is the CLI's configured threshold (e.g. "6h"); empty means
// use the runtime default. The default is deliberately conservative (6h)
// because the alternative is no freshness story at all.
func collectCacheReport(ctx context.Context, staleAfterSpec string) map[string]any {
	report := map[string]any{}
	dbPath := defaultDBPath("printing-press-golden-pp-cli")
	report["db_path"] = dbPath

	fi, err := os.Stat(dbPath)
	if err != nil {
		if os.IsNotExist(err) {
			report["status"] = "unknown"
			report["hint"] = "Database not created yet; run 'printing-press-golden-pp-cli sync' to hydrate."
			return report
		}
		report["status"] = "error"
		report["error"] = err.Error()
		return report
	}
	report["db_bytes"] = fi.Size()

	s, err := store.OpenWithContext(ctx, dbPath)
	if err != nil {
		report["status"] = "error"
		report["error"] = err.Error()
		return report
	}
	defer s.Close()

	if v, verr := s.SchemaVersion(); verr == nil {
		report["schema_version"] = v
	}

	staleAfter := 6 * time.Hour
	if staleAfterSpec != "" {
		if d, derr := time.ParseDuration(staleAfterSpec); derr == nil {
			staleAfter = d
		}
	}

	rows, qerr := s.DB().Query(`SELECT resource_type, COALESCE(total_count, 0), last_synced_at FROM sync_state ORDER BY resource_type`)
	if qerr != nil {
		// sync_state may not exist on a fresh DB that has migrated but not
		// yet had any sync runs — treat as unknown rather than error.
		report["status"] = "unknown"
		report["hint"] = "No sync state recorded; run 'printing-press-golden-pp-cli sync' to populate."
		return report
	}
	defer rows.Close()

	var resources []map[string]any
	fresh := true
	haveAny := false
	oldest := time.Duration(0)
	for rows.Next() {
		var rtype string
		var count int64
		var lastSynced sql.NullTime
		if err := rows.Scan(&rtype, &count, &lastSynced); err != nil {
			continue
		}
		r := map[string]any{"type": rtype, "rows": count}
		if lastSynced.Valid {
			haveAny = true
			r["last_synced_at"] = lastSynced.Time.UTC().Format(time.RFC3339)
			age := time.Since(lastSynced.Time)
			r["staleness"] = age.Round(time.Minute).String()
			if age > staleAfter {
				fresh = false
			}
			if age > oldest {
				oldest = age
			}
		} else {
			r["staleness"] = "never"
			fresh = false
		}
		resources = append(resources, r)
	}
	report["resources"] = resources
	report["stale_after"] = staleAfter.String()

	switch {
	case !haveAny && len(resources) == 0:
		report["status"] = "unknown"
		report["hint"] = "sync_state is empty; run 'printing-press-golden-pp-cli sync' to hydrate."
	case fresh:
		report["status"] = "fresh"
	default:
		report["status"] = "stale"
		report["oldest_age"] = oldest.Round(time.Minute).String()
		report["hint"] = "Some resources are older than stale_after; run 'printing-press-golden-pp-cli sync' to refresh."
	}
	return report
}

func renderCacheReport(w io.Writer, rep map[string]any) {
	status, _ := rep["status"].(string)
	indicator := green("OK")
	switch status {
	case "stale":
		indicator = yellow("WARN")
	case "error":
		indicator = red("FAIL")
	case "unknown":
		indicator = yellow("INFO")
	}
	fmt.Fprintf(w, "  %s Cache: %s\n", indicator, status)
	if v, ok := rep["db_path"]; ok {
		fmt.Fprintf(w, "    db_path: %v\n", v)
	}
	if v, ok := rep["schema_version"]; ok {
		fmt.Fprintf(w, "    schema_version: %v\n", v)
	}
	if v, ok := rep["db_bytes"]; ok {
		fmt.Fprintf(w, "    db_bytes: %v\n", v)
	}
	if v, ok := rep["stale_after"]; ok {
		fmt.Fprintf(w, "    stale_after: %v\n", v)
	}
	if v, ok := rep["oldest_age"]; ok {
		fmt.Fprintf(w, "    oldest_age: %v\n", v)
	}
	if resourcesAny, ok := rep["resources"]; ok {
		if resources, ok := resourcesAny.([]map[string]any); ok && len(resources) > 0 {
			fmt.Fprintf(w, "    resources:\n")
			for _, r := range resources {
				rtype, _ := r["type"].(string)
				rows := r["rows"]
				staleness, _ := r["staleness"].(string)
				fmt.Fprintf(w, "      - %s: %v rows, %s\n", rtype, rows, staleness)
			}
		}
	}
	if hint, ok := rep["hint"]; ok {
		fmt.Fprintf(w, "    hint: %v\n", hint)
	}
}
