package pipeline

import (
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strings"

	"github.com/mvanhorn/cli-printing-press/v4/internal/naming"
	"github.com/mvanhorn/cli-printing-press/v4/internal/spec"
)

// AgentcookieManifestFilename is the per-CLI adoption manifest emitted
// alongside CLIManifestFilename. agentcookie's discovery loop walks
// well-known paths for this file and ships the named keys to the sink
// machine.
const AgentcookieManifestFilename = "agentcookie.toml"

// AgentcookieOverrideMarker, when present as a comment in the first
// bytes of an existing agentcookie.toml, signals that the file was hand-
// edited and must not be overwritten by the generator. Authors who want
// to take over the file remove the canonical "Generated by..." header
// and replace it with this marker.
const AgentcookieOverrideMarker = "# agentcookie-manual-override"

// WriteAgentcookieManifest emits agentcookie.toml into the generated CLI
// directory when the CLI has non-cookie auth. Skips emission for:
//   - cookie-only CLIs (no env-var auth, served by agentcookie's cookie
//     adapters instead);
//   - CLIs whose existing agentcookie.toml carries the override marker.
//
// Both skip cases log to stderr so the operator can see why a particular
// CLI is not getting an explicit-manifest entry.
func WriteAgentcookieManifest(p GenerateManifestParams) error {
	if p.Spec == nil {
		return nil
	}
	if !p.Spec.Auth.HasNonCookieAuth() {
		fmt.Fprintf(os.Stderr, "agentcookie: skipping %s (cookie-only or no env-var auth)\n", p.APIName)
		return nil
	}
	cliName := naming.CLI(p.APIName)
	outPath := filepath.Join(p.OutputDir, AgentcookieManifestFilename)
	if hasOverrideMarker(outPath) {
		fmt.Fprintf(os.Stderr, "agentcookie: skipping %s (manual override marker in %s)\n", cliName, outPath)
		return nil
	}
	displayName := strings.TrimSpace(p.DisplayName)
	if displayName == "" {
		displayName = cliName
	}
	description := strings.TrimSpace(p.Description)
	body := renderAgentcookieManifest(cliName, displayName, description, syncKeysFromAuth(&p.Spec.Auth))
	if err := os.WriteFile(outPath, []byte(body), 0o644); err != nil {
		return fmt.Errorf("writing agentcookie.toml: %w", err)
	}
	return nil
}

func renderAgentcookieManifest(cliName, displayName, description string, syncKeys map[string]bool) string {
	var b strings.Builder
	fmt.Fprintln(&b, "# agentcookie.toml: secrets-bus adoption manifest v2")
	fmt.Fprintln(&b, "# See docs/spec-agentcookie-secrets-bus-v2-adoption.md")
	fmt.Fprintln(&b, "schema_version = 2")
	fmt.Fprintf(&b, "name = %s\n", tomlString(cliName))
	fmt.Fprintf(&b, "display_name = %s\n", tomlString(displayName))
	fmt.Fprintf(&b, "description = %s\n", tomlString(description))
	fmt.Fprintln(&b, `project_kind = "cli"`)
	fmt.Fprintln(&b)
	fmt.Fprintln(&b, "[secrets.file]")
	fmt.Fprintf(&b, "path = %s\n", tomlString(fmt.Sprintf("~/.config/%s/config.toml", cliName)))
	fmt.Fprintln(&b)
	fmt.Fprintln(&b, "[sync]")
	fmt.Fprintln(&b, "default = false")
	fmt.Fprintln(&b)
	fmt.Fprintln(&b, "[sync.keys]")
	keys := make([]string, 0, len(syncKeys))
	for key := range syncKeys {
		keys = append(keys, key)
	}
	sort.Strings(keys)
	for _, key := range keys {
		fmt.Fprintf(&b, "%s = %t\n", key, syncKeys[key])
	}
	return b.String()
}

func tomlString(s string) string {
	replacer := strings.NewReplacer(
		`\`, `\\`,
		`"`, `\"`,
		"\n", `\n`,
		"\r", `\r`,
		"\t", `\t`,
	)
	return `"` + replacer.Replace(s) + `"`
}

// syncKeysFromAuth builds the [sync.keys] map from the auth config.
// Prefers EnvVarSpecs (which carry per-key Sensitive flags) and falls
// back to EnvVars (defaulting Sensitive=true since auth env vars are
// credentials by definition).
func syncKeysFromAuth(a *spec.AuthConfig) map[string]bool {
	out := map[string]bool{}
	if a == nil {
		return out
	}
	if len(a.EnvVarSpecs) > 0 {
		for _, ev := range a.EnvVarSpecs {
			name := strings.TrimSpace(ev.Name)
			if name == "" {
				continue
			}
			out[name] = ev.Sensitive
		}
		// Some EnvVarSpecs entries may have Sensitive=false (e.g. an OAuth
		// client_id paired with a sensitive client_secret); accept the
		// per-spec value. If every spec entry is sensitive=false, that's
		// the author's call, not a bug.
		return out
	}
	for _, name := range a.EnvVars {
		name = strings.TrimSpace(name)
		if name == "" {
			continue
		}
		out[name] = true
	}
	return out
}

// hasOverrideMarker returns true when the file at path exists and its
// leading bytes contain the manual-override marker. Missing files,
// unreadable files, and files without the marker all return false.
func hasOverrideMarker(path string) bool {
	data, err := os.ReadFile(path)
	if err != nil {
		return false
	}
	// Only inspect the first kilobyte; the marker is required at the top
	// of the file for visibility, and reading the full body wastes work.
	head := data
	if len(head) > 1024 {
		head = head[:1024]
	}
	return strings.Contains(string(head), AgentcookieOverrideMarker)
}
