// 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 (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"github.com/spf13/cobra"
	"github.com/spf13/pflag"
	"io"
	"os"
	"path/filepath"
	"printing-press-rich-pp-cli/internal/client"
	"printing-press-rich-pp-cli/internal/cliutil"
	"regexp"
	"sort"
	"strings"
	"text/tabwriter"
	"time"
	"unicode"
)

var As = errors.As

// noColor is set by the --no-color flag
var noColor bool

// humanFriendly is set by the --human-friendly flag; colors are off by default (agent-safe)
var humanFriendly bool

func colorEnabled() bool {
	if noColor {
		return false
	}
	if !humanFriendly {
		return false
	}
	if os.Getenv("NO_COLOR") != "" {
		return false
	}
	if os.Getenv("TERM") == "dumb" {
		return false
	}
	return isTerminal(os.Stdout)
}

func isTerminal(w io.Writer) bool {
	if f, ok := w.(*os.File); ok {
		fi, err := f.Stat()
		if err != nil {
			return true
		}
		return (fi.Mode() & os.ModeCharDevice) != 0
	}
	return false
}

func bold(s string) string {
	if !colorEnabled() {
		return s
	}
	return "\033[1m" + s + "\033[0m"
}

func green(s string) string {
	if !colorEnabled() {
		return s
	}
	return "\033[32m" + s + "\033[0m"
}

func red(s string) string {
	if !colorEnabled() {
		return s
	}
	return "\033[31m" + s + "\033[0m"
}

func yellow(s string) string {
	if !colorEnabled() {
		return s
	}
	return "\033[33m" + s + "\033[0m"
}

type cliError struct {
	code int
	err  error
}

func (e *cliError) Error() string { return e.err.Error() }
func (e *cliError) Unwrap() error { return e.err }

func usageErr(err error) error     { return &cliError{code: 2, err: err} }
func notFoundErr(err error) error  { return &cliError{code: 3, err: err} }
func authErr(err error) error      { return &cliError{code: 4, err: err} }
func apiErr(err error) error       { return &cliError{code: 5, err: err} }
func configErr(err error) error    { return &cliError{code: 10, err: err} }
func rateLimitErr(err error) error { return &cliError{code: 7, err: err} }

// partialFailureErr signals that the upstream API returned a 2xx with a
// body shape indicating some operations in a batch failed (e.g. Google
// Ads `partialFailureError`, similar shapes from Drive batch, Sheets
// batchUpdate, Cloud Resource Manager). Distinct from apiErr (HTTP-level
// failure) so callers can distinguish "request rejected" from "request
// accepted but some ops failed".
func partialFailureErr(err error) error { return &cliError{code: 6, err: err} }

// partialFailureReport describes the structured detection result for a
// mutate-style response body. Emitted in the envelope under
// "partial_failure" so machine-readable callers can route per-operation
// remediation.
type partialFailureReport struct {
	Field         string   `json:"field"`
	Message       string   `json:"message,omitempty"`
	Code          int      `json:"code,omitempty"`
	Details       any      `json:"details,omitempty"`
	ResourceNames []string `json:"resource_names,omitempty"`
}

// detectPartialFailure inspects a mutate-style JSON response for a
// partial-failure-shaped field. Returns nil when no partial failure is
// detected. The detector is intentionally generic across APIs that emit
// 2xx-with-batch-errors. New partial-failure-shaped fields are added to
// partialFailureFields, not at call sites. When `results[]` is present
// (Google Ads convention) it extracts per-op `resourceName` so callers
// can see which operations did succeed.
func detectPartialFailure(data []byte) *partialFailureReport {
	if len(data) == 0 {
		return nil
	}
	var top map[string]any
	if err := json.Unmarshal(data, &top); err != nil {
		return nil
	}
	partialFailureFields := []string{"partialFailureError"}
	for _, field := range partialFailureFields {
		raw, ok := top[field]
		if !ok || raw == nil {
			continue
		}
		obj, ok := raw.(map[string]any)
		if !ok {
			continue
		}
		message, _ := obj["message"].(string)
		var code int
		if n, ok := obj["code"].(float64); ok {
			code = int(n)
		}
		// Empty object means partial-failure mode was off or no ops
		// failed; do not flag.
		if code == 0 && strings.TrimSpace(message) == "" {
			continue
		}
		report := &partialFailureReport{
			Field:   field,
			Message: message,
			Code:    code,
			Details: obj["details"],
		}
		if results, ok := top["results"].([]any); ok {
			for _, r := range results {
				if rm, ok := r.(map[string]any); ok {
					if name, ok := rm["resourceName"].(string); ok && name != "" {
						report.ResourceNames = append(report.ResourceNames, name)
					}
				}
			}
		}
		return report
	}
	return nil
}

// dryRunOK reports whether the command should short-circuit without doing any
// real work because --dry-run was set. The verify pipeline probes hand-written
// commands with --dry-run; commands that put validation in cobra's `Args:` or
// `MarkFlagRequired` cannot reach a dry-run guard inside RunE because cobra
// runs those checks before RunE. The verify-friendly pattern for hand-written
// commands is:
//
//	RunE: func(cmd *cobra.Command, args []string) error {
//	    if len(args) == 0 {
//	        return cmd.Help()
//	    }
//	    if dryRunOK(flags) {
//	        return nil
//	    }
//	    // ... real work ...
//	}
//
// See SKILL.md "Phase 3: Build The GOAT" for the full pattern.
func dryRunOK(flags *rootFlags) bool {
	return flags != nil && flags.dryRun
}

// parentNoSubcommandRunE returns a RunE that handles parents invoked without a
// subcommand. In machine output (--json/--agent) the parent emits a structured
// error to stdout listing valid subcommands and exits 2; otherwise cobra's
// default help text is printed. Without this, agents driving the CLI in
// --agent mode received only human-readable help on stdout and exit 0, with no
// signal that the invocation was incomplete.
func parentNoSubcommandRunE(flags *rootFlags) func(*cobra.Command, []string) error {
	return func(cmd *cobra.Command, args []string) error {
		if flags != nil && flags.asJSON {
			subs := make([]string, 0, len(cmd.Commands()))
			for _, c := range cmd.Commands() {
				if c.IsAvailableCommand() && c.Name() != "help" {
					subs = append(subs, c.Name())
				}
			}
			sort.Strings(subs)
			_ = json.NewEncoder(cmd.OutOrStdout()).Encode(map[string]any{
				"error":             "subcommand required",
				"valid_subcommands": subs,
			})
			return usageErr(fmt.Errorf("subcommand required for %q", cmd.CommandPath()))
		}
		return cmd.Help()
	}
}

// accessWarning describes an API access-denial that sync converts into a
// non-fatal warning. It carries enough structured data for the sync_warning
// JSON event without parsing free-form error strings downstream.
type accessWarning struct {
	Status  int    // HTTP status when applicable; 0 for GraphQL field-level denials.
	Reason  string // "forbidden" | "insufficient_access" | "unauthenticated"
	Message string // human-readable detail (the API's body or GraphQL error message)
}

