// 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 (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"strings"
	"time"

	"printing-press-golden-pp-cli/internal/cliutil"
	"printing-press-golden-pp-cli/internal/store"
)

// readCommandResources maps command paths (`cmd.CommandPath()`) to the
// resource types those commands read. The auto-refresh hook consults this
// map to decide whether to refresh the local cache before serving.
// Populated from generated syncable resource commands and any custom
// command-path coverage declared in spec.Cache.Commands.
var readCommandResources = map[string][]string{
	"printing-press-golden-pp-cli currencies":        {"currencies"},
	"printing-press-golden-pp-cli currencies list":   {"currencies"},
	"printing-press-golden-pp-cli currencies get":    {"currencies"},
	"printing-press-golden-pp-cli currencies search": {"currencies"},
	"printing-press-golden-pp-cli projects":          {"projects"},
	"printing-press-golden-pp-cli projects list":     {"projects"},
	"printing-press-golden-pp-cli projects get":      {"projects"},
	"printing-press-golden-pp-cli projects search":   {"projects"},
}

// cachePolicy returns the cache freshness policy assembled from spec
// configuration. Defaults: 6h global stale-after, env opt-out named after
// the CLI. Per-resource overrides from spec.Cache.Resources take priority.
func cachePolicy() cliutil.Policy {
	staleAfter := 6 * time.Hour
	perResource := map[string]time.Duration{}
	// Default env opt-out name is the CLI name normalized with the same
	// ASCII-safe convention used in generated docs and config env vars.
	envOptOut := "PRINTING_PRESS_GOLDEN_NO_AUTO_REFRESH"
	return cliutil.Policy{
		StaleAfter:   staleAfter,
		PerResource:  perResource,
		EnvOptOut:    envOptOut,
		ShareEnabled: false,
	}
}

// refreshTimeout returns the wall-clock budget for a single auto-refresh
// call. Beyond this, the command serves stale data with a stderr warning
// instead of blocking the agent or user indefinitely.
func refreshTimeout() time.Duration {
	if d, err := time.ParseDuration("30s"); err == nil {
		return d
	}
	return 30 * time.Second
}

// autoRefreshIfStale decides whether to refresh and runs the refresh in
// one call. Refresh failures become stderr warnings and the command proceeds
// with the stale cache. The returned metadata is attached to generated JSON
// provenance envelopes under meta.freshness.
func autoRefreshIfStale(ctx context.Context, flags *rootFlags, resources []string) (meta cliutil.FreshnessMeta) {
	started := time.Now()
	meta = cliutil.FreshnessMeta{
		Decision:  "skipped",
		Resources: append([]string(nil), resources...),
		Source:    flags.dataSource,
	}
	defer func() {
		meta.ElapsedMS = time.Since(started).Milliseconds()
	}()
	if flags.dataSource != "auto" {
		meta.Reason = "data_source_" + flags.dataSource
		return meta
	}
	if len(resources) == 0 {
		meta.Reason = "no_resources"
		return meta
	}
	policy := cachePolicy()
	if policy.EnvOptOut != "" && os.Getenv(policy.EnvOptOut) == "1" {
		meta.Decision = "skipped"
		meta.Reason = "env_opt_out"
		return meta
	}
	dbPath := defaultDBPath("printing-press-golden-pp-cli")
	db, err := store.OpenWithContext(ctx, dbPath)
	if err != nil {
		fmt.Fprintf(os.Stderr, "warning: auto-refresh skipped (open: %v)\n", err)
		meta.Decision = "error"
		meta.Reason = "open_store"
		meta.Error = err.Error()
		return meta
	}
	defer db.Close()

	decision, err := cliutil.EnsureFresh(ctx, db.DB(), resources, policy)
	meta.Decision = decision.String()
	if err != nil {
		fmt.Fprintf(os.Stderr, "warning: auto-refresh decision failed: %v\n", err)
		meta.Decision = "error"
		meta.Reason = "decision_failed"
		meta.Error = err.Error()
		return meta
	}
	if decision == cliutil.DecisionFresh || decision == cliutil.DecisionNoStore {
		meta.Reason = decision.String()
		return meta
	}

	refreshCtx, cancel := context.WithTimeout(ctx, refreshTimeout())
	defer cancel()
	meta.Ran = true
	if err := runAutoRefresh(refreshCtx, flags, db, resources); err != nil {
		fmt.Fprintf(os.Stderr, "warning: using stale printing-press-golden-pp-cli cache (refresh failed: %v)\n", err)
		emitCacheRefreshFailedEvent(resources, err)
		meta.Reason = "refresh_failed"
		meta.Error = err.Error()
		return meta
	}
	meta.Reason = "refreshed"
	return meta
}