// syncErrorJSON returns a one-line JSON sync_error event. When err wraps a
// *client.APIError, the structured status/method/path/body fields let
// operators see the API's response body without parsing a wrapped error
// string — required for diagnosing 4xx responses on endpoints whose OpenAPI
// spec marks filter params optional but the API treats them as required.
//
// parent is non-empty only on the dependent-resource path, where it
// identifies which parent ID's child request failed.
func syncErrorJSON(resource, parent string, err error) string {
	payload := struct {
		Event    string `json:"event"`
		Resource string `json:"resource"`
		Parent   string `json:"parent,omitempty"`
		Status   int    `json:"status,omitempty"`
		Method   string `json:"method,omitempty"`
		Path     string `json:"path,omitempty"`
		Body     string `json:"body,omitempty"`
		Error    string `json:"error"`
	}{
		Event:    "sync_error",
		Resource: resource,
		Parent:   parent,
		Error:    err.Error(),
	}
	var apiErr *client.APIError
	if errors.As(err, &apiErr) {
		payload.Status = apiErr.StatusCode
		payload.Method = apiErr.Method
		payload.Path = apiErr.Path
		payload.Body = apiErr.Body
	}
	out, _ := json.Marshal(payload)
	return string(out)
}

// syncUserParams carries user-supplied query parameters injected into sync
// HTTP requests. flatGlobal entries come from --param and inject into
// flat-list requests only; trueGlobal entries come from --global-param and
// inject into every request including dependent path-scoped calls.
// perResource entries win over both on key conflict.
//
// The flat/dependent split avoids a real failure mode: a top-level scope
// like workspace=<gid> belongs on flat-list requests (/projects, /tags) but
// re-injecting it onto a path-scoped dependent request
// (/projects/<gid>/tasks?workspace=<gid>) makes APIs like Asana reject the
// call ("Must specify exactly one of project, tag, ..."). Operators who
// need the old "apply everywhere" semantic opt back in with --global-param.
type syncUserParams struct {
	flatGlobal  map[string]string
	trueGlobal  map[string]string
	perResource map[string]map[string]string
}

// parseSyncUserParams parses the repeatable --param key=value,
// --global-param key=value, and --resource-param resource:key=value flags.
// Returns usage errors keyed on the specific invalid token so the user
// sees which entry was rejected.
func parseSyncUserParams(flatGlobalFlags, resourceParamFlags, trueGlobalFlags []string) (*syncUserParams, error) {
	flatGlobal, err := parseSyncKVFlags(flatGlobalFlags, "--param")
	if err != nil {
		return nil, err
	}
	trueGlobal, err := parseSyncKVFlags(trueGlobalFlags, "--global-param")
	if err != nil {
		return nil, err
	}
	p := &syncUserParams{
		flatGlobal:  flatGlobal,
		trueGlobal:  trueGlobal,
		perResource: map[string]map[string]string{},
	}
	for _, spec := range resourceParamFlags {
		resource, kv, ok := strings.Cut(spec, ":")
		if !ok || resource == "" {
			return nil, fmt.Errorf("invalid --resource-param %q: expected resource:key=value", spec)
		}
		k, v, ok := strings.Cut(kv, "=")
		if !ok || k == "" {
			return nil, fmt.Errorf("invalid --resource-param %q: expected resource:key=value", spec)
		}
		if p.perResource[resource] == nil {
			p.perResource[resource] = map[string]string{}
		}
		p.perResource[resource][k] = v
	}
	return p, nil
}

// parseSyncKVFlags parses a slice of "key=value" tokens into a map. The
// flagName label flows into the usage error so a malformed entry tells
// the user which flag was at fault.
func parseSyncKVFlags(flags []string, flagName string) (map[string]string, error) {
	out := map[string]string{}
	for _, kv := range flags {
		k, v, ok := strings.Cut(kv, "=")
		if !ok || k == "" {
			return nil, fmt.Errorf("invalid %s %q: expected key=value", flagName, kv)
		}
		out[k] = v
	}
	return out, nil
}

// applyTo merges user params into the request map. Called after
// spec-derived params (cursor, since, page-size, dates) so user flags can
// override them. isDependent=true skips flatGlobal (--param), which
// targets flat-list endpoints; trueGlobal (--global-param) and perResource
// always apply.
func (p *syncUserParams) applyTo(resource string, params map[string]string, isDependent bool) {
	if p == nil {
		return
	}
	if !isDependent {
		for k, v := range p.flatGlobal {
			params[k] = v
		}
	}
	for k, v := range p.trueGlobal {
		params[k] = v
	}
	for k, v := range p.perResource[resource] {
		params[k] = v
	}
}

// validateResourceNames returns a usage-shaped error when any --resource-param
// key targets a resource not in known. Without this check a typo
// (e.g. --resource-param chanels:mine=true) is a silent no-op: the param
// never matches a real resource and sync proceeds with missing rows.
func (p *syncUserParams) validateResourceNames(known []string) error {
	if p == nil || len(p.perResource) == 0 {
		return nil
	}
	set := make(map[string]struct{}, len(known))
	for _, r := range known {
		set[r] = struct{}{}
	}
	var unknown []string
	for r := range p.perResource {
		if _, ok := set[r]; !ok {
			unknown = append(unknown, r)
		}
	}
	if len(unknown) == 0 {
		return nil
	}
	sort.Strings(unknown)
	return fmt.Errorf("--resource-param references unknown resource(s): %s (known: %s)",
		strings.Join(unknown, ", "), strings.Join(known, ", "))
}

// accessDenialPatterns matches API error bodies that indicate the request was
// rejected for access-policy reasons rather than for input validity. Matching
// is case-insensitive and uses word boundaries so common substrings inside
// unrelated tokens (e.g. "author", "pagination_token", "insufficient_funds")
// do not produce false positives. The set deliberately excludes brand names —
// vendor-specific phrasings should be addressed at the spec/profiler level,
// not in this universal classifier.
var accessDenialPatterns = []*regexp.Regexp{
	regexp.MustCompile(`\bforbidden\b`),
	regexp.MustCompile(`\bunauthorized\b`),
	regexp.MustCompile(`\bnot[\s_-]?authorized\b`),
	regexp.MustCompile(`\bpermission[\s_-]?denied\b`),
	regexp.MustCompile(`\baccess[\s_-]?denied\b`),
	regexp.MustCompile(`\binsufficient[\s_-]?(scope|permission|privilege)`),
	regexp.MustCompile(`\binvalid[\s_-]?scope\b`),
	regexp.MustCompile(`\bmissing[\s_-]?scope\b`),
	regexp.MustCompile(`\brequires?\s+(elevated|admin|enterprise|business|workspace|enterprise[\s_-]?tier)`),
}

// looksLikeAccessDenial reports whether body text describes an access-policy
// rejection. Use it on response-body content (apiErr.Body), not on the full
// error string — the request path can contain words like "auth" or "tokens"
// that would produce false positives if the whole error message were scanned.
func looksLikeAccessDenial(body string) bool {
	lower := strings.ToLower(body)
	for _, p := range accessDenialPatterns {
		if p.MatchString(lower) {
			return true
		}
	}
	return false
}

// isSyncAccessWarning classifies err as an access-denial warning suitable for
// sync's warn-and-continue path. It returns nil, false for any error that
// should remain a hard sync failure: HTTP 401 (token-level auth failure
// requiring re-auth), 5xx, network errors, and HTTP 400 responses whose
// bodies do not match an access-policy pattern.
//
// Recognized warning shapes:
//   - HTTP 403 (per-resource ACL rejection)
//   - HTTP 400 + access-denial body keyword (insufficient scope, etc.)
//   - GraphQL response carrying only access-denial extension codes
func isSyncAccessWarning(err error) (*accessWarning, bool) {
	if err == nil {
		return nil, false
	}

	var apiErr *client.APIError
	if errors.As(err, &apiErr) {
		switch apiErr.StatusCode {
		case 403:
			return &accessWarning{Status: 403, Reason: "forbidden", Message: apiErr.Body}, true
		case 400:
			if looksLikeAccessDenial(apiErr.Body) {
				return &accessWarning{Status: 400, Reason: "insufficient_access", Message: apiErr.Body}, true
			}
		}
	}

	return nil, false
}

type noopResult struct {
	Status string `json:"status"`
	Reason string `json:"reason"`
}

func writeNoop(flags *rootFlags, reason, prose string) error {
	if flags != nil && flags.asJSON {
		return json.NewEncoder(os.Stdout).Encode(noopResult{Status: "noop", Reason: reason})
	}
	fmt.Fprintln(os.Stderr, prose)
	return nil
}

func writeAPIErrorEnvelope(flags *rootFlags, err error, code int) {
	if flags == nil || !flags.asJSON {
		return
	}
	_ = json.NewEncoder(os.Stdout).Encode(map[string]any{
		"error": err.Error(),
		"code":  code,
	})
}

// classifyAPIError maps API errors to structured exit codes with actionable hints.
func classifyAPIError(err error, flags *rootFlags) error {
	msg := err.Error()
	switch {
	case strings.Contains(msg, "HTTP 409"):
		if flags != nil && flags.idempotent {
			return writeNoop(flags, "already_exists", "already exists (no-op)")
		}
		classified := apiErr(err)
		writeAPIErrorEnvelope(flags, classified, ExitCode(classified))
		return classified
	case strings.Contains(msg, "HTTP 400") && cliutil.LooksLikeAuthError(msg):
		return authErr(fmt.Errorf("%w\nhint: the API rejected the request — this usually means auth is missing or invalid."+
			"\n      Set your API key: export RICH_AUTH_API_KEY=<your-key>"+
			"\n      Run 'printing-press-rich-pp-cli doctor' to check auth status."+
			"\n      Response: "+cliutil.SanitizeErrorBody(msg), err))
	case strings.Contains(msg, "HTTP 401"):
		return authErr(fmt.Errorf("%w\nhint: check your API key."+
			" Set it with: export RICH_AUTH_API_KEY=<your-key>"+
			"\n      Run 'printing-press-rich-pp-cli doctor' to check auth status.", err))
	case strings.Contains(msg, "HTTP 403"):
		return authErr(fmt.Errorf("%w\nhint: permission denied. Your credentials are valid but lack access to this resource."+
			"\n      Check that your API key has the required permissions."+
			"\n      Set it with: export RICH_AUTH_API_KEY=<your-key>"+
			"\n      Run 'printing-press-rich-pp-cli doctor' to check auth status.", err))
	case strings.Contains(msg, "HTTP 404"):
		return notFoundErr(fmt.Errorf("%w\nhint: resource not found. Run the 'list' command to see available items", err))
	case strings.Contains(msg, "HTTP 429"):
		return rateLimitErr(err)
	default:
		return apiErr(err)
	}
}

func truncate(s string, max int) string {
	if len(s) <= max {
		return s
	}
	if max <= 3 {
		return s[:max]
	}
	return s[:max-3] + "..."
}

func newTabWriter(w io.Writer) *tabwriter.Writer {
	return tabwriter.NewWriter(w, 2, 4, 2, ' ', 0)
}

// paginatedGet fetches pages and concatenates array results. The headers
// argument carries per-endpoint required headers (e.g. cal-api-version) that
// must be sent on every page request, including the first; pass nil when the
// endpoint has no per-endpoint header overrides.
func paginatedGet(c interface {
	GetWithHeaders(path string, params map[string]string, headers map[string]string) (json.RawMessage, error)
}, path string, params map[string]string, headers map[string]string, fetchAll bool, cursorParam, nextCursorPath, hasMoreField string) (json.RawMessage, error) {
	// Cursor params are exempt from the "0"/"false" strip: offset-paginated
	// APIs send offset=0 on the first page.
	clean := map[string]string{}
	for k, v := range params {
		if v == "" {
			continue
		}
		if k == cursorParam || (v != "0" && v != "false") {
			clean[k] = v
		}
	}

	if !fetchAll {
		data, err := c.GetWithHeaders(path, clean, headers)
		if err != nil {
			return nil, err
		}
		emitTruncationWarning(data, nextCursorPath, hasMoreField)
		return data, nil
	}

	// Fetch all pages
	allItems := make([]json.RawMessage, 0)
	page := 0
	for {
		page++
		if humanFriendly {
			fmt.Fprintf(os.Stderr, "fetching page %d...\n", page)
		} else {
			fmt.Fprintf(os.Stderr, `{"event":"page_fetch","page":%d}`+"\n", page)
		}

		data, err := c.GetWithHeaders(path, clean, headers)
		if err != nil {
			return nil, err
		}

		// Try to extract items array
		var items []json.RawMessage
		if json.Unmarshal(data, &items) == nil {
			allItems = append(allItems, items...)
		} else {
			// Response is an object - look for array inside
			var obj map[string]json.RawMessage
			if json.Unmarshal(data, &obj) == nil {
				if nested, ok := extractPaginatedItems(obj); ok {
					allItems = append(allItems, nested...)
				}

				// Check for next cursor
				if nextCursorPath != "" {
					if tokenRaw, ok := rawAtPath(obj, nextCursorPath); ok {
						if token := paginationCursorToken(tokenRaw); token != "" {
							clean[cursorParam] = token
							continue
						}
					}
				}

				// Check has_more. A has-more flag without an extracted cursor
				// proves truncation but cannot advance the request safely.
				if hasMoreField != "" {
					if moreRaw, ok := rawAtPath(obj, hasMoreField); ok {
						var more bool
						if json.Unmarshal(moreRaw, &more) == nil && more {
							emitMissingPaginationCursorWarning(nextCursorPath)
							break
						}
					}
				}
			}
			// No more pages
			break
		}

		// For direct arrays, can't paginate without cursor
		break
	}

	if fetchAll && page == 1 && nextCursorPath == "" && hasMoreField == "" {
		emitMissingPaginationSignalWarning()
	}
	if humanFriendly {
		fmt.Fprintf(os.Stderr, "fetched %d items across %d pages\n", len(allItems), page)
	} else {
		fmt.Fprintf(os.Stderr, `{"event":"complete","total":%d,"pages":%d}`+"\n", len(allItems), page)
	}
	result, _ := json.Marshal(allItems)
	return json.RawMessage(result), nil
}