// emitCacheRefreshFailedEvent writes a structured one-line JSON event to
// stderr alongside the prose warning so agents have a parseable signal
// that subsequent results are being served from a stale cache. Novel
// commands that bypass wrapWithProvenance (so meta.freshness never
// reaches stdout) would otherwise leave agents with only the prose
// warning. Mirrors the sync_warning shape from sync.go so the same
// event vocabulary covers both refresh-time and pre-run paths.
func emitCacheRefreshFailedEvent(resources []string, err error) {
	payload := struct {
		Event     string   `json:"event"`
		Reason    string   `json:"reason"`
		Resources []string `json:"resources,omitempty"`
		Error     string   `json:"error"`
	}{
		Event:     "cache_warning",
		Reason:    "refresh_failed",
		Resources: resources,
		Error:     err.Error(),
	}
	if out, mErr := json.Marshal(payload); mErr == nil {
		fmt.Fprintln(os.Stderr, string(out))
	}
}

// ensureFreshForResources lets hand-authored commands participate in the same
// freshness hook as generated resource commands. Custom commands should call
// this before reading from the store, then use wrapWithProvenance for JSON
// output if they want meta.freshness.
func ensureFreshForResources(ctx context.Context, flags *rootFlags, resources ...string) cliutil.FreshnessMeta {
	meta := autoRefreshIfStale(ctx, flags, resources)
	flags.freshnessMeta = meta
	return meta
}

// ensureFreshForCommand looks up a registered command path in
// readCommandResources and applies the same freshness hook used by root
// pre-run. commandPath must match cobra.CommandPath(), including the binary
// name. It returns skipped metadata for unregistered commands.
func ensureFreshForCommand(ctx context.Context, flags *rootFlags, commandPath string) cliutil.FreshnessMeta {
	resources, ok := readCommandResources[commandPath]
	if !ok {
		meta := cliutil.FreshnessMeta{
			Decision: "skipped",
			Reason:   "unregistered_command",
			Source:   flags.dataSource,
		}
		flags.freshnessMeta = meta
		return meta
	}
	return ensureFreshForResources(ctx, flags, resources...)
}

// runAutoRefresh invokes the API-backed refresh path for the stale resources.
// Phase 3 will split this into share-vs-API based on cliutil.Decision; today
// we always go to the API with a tight page budget (maxPages=1), matching
// sync --latest-only semantics.
func runAutoRefresh(ctx context.Context, flags *rootFlags, db *store.Store, resources []string) error {
	c, err := flags.newClient()
	if err != nil {
		return fmt.Errorf("build client: %w", err)
	}
	c.NoCache = true
	var failures []string
	for _, resource := range resources {
		select {
		case <-ctx.Done():
			return ctx.Err()
		default:
		}
		result := syncResource(ctx, c, db, resource, "", false, 1, true, false, nil, os.Stderr)
		if result.Err != nil {
			failures = append(failures, fmt.Sprintf("%s: %v", resource, result.Err))
		}
	}
	if len(failures) > 0 {
		return fmt.Errorf("%s", strings.Join(failures, "; "))
	}
	return nil
}