// Silent page-1 truncation is the worst-possible mode for agents,
// who otherwise compute totals against an incomplete set without
// passing --all.
func emitTruncationWarning(data json.RawMessage, nextCursorPath, hasMoreField string) {
	if nextCursorPath == "" && hasMoreField == "" {
		return
	}
	var obj map[string]json.RawMessage
	if err := json.Unmarshal(data, &obj); err != nil {
		return
	}
	var nextCursor string
	if nextCursorPath != "" {
		if tokenRaw, ok := rawAtPath(obj, nextCursorPath); ok {
			nextCursor = paginationCursorToken(tokenRaw)
		}
	}
	var hasMore bool
	if hasMoreField != "" {
		if moreRaw, ok := rawAtPath(obj, hasMoreField); ok {
			_ = json.Unmarshal(moreRaw, &hasMore)
		}
	}
	if nextCursor == "" && !hasMore {
		return
	}
	// --all only advances when a next-cursor is configured. has_more-only
	// endpoints have no cursor to set on the next page, so the --all loop
	// re-fetches the same response forever. Don't advertise an escape
	// hatch that doesn't work for this topology.
	if nextCursor != "" {
		if humanFriendly {
			fmt.Fprintf(os.Stderr, "warning: results truncated; more pages available. Re-run with --all to fetch every page.\n")
		} else {
			fmt.Fprintf(os.Stderr, `{"event":"truncated","hint":"pass --all to fetch every page"}`+"\n")
		}
		return
	}
	if humanFriendly {
		fmt.Fprintf(os.Stderr, "warning: results truncated; more pages available.\n")
	} else {
		fmt.Fprintf(os.Stderr, `{"event":"truncated"}`+"\n")
	}
}

func emitMissingPaginationSignalWarning() {
	if humanFriendly {
		fmt.Fprintf(os.Stderr, "warning: --all requested, but this endpoint does not declare a next cursor or has-more field; returning page 1 only.\n")
	} else {
		fmt.Fprintf(os.Stderr, `{"event":"truncated","reason":"pagination_signal_missing","message":"--all requested but this endpoint does not declare a next cursor or has-more field; returning page 1 only"}`+"\n")
	}
}

func emitMissingPaginationCursorWarning(nextCursorPath string) {
	if humanFriendly {
		fmt.Fprintf(os.Stderr, "warning: --all requested, but the response indicated more pages without a usable next cursor; returning fetched pages only.\n")
	} else if nextCursorPath != "" {
		fmt.Fprintf(os.Stderr, `{"event":"truncated","reason":"pagination_cursor_missing","next_cursor_path":%q,"message":"--all requested but the response indicated more pages without a usable next cursor; returning fetched pages only"}`+"\n", nextCursorPath)
	} else {
		fmt.Fprintf(os.Stderr, `{"event":"truncated","reason":"pagination_cursor_missing","message":"--all requested but the response indicated more pages without a usable next cursor; returning fetched pages only"}`+"\n")
	}
}

func paginationCursorToken(raw json.RawMessage) string {
	var token string
	if json.Unmarshal(raw, &token) == nil && token != "" {
		return token
	}
	var number json.Number
	if json.Unmarshal(raw, &number) == nil {
		if n, err := number.Int64(); err == nil && n > 0 {
			return number.String()
		}
	}
	return ""
}

func extractPaginatedItems(obj map[string]json.RawMessage) ([]json.RawMessage, bool) {
	for _, field := range []string{"data", "items", "results", "messages", "members", "values"} {
		if arr, ok := obj[field]; ok {
			var nested []json.RawMessage
			if json.Unmarshal(arr, &nested) == nil {
				return nested, true
			}
		}
	}

	var onlyArray []json.RawMessage
	arrayCount := 0
	for _, raw := range obj {
		var candidate []json.RawMessage
		if json.Unmarshal(raw, &candidate) == nil {
			onlyArray = candidate
			arrayCount++
		}
	}
	if arrayCount == 1 {
		return onlyArray, true
	}
	return nil, false
}

func rawAtPath(obj map[string]json.RawMessage, path string) (json.RawMessage, bool) {
	if raw, ok := obj[path]; ok {
		return raw, true
	}

	current := obj
	parts := strings.Split(path, ".")
	for i, part := range parts {
		raw, ok := current[part]
		if !ok {
			return nil, false
		}
		if i == len(parts)-1 {
			return raw, true
		}
		if err := json.Unmarshal(raw, &current); err != nil {
			return nil, false
		}
	}
	return nil, false
}

// printJSONFiltered marshals a Go-typed value through the same output
// pipeline endpoint-mirror commands use. Hand-written novel commands that
// build a typed slice/struct call this so --select, --compact, --csv, and
// --quiet all behave the same way as on generator-emitted commands.
func printJSONFiltered(w io.Writer, v any, flags *rootFlags) error {
	raw, err := json.Marshal(v)
	if err != nil {
		return err
	}
	return printOutputWithFlags(w, json.RawMessage(raw), flags)
}

// filterFields keeps only the specified fields (comma-separated) from JSON objects/arrays.
// Supports dotted paths like "events.shortName" to descend into nested structures.
// Arrays are traversed element-wise: "events.shortName" keeps shortName on each event.
func filterFields(data json.RawMessage, fields string) json.RawMessage {
	var paths [][]string
	for _, f := range strings.Split(fields, ",") {
		f = strings.TrimSpace(f)
		if f == "" {
			continue
		}
		parts := strings.Split(f, ".")
		for i := range parts {
			parts[i] = strings.ToLower(parts[i])
		}
		paths = append(paths, parts)
	}
	if len(paths) == 0 {
		return data
	}
	return filterFieldsRec(data, paths)
}

// filterFieldsRec applies path filters to a JSON value. Each path is a list of
// lowercase segments; arrays descend element-wise.
func filterFieldsRec(data json.RawMessage, paths [][]string) json.RawMessage {
	var arr []json.RawMessage
	if err := json.Unmarshal(data, &arr); err == nil {
		out := make([]json.RawMessage, len(arr))
		for i, el := range arr {
			out[i] = filterFieldsRec(el, paths)
		}
		result, _ := json.Marshal(out)
		return result
	}

	var obj map[string]json.RawMessage
	if err := json.Unmarshal(data, &obj); err == nil {
		keepWhole := map[string]bool{}
		subPaths := map[string][][]string{}
		for _, p := range paths {
			if len(p) == 0 {
				continue
			}
			head := p[0]
			if len(p) == 1 {
				keepWhole[head] = true
			} else {
				subPaths[head] = append(subPaths[head], p[1:])
			}
		}
		filtered := map[string]json.RawMessage{}
		matchedAny := false
		for k, v := range obj {
			matched := matchSelectSegment(k, keepWhole, subPaths)
			if matched == "" {
				continue
			}
			matchedAny = true
			if keepWhole[matched] {
				filtered[k] = v
				continue
			}
			if subs := subPaths[matched]; subs != nil {
				filtered[k] = filterFieldsRec(v, subs)
			}
		}
		// Envelope fallback: when no top-level keys matched but at least one
		// sibling is a non-null array, treat the object as a list envelope
		// (`{"items":[...]}`, `{"data":[...]}`, `{"total_count":N,"items":[...]}`)
		// and apply the selector inside the array(s). Non-array siblings pass
		// through verbatim so envelope metadata (counts, null pagination
		// cursors) stays visible. The foundArray guard preserves the prior
		// empty-object result for flat objects where no key matches and no
		// array exists. The `arr != nil` check rejects JSON null, which
		// json.Unmarshal otherwise accepts into a []json.RawMessage as a
		// nil slice and would coerce to `[]`.
		if !matchedAny {
			pending := map[string]json.RawMessage{}
			foundArray := false
			for k, v := range obj {
				var arr []json.RawMessage
				if json.Unmarshal(v, &arr) == nil && arr != nil {
					foundArray = true
					pending[k] = filterFieldsRec(v, paths)
				} else {
					pending[k] = v
				}
			}
			if foundArray {
				for k, v := range pending {
					filtered[k] = v
				}
			}
		}
		result, _ := json.Marshal(filtered)
		return result
	}

	return data
}

// matchSelectSegment returns the matching lowercase segment, or "" if no match.
// Supports direct case-insensitive match and camelCase→kebab-case conversion.
func matchSelectSegment(fieldName string, keepWhole map[string]bool, subPaths map[string][][]string) string {
	lower := strings.ToLower(fieldName)
	if keepWhole[lower] || subPaths[lower] != nil {
		return lower
	}
	kebab := camelToKebab(fieldName)
	if kebab != lower && (keepWhole[kebab] || subPaths[kebab] != nil) {
		return kebab
	}
	return ""
}

// camelToKebab converts "orderDate" or "orderdate" to "order-date" by splitting on
// uppercase boundaries. For already-lowercase input, splits on known word boundaries.
func camelToKebab(s string) string {
	var b strings.Builder
	runes := []rune(s)
	for i, r := range runes {
		if i > 0 && unicode.IsUpper(r) && unicode.IsLower(runes[i-1]) {
			b.WriteByte('-')
		}
		b.WriteRune(unicode.ToLower(r))
	}
	return b.String()
}

// printOutputWithFlags routes output through the right format based on flags.
func printOutputWithFlags(w io.Writer, data json.RawMessage, flags *rootFlags) error {
	// --select wins over --compact when both are set: an explicit field list
	// is the user's authoritative request, so the high-gravity allow-list
	// must not strip those fields out before --select can pick them. When
	// only --compact is set (e.g., --agent without --select), the allow-list
	// still runs.
	if flags.selectFields != "" {
		data = filterFields(data, flags.selectFields)
	} else if flags.compact {
		data = compactFields(data)
	}
	// --quiet: suppress all output, exit code communicates result
	if flags.quiet {
		return nil
	}
	// --csv: render as CSV
	if flags.csv {
		return printCSV(w, data)
	}
	return printOutput(w, data, flags.asJSON)
}

// extractResponseData unwraps common API response envelopes for display.
// Many APIs return {"status":"success","data":[...]} instead of a bare array.
// This extracts the inner data for output helpers (filterFields, compactFields,
// printAutoTable) that expect arrays or flat objects.
//
// Only unwraps when a "status" field is present and indicates success — this
// avoids false positives on APIs where "data" is a regular field (e.g., Stripe
// returns {"data":[...],"has_more":true} where "data" is the list, not an
// envelope wrapper).
func extractResponseData(data json.RawMessage) json.RawMessage {
	var envelope struct {
		Status string          `json:"status"`
		Data   json.RawMessage `json:"data"`
	}
	if err := json.Unmarshal(data, &envelope); err != nil {
		return data
	}
	if envelope.Data == nil || envelope.Status == "" {
		return data // No status field = not an envelope, might be regular "data" field
	}
	switch envelope.Status {
	case "success", "ok", "OK", "Success":
		return envelope.Data
	default:
		return data
	}
}

// compactVerboseListFields are prose-shaped fields stripped from list-item
// projections. On lists, "body"/"content"/"html"/"markdown" are verbose
// noise and the row's identity is carried by id/name/title/etc.
var compactVerboseListFields = map[string]bool{
	"description": true, "body": true, "content": true,
	"comments": true, "attachments": true, "html": true, "markdown": true,
}

// compactVerboseObjectFields are metadata fields stripped from single-object
// responses. "body"/"content"/"html"/"markdown" are intentionally absent:
// for a `get` command those fields are the primary payload, and stripping
// them under `--agent`/`--compact` silently emits a useless envelope.
// Use `--select` to drop them explicitly.
var compactVerboseObjectFields = map[string]bool{
	"description": true,
	"comments":    true,
	"attachments": true,
}

// compactFields keeps only the most important fields for agent consumption.
// For arrays: allowlist of high-gravity fields (no descriptions).
// For single objects: blocklist that strips known-verbose fields (descriptions, comments, etc.).
func compactFields(data json.RawMessage) json.RawMessage {
	// Try array first
	var items []map[string]any
	if err := json.Unmarshal(data, &items); err == nil {
		return compactListFields(items)
	}

	// Single object — use blocklist
	var obj map[string]any
	if err := json.Unmarshal(data, &obj); err == nil {
		return compactObjectFields(obj)
	}

	return data
}

// compactListFields keeps only high-gravity fields for array responses.
//
// Two-layer keep rule:
//
//  1. A static allow-list covers canonical scalars (id/name/price/status/...).
//  2. A data-driven extension also keeps any scalar key present in at least
//     80% of input rows. This catches hand-written novel commands whose
//     output keys (object_name, match_key, snippet) aren't on the canonical
//     allow-list, without forcing every printed CLI to expand the list.
//
// Verbose fields (description, body, content, etc.) are excluded from the
// data-driven extension regardless of frequency, so the compact intent
// (short identifying values for agent consumption, not full prose) is
// preserved.
//
// When an item still carries none of the keep keys, the original is
// preserved so `--agent` does not silently emit {} for shapes whose key
// names are entirely off-canonical.
func compactListFields(items []map[string]any) json.RawMessage {
	keepFields := map[string]bool{
		// Identity
		"id": true, "name": true, "title": true, "identifier": true,
		"code": true, "slug": true, "key": true,
		// Categorization
		"status": true, "state": true, "type": true, "kind": true, "priority": true,
		// Communication
		"url": true, "email": true,
		// Monetary
		"price": true, "amount": true, "cost": true, "fare": true,
		"rate": true, "currency": true,
		// Metrics
		"rating": true, "score": true, "count": true,
		// Locale / geo
		"language": true, "locale": true, "country": true, "region": true,
		"city": true, "domain": true,
		// Temporal
		"created_at": true, "updated_at": true, "createdAt": true, "updatedAt": true,
		"date": true,
		// Versioning
		"version": true,
	}
	if len(items) > 0 {
		keyCounts := map[string]int{}
		for _, item := range items {
			for k, v := range item {
				if compactVerboseListFields[k] || !isCompactScalar(v) {
					continue
				}
				keyCounts[k]++
			}
		}
		// ceil(len(items) * 0.8) without importing math. Capped at len-1 for
		// len >= 2 so a single missing row cannot veto a key on small lists
		// (without the cap, ceil(0.8*n) == n for n in {2,3,4}, which silently
		// reintroduces the partial-strip bug whenever a heterogeneous 2-4 row
		// response mixes one allow-list key with novel keys).
		threshold := (len(items)*4 + 4) / 5
		if len(items) >= 2 && threshold > len(items)-1 {
			threshold = len(items) - 1
		}
		for k, count := range keyCounts {
			if count >= threshold {
				keepFields[k] = true
			}
		}
	}

	filtered := make([]map[string]any, 0, len(items))
	for _, item := range items {
		compact := map[string]any{}
		for k, v := range item {
			if keepFields[k] {
				compact[k] = v
			}
		}
		if len(compact) == 0 {
			compact = item
		}
		filtered = append(filtered, compact)
	}
	result, _ := json.Marshal(filtered)
	return result
}

// isCompactScalar reports whether v is a small primitive (string, number,
// bool, null) suitable for --compact projection. Objects and arrays are
// rejected: they almost always represent nested structure that defeats the
// "short identifying value" intent of --compact, and including them in the
// data-driven keep set would bloat agent output.
func isCompactScalar(v any) bool {
	switch v.(type) {
	case nil, bool, float64, string:
		return true
	default:
		return false
	}
}

// compactObjectFields strips known-verbose metadata fields from single-object
// responses. The blocklist deliberately excludes "body"/"content"/"html"/
// "markdown" — those fields are payload on `get` commands and stripping them
// under `--agent`/`--compact` is a silent loss; agents who want to omit them
// can pass `--select` to specify only the fields they need.
func compactObjectFields(obj map[string]any) json.RawMessage {
	compact := map[string]any{}
	for k, v := range obj {
		if !compactVerboseObjectFields[k] {
			compact[k] = v
		}
	}
	result, _ := json.Marshal(compact)
	return result
}

// printCSV renders JSON arrays as CSV with header row.
func printCSV(w io.Writer, data json.RawMessage) error {
	var items []map[string]any
	if err := json.Unmarshal(data, &items); err != nil || len(items) == 0 {
		// Single object or empty - just print as JSON
		fmt.Fprintln(w, string(data))
		return nil
	}
	// Collect all keys for header
	keySet := map[string]bool{}
	for _, item := range items {
		for k := range item {
			keySet[k] = true
		}
	}
	var keys []string
	for k := range keySet {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	// Header
	fmt.Fprintln(w, strings.Join(keys, ","))
	// Rows
	for _, item := range items {
		var vals []string
		for _, k := range keys {
			v := item[k]
			if v == nil {
				vals = append(vals, "")
			} else {
				s := fmt.Sprintf("%v", v)
				if strings.ContainsAny(s, ",\"\n") {
					s = `"` + strings.ReplaceAll(s, `"`, `""`) + `"`
				}
				vals = append(vals, s)
			}
		}
		fmt.Fprintln(w, strings.Join(vals, ","))
	}
	return nil
}

// printOutput auto-detects arrays and renders as tables, or prints raw JSON for objects.
func printOutput(w io.Writer, data json.RawMessage, asJSON bool) error {
	if !asJSON && !isTerminal(w) {
		asJSON = true
	}

	if asJSON {
		enc := json.NewEncoder(w)
		enc.SetIndent("", "  ")
		return enc.Encode(data)
	}

	// Try to detect if response is an array
	var items []map[string]any
	if err := json.Unmarshal(data, &items); err == nil && len(items) > 0 {
		if err := printAutoTable(w, items); err != nil {
			return err
		}
		// Agent-friendly: show count and suggest narrowing when results are large
		if len(items) >= 25 {
			fmt.Fprintf(os.Stderr, "\nShowing %d results. To narrow: add --limit, --json --select, or filter flags.\n", len(items))
		}
		return nil
	}

	// Single object - pretty print
	var obj map[string]any
	if err := json.Unmarshal(data, &obj); err == nil {
		enc := json.NewEncoder(w)
		enc.SetIndent("", "  ")
		return enc.Encode(obj)
	}

	// Fallback: print raw
	fmt.Fprintln(w, string(data))
	return nil
}

// levenshteinDistance computes the edit distance between two strings using a two-row DP approach.
func levenshteinDistance(a, b string) int {
	if len(a) == 0 {
		return len(b)
	}
	if len(b) == 0 {
		return len(a)
	}
	if len(a) < len(b) {
		a, b = b, a
	}
	prev := make([]int, len(b)+1)
	curr := make([]int, len(b)+1)
	for j := range prev {
		prev[j] = j
	}
	for i := 1; i <= len(a); i++ {
		curr[0] = i
		for j := 1; j <= len(b); j++ {
			cost := 1
			if a[i-1] == b[j-1] {
				cost = 0
			}
			ins := curr[j-1] + 1
			del := prev[j] + 1
			sub := prev[j-1] + cost
			min := ins
			if del < min {
				min = del
			}
			if sub < min {
				min = sub
			}
			curr[j] = min
		}
		prev, curr = curr, prev
	}
	return prev[len(b)]
}

// suggestFlag returns the closest known flag name to the unknown string, or "" if none is close enough.
func suggestFlag(unknown string, cmd *cobra.Command) string {
	unknown = strings.TrimLeft(unknown, "-")
	best := ""
	bestDist := 4 // only consider distance <= 3
	check := func(name string) {
		d := levenshteinDistance(unknown, name)
		if d < bestDist && d*5 <= len(unknown)*2 {
			bestDist = d
			best = name
		}
	}
	cmd.Flags().VisitAll(func(f *pflag.Flag) {
		check(f.Name)
	})
	cmd.InheritedFlags().VisitAll(func(f *pflag.Flag) {
		check(f.Name)
	})
	return best
}

// wantsHumanTable returns true when output should be a human-friendly table.
// Smart default: terminal=table, pipe=JSON.
// - Human in terminal: isTerminal()=true → table
// - Claude Code/Codex bash tool: stdout piped → JSON
// - --json/--csv/--compact/--agent: machine format → JSON
func wantsHumanTable(w io.Writer, flags *rootFlags) bool {
	if flags.asJSON || flags.csv || flags.compact || flags.quiet || flags.plain {
		return false
	}
	if flags.selectFields != "" {
		return false
	}
	return isTerminal(w)
}

func printAutoTable(w io.Writer, items []map[string]any) error {
	if len(items) == 0 {
		return nil
	}

	// Count scalar vs complex fields to decide format
	scalarCount := 0
	for _, v := range items[0] {
		if isCompactScalar(v) {
			scalarCount++
		}
	}

	// Use sectional/card layout for complex items (many fields or nested data)
	if len(items[0]) > 8 || scalarCount < len(items[0])-2 {
		return printAutoCards(w, items)
	}

	headers := prioritizeHeaders(items[0])

	// Limit to 6 columns max for readability
	if len(headers) > 6 {
		headers = headers[:6]
	}

	// Build rows
	rows := make([][]string, 0, len(items))
	for _, item := range items {
		row := make([]string, len(headers))
		for i, h := range headers {
			row[i] = formatCellValue(item[h])
		}
		rows = append(rows, row)
	}

	// Print with tab alignment using tabwriter
	tw := newTabWriter(w)
	upperHeaders := make([]string, len(headers))
	for i, h := range headers {
		upperHeaders[i] = bold(strings.ToUpper(h))
	}

	fmt.Fprintln(tw, strings.Join(upperHeaders, "\t"))
	for _, row := range rows {
		fmt.Fprintln(tw, strings.Join(row, "\t"))
	}
	return tw.Flush()
}

// prioritizeHeaders orders scalar fields by importance for table display.
func prioritizeHeaders(item map[string]any) []string {
	return prioritizeFields(item, false)
}

// prioritizeAllHeaders orders all fields (including arrays) by importance for card display.
func prioritizeAllHeaders(item map[string]any) []string {
	return prioritizeFields(item, true)
}

// prioritizeFields orders fields by importance: identity → temporal → status → other.
// When includeComplex is true, arrays and objects are included (for card layout).
//
// Uses exact-or-suffix matching to avoid false positives: "name" matches "Name" and
// "UserName" but not "BuildingName" (because "Building" is not a known prefix that
// indicates identity). The field is split on camelCase/snake_case boundaries and the
// LAST segment is matched against patterns.
func prioritizeFields(item map[string]any, includeComplex bool) []string {
	// Priority tiers — matched against the last segment of the field name.
	// "OrderDate" → last segment "date" → tier 1 (temporal).
	// "BuildingName" → last segment "name" → tier 0... but we want to avoid this.
	// Solution: exact match on the full lowered name OR suffix segment match,
	// with a penalty for compound names that have a non-identity prefix.
	type pattern struct {
		word string
		tier int
	}
	// Exact matches (full field name, case-insensitive) — highest confidence
	exactMatches := map[string]int{
		"id": 0, "name": 0, "title": 0, "slug": 0, "key": 0,
		"date": 1, "created": 1, "updated": 1, "createdat": 1, "updatedat": 1,
		"status": 2, "state": 2, "statuscode": 2,
		"summary": 3, "description": 3, "price": 3, "amount": 3, "total": 3,
		"cost": 3, "points": 3, "score": 3,
		"type": 4, "kind": 4, "category": 4, "email": 4, "phone": 4, "url": 4,
	}
	// Suffix patterns — match when the field ends with this word (after splitting)
	suffixMatches := map[string]int{
		"id": 0, "name": 0, "title": 0,
		"date": 1, "time": 1,
		"status": 2, "state": 2, "code": 2,
		"price": 3, "amount": 3, "total": 3, "cost": 3,
		"summary": 3, "description": 3, "points": 3, "score": 3,
		"type": 4, "kind": 4, "category": 4, "method": 4,
	}

	numTiers := 5

	type scored struct {
		name  string
		tier  int
		index int
	}

	var all []scored
	idx := 0
	for k, v := range item {
		if !includeComplex {
			switch v.(type) {
			case []any, map[string]any:
				continue
			}
		}
		// Skip values that won't render usefully in cards
		if includeComplex {
			formatted := formatCellValue(v)
			if formatted == "" {
				continue
			}
		}

		tier := numTiers // default: unclassified
		lower := strings.ToLower(k)

		// 1. Exact match on full field name
		if t, ok := exactMatches[lower]; ok {
			tier = t
		} else {
			// 2. Split camelCase into segments and match the last one
			segments := splitCamelCase(lower)
			if len(segments) > 0 {
				lastSeg := segments[len(segments)-1]
				if t, ok := suffixMatches[lastSeg]; ok {
					// Compound names with identity suffixes (BuildingName, TipTime)
					// get demoted one tier because the prefix dilutes the signal
					if len(segments) > 1 {
						tier = t + 1
					} else {
						tier = t
					}
				}
			}
		}

		// Demote booleans to last
		if _, ok := v.(bool); ok && tier >= numTiers {
			tier = numTiers + 1
		}
		all = append(all, scored{name: k, tier: tier, index: idx})
		idx++
	}

	sort.Slice(all, func(i, j int) bool {
		if all[i].tier != all[j].tier {
			return all[i].tier < all[j].tier
		}
		return all[i].index < all[j].index
	})

	headers := make([]string, len(all))
	for i, s := range all {
		headers[i] = s.name
	}
	return headers
}

// splitCamelCase splits "OrderDate" → ["order", "date"], "statusCode" → ["status", "code"],
// "page_size" → ["page", "size"].
func splitCamelCase(s string) []string {
	var segments []string
	var current strings.Builder
	runes := []rune(s)
	for i, r := range runes {
		if r == '_' || r == '-' {
			if current.Len() > 0 {
				segments = append(segments, current.String())
				current.Reset()
			}
			continue
		}
		if i > 0 && unicode.IsUpper(r) && unicode.IsLower(runes[i-1]) {
			if current.Len() > 0 {
				segments = append(segments, current.String())
				current.Reset()
			}
		}
		current.WriteRune(unicode.ToLower(r))
	}
	if current.Len() > 0 {
		segments = append(segments, current.String())
	}
	return segments
}

// printAutoCards renders items as labeled cards — one block per item.
// Used for complex responses with many fields or nested data.
func printAutoCards(w io.Writer, items []map[string]any) error {
	headers := prioritizeAllHeaders(items[0])

	// Find the longest header for alignment (from fields we'll actually show)
	maxLen := 0
	for _, h := range headers {
		if len(h) > maxLen {
			maxLen = len(h)
		}
	}

	for i, item := range items {
		if i > 0 {
			fmt.Fprintln(w)
		}

		// Card header: use first priority field as the card title
		titleVal := formatCellValue(item[headers[0]])
		if len(headers) > 1 {
			secondVal := formatCellValue(item[headers[1]])
			if secondVal != "" {
				fmt.Fprintf(w, "%s %s — %s\n", bold(strings.ToUpper(headers[0])), titleVal, secondVal)
			} else {
				fmt.Fprintf(w, "%s %s\n", bold(strings.ToUpper(headers[0])), titleVal)
			}
		} else {
			fmt.Fprintf(w, "%s %s\n", bold(strings.ToUpper(headers[0])), titleVal)
		}

		// Remaining fields indented — skip empty, zero, and false values
		for _, h := range headers[2:] {
			v := formatCellValue(item[h])
			if v == "" || v == "false" || v == "0" || v == "[]" || v == "null" {
				continue
			}
			// Multi-line values (nested arrays) start with \n
			if strings.HasPrefix(v, "\n") {
				fmt.Fprintf(w, "  %s:%s\n", h, v)
			} else {
				fmt.Fprintf(w, "  %-*s  %s\n", maxLen, h+":", v)
			}
		}
	}
	return nil
}

func formatCellValue(v any) string {
	switch val := v.(type) {
	case string:
		// Format ISO dates as just the date portion
		if len(val) >= 19 && val[4] == '-' && val[7] == '-' && val[10] == 'T' {
			return val[:10]
		}
		return truncate(val, 60)
	case float64:
		if val == float64(int64(val)) {
			return fmt.Sprintf("%d", int64(val))
		}
		return fmt.Sprintf("%.2f", val)
	case bool:
		return fmt.Sprintf("%t", val)
	case nil:
		return ""
	case []any:
		if len(val) == 0 {
			return ""
		}
		// If array contains objects, format each as a summary line
		if obj, isObj := val[0].(map[string]any); isObj {
			_ = obj
			return formatObjectArray(val)
		}
		// Flatten simple arrays into comma-separated string
		parts := make([]string, 0, len(val))
		for _, item := range val {
			if s, ok := item.(string); ok {
				parts = append(parts, s)
			} else {
				b, _ := json.Marshal(item)
				parts = append(parts, string(b))
			}
		}
		return truncate(strings.Join(parts, ", "), 60)
	case map[string]any:
		return formatSingleObject(val)
	default:
		b, _ := json.Marshal(val)
		return truncate(string(b), 60)
	}
}

// formatObjectArray renders an array of objects as multi-line summary.
// Each object is summarized by its most descriptive fields: name/title, qty, size, price.
func formatObjectArray(items []any) string {
	var lines []string
	for _, raw := range items {
		obj, ok := raw.(map[string]any)
		if !ok {
			continue
		}
		lines = append(lines, formatObjectSummary(obj))
	}
	if len(lines) == 0 {
		return ""
	}
	// Multi-line: newline-prefixed so the card renderer can indent
	return "\n" + strings.Join(lines, "\n")
}

// formatObjectSummary extracts the most useful fields from an object into a one-line summary.
// Looks for: qty/count → name/title → size → price, in that order.
func formatObjectSummary(obj map[string]any) string {
	var parts []string

	// Quantity
	qty := findField(obj, "qty", "count", "quantity")
	if qty != "" && qty != "1" && qty != "0" {
		parts = append(parts, qty+"x")
	} else if qty == "1" {
		parts = append(parts, "1x")
	}

	// Name — check nested objects too (e.g., Side1.Name)
	name := findField(obj, "name", "title", "label", "description")
	if name == "" {
		// Check nested objects for name
		for _, key := range []string{"Side1", "side1", "Item", "item", "Product", "product"} {
			if nested, ok := obj[key].(map[string]any); ok {
				name = findField(nested, "name", "title", "label")
				if name != "" {
					break
				}
			}
		}
	}
	if name != "" {
		parts = append(parts, name)
	}

	// Size
	size := findField(obj, "sizename", "size_name")
	if size == "" {
		size = findField(obj, "catname", "cat_name", "category")
	}
	if size != "" {
		parts = append(parts, "—")
		parts = append(parts, size)
	}

	// Price
	price := findField(obj, "extprice", "price", "amount", "total")
	if price != "" && price != "0" {
		parts = append(parts, fmt.Sprintf("($%s)", price))
	}

	if len(parts) == 0 {
		// Fallback: JSON summary
		b, _ := json.Marshal(obj)
		return truncate(string(b), 80)
	}
	return "    " + strings.Join(parts, " ")
}

// formatSingleObject renders a single object by its most descriptive fields.
func formatSingleObject(obj map[string]any) string {
	name := findField(obj, "name", "title", "label", "description")
	if name != "" {
		return name
	}
	id := findField(obj, "id", "key", "code")
	if id != "" {
		return id
	}
	return ""
}

// findField searches an object for a field name (case-insensitive) and returns its formatted value.
func findField(obj map[string]any, names ...string) string {
	for _, name := range names {
		for k, v := range obj {
			if strings.EqualFold(k, name) {
				return formatCellValue(v)
			}
		}
	}
	return ""
}

// DataProvenance describes where data came from and when it was last synced.
type DataProvenance struct {
	Source       string     `json:"source"`                  // "live" or "local"
	SyncedAt     *time.Time `json:"synced_at,omitempty"`     // when local data was last synced
	Reason       string     `json:"reason,omitempty"`        // why local was used: "user_requested", "api_unreachable", "no_search_endpoint"
	ResourceType string     `json:"resource_type,omitempty"` // which resource type was queried
	Freshness    any        `json:"freshness,omitempty"`     // optional machine-owned freshness metadata for covered command paths
}

// printProvenance writes a one-line provenance message to stderr for TTY users.
// Suppressed when stdout is piped or redirected — the JSON response envelope
// already carries meta.source, so the stderr line is redundant and becomes
// noise in agent flows that merge stderr into stdout.
func printProvenance(cmd *cobra.Command, count int, prov DataProvenance) {
	if !isTerminal(cmd.OutOrStdout()) {
		return
	}
	if prov.Source == "live" {
		fmt.Fprintf(cmd.ErrOrStderr(), "%d results (live)\n", count)
		return
	}
	age := "unknown"
	if prov.SyncedAt != nil {
		d := time.Since(*prov.SyncedAt)
		switch {
		case d < time.Minute:
			age = "just now"
		case d < time.Hour:
			age = fmt.Sprintf("%d minutes ago", int(d.Minutes()))
		case d < 24*time.Hour:
			age = fmt.Sprintf("%d hours ago", int(d.Hours()))
		default:
			age = fmt.Sprintf("%d days ago", int(d.Hours()/24))
		}
	}
	prefix := ""
	if prov.Reason == "api_unreachable" {
		prefix = "API unreachable. "
	}
	fmt.Fprintf(cmd.ErrOrStderr(), "%s%d results (cached, synced %s)\n", prefix, count, age)
}

// unwrapSingleKeyArray flattens single-key collection envelopes
// ({"results":[...]}, {"data":[...]}, etc.) so the agent envelope
// emits a stable .results[] across APIs. Multi-key objects pass
// through so cursor/pagination fields stay accessible; non-array
// values pass through so non-collection responses aren't reshaped.
//
// The wrapper-key set is intentionally narrower than
// extractPaginatedItems (which also walks domain-specific keys like
// "messages", "members", "values" used by social/messaging APIs).
// This helper only flattens canonical collection envelopes for
// --json output; the pagination walker has a broader remit.
func unwrapSingleKeyArray(data json.RawMessage) json.RawMessage {
	leading := bytes.TrimLeft(data, " \t\r\n")
	if len(leading) == 0 || leading[0] != '{' {
		return data
	}
	var obj map[string]json.RawMessage
	if err := json.Unmarshal(data, &obj); err != nil {
		return data
	}
	if len(obj) != 1 {
		return data
	}
	for key, val := range obj {
		if key != "results" && key != "data" && key != "items" && key != "nodes" && key != "entries" && key != "records" {
			return data
		}
		trimmed := bytes.TrimLeft(val, " \t\r\n")
		if len(trimmed) == 0 || trimmed[0] != '[' {
			return data
		}
		return val
	}
	return data
}

// wrapWithProvenance wraps response data in a provenance envelope:
// {"results": ..., "meta": {...}}. When data is valid JSON, it embeds as
// the parsed shape; when data is non-JSON (e.g., XML/RSS responses, plain
// text), it embeds as a JSON string so json.Marshal doesn't choke on
// "invalid character '<'" while still passing the raw payload through to
// the consumer. Single-key array envelopes from the API (e.g.
// {"results": [...]}, {"data": [...]}) are unwrapped first so the output
// shape is the same regardless of the API's wrapper key.
func wrapWithProvenance(data json.RawMessage, prov DataProvenance) (json.RawMessage, error) {
	meta := map[string]any{"source": prov.Source}
	if prov.SyncedAt != nil {
		meta["synced_at"] = prov.SyncedAt.UTC().Format(time.RFC3339)
	}
	if prov.Reason != "" {
		meta["reason"] = prov.Reason
	}
	if prov.ResourceType != "" {
		meta["resource_type"] = prov.ResourceType
	}
	if prov.Freshness != nil {
		meta["freshness"] = prov.Freshness
	}
	var results any
	if json.Valid(data) {
		results = json.RawMessage(unwrapSingleKeyArray(data))
	} else {
		results = string(data)
	}
	envelope := map[string]any{
		"results": results,
		"meta":    meta,
	}
	return json.Marshal(envelope)
}

// defaultDBPath returns the canonical path for the local SQLite database.
func defaultDBPath(name string) string {
	home, _ := os.UserHomeDir()
	return filepath.Join(home, ".local", "share", name, "data.db")
}
