// 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 config

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

	"github.com/pelletier/go-toml/v2"
	"printing-press-oauth2-pp-cli/internal/cliutil"
)

type Config struct {
	BaseURL            string            `toml:"base_url"`
	AuthHeaderVal      string            `toml:"auth_header"`
	Headers            map[string]string `toml:"headers,omitempty"`
	AuthSource         string            `toml:"-"`
	CredentialSource   string            `toml:"-"`
	AgentcookieManaged bool              `toml:"-"`
	// configOwner records which on-disk file parseConfigData populated this
	// config from ("config-kind path" or "legacy config path") so the
	// credential-source fallback below reports where config-stored
	// credentials actually live. Unexported: never persisted.
	configOwner string
	// legacySourcePath records the legacy config path when Load fell
	// back to it. Used by save() to scrub credential fields from the
	// old location after relocation. Unexported: never persisted.
	legacySourcePath string
	AccessToken      string    `toml:"access_token"`
	RefreshToken     string    `toml:"refresh_token"`
	TokenExpiry      time.Time `toml:"token_expiry"`
	ClientID         string    `toml:"client_id"`
	ClientSecret     string    `toml:"client_secret"`
	// TokenURL overrides the spec-baked OAuth2 token endpoint. Same fallback
	// pattern as AuthorizationURL.
	TokenURL                        string          `toml:"token_url,omitempty"`
	Path                            string          `toml:"-"`
	envOverrides                    map[string]bool `toml:"-"`
	fileConfig                      *Config         `toml:"-"`
	PrintingPressOauth2ClientId     string          `toml:"press_oauth2_client_id"`
	PrintingPressOauth2ClientSecret string          `toml:"press_oauth2_client_secret"`
}

func Load(configPath string) (*Config, error) {
	cfg := &Config{
		BaseURL: "https://api.cc.example/v1",
	}

	// Resolve config path
	path, explicitConfigFile, err := resolveConfigPath(configPath)
	if err != nil {
		return nil, err
	}
	cfg.Path = path

	if explicitConfigFile {
		if err := readConfigFile(path, cfg, "config-kind path"); err != nil && !os.IsNotExist(err) {
			return nil, err
		}
	} else {
		legacyPath, err := LegacyConfigPath()
		if err != nil {
			return nil, err
		}
		data, sourcePath, err := cliutil.ReadFileWithLegacyFallback(path, legacyPath)
		if err != nil {
			if !os.IsNotExist(err) {
				return nil, err
			}
		} else {
			owner := "config-kind path"
			if sourcePath == legacyPath {
				owner = "legacy config path"
			}
			parsed := *cfg
			if err := parseConfigData(data, &parsed, sourcePath, owner); err != nil {
				if sourcePath == legacyPath {
					fmt.Fprintf(os.Stderr, "warning: legacy config parse skipped for %s: %v\n", sourcePath, err)
				} else {
					return nil, err
				}
			} else {
				*cfg = parsed
				if sourcePath == legacyPath {
					cfg.legacySourcePath = legacyPath
				}
			}
		}
	}
	cfg.Path = path
	if cfg.AgentcookieManagedByExternalStore() {
		cfg.markAgentcookieManaged()
	} else {
		creds, ok, err := cliutil.LoadCredentials()
		if err != nil {
			return nil, err
		}
		if ok && creds.HasValues() {
			cfg.clearCredentialFields()
			cfg.applyCredentials(creds)
			if cfg.hasCredentialFields() {
				cfg.AuthSource = "config"
				cfg.CredentialSource = "credentials file"
			}
		}
	}

	cfg.snapshotFileConfig()

	// Env var overrides
	if v := os.Getenv("PRINTING_PRESS_OAUTH2_CLIENT_ID"); v != "" {
		cfg.PrintingPressOauth2ClientId = v
		cfg.markEnvOverride("PrintingPressOauth2ClientId")
		cfg.AuthSource = "env:PRINTING_PRESS_OAUTH2_CLIENT_ID"
		cfg.CredentialSource = "env:PRINTING_PRESS_OAUTH2_CLIENT_ID"
	}
	if v := os.Getenv("PRINTING_PRESS_OAUTH2_CLIENT_SECRET"); v != "" {
		cfg.PrintingPressOauth2ClientSecret = v
		cfg.markEnvOverride("PrintingPressOauth2ClientSecret")
		cfg.AuthSource = "env:PRINTING_PRESS_OAUTH2_CLIENT_SECRET"
		cfg.CredentialSource = "env:PRINTING_PRESS_OAUTH2_CLIENT_SECRET"
	}
	// Label config-file-derived credentials so doctor can distinguish
	// "credentials persisted on disk" from "no credentials at all" — without
	// this, users who saved via set-token without an env var see a blank
	// auth_source and can't tell whether their config is being picked up.
	// The label is the literal "config" rather than "config:<path>"; the
	// config file path is exposed separately as report["config_path"], and
	// embedding it in auth_source leaks the user's home directory through
	// doctor's JSON envelope.
	if cfg.AuthSource == "" && cfg.hasCredentialFields() {
		cfg.AuthSource = "config"
	}
	if cfg.CredentialSource == "" && cfg.AuthSource == "config" {
		// Label config-stored credentials with the file they were parsed
		// from: the resolved config-kind path (covers --home and per-kind
		// env relocation as well as explicit --config files) or the legacy
		// config path when the read fell back to the pre-paths layout.
		cfg.CredentialSource = cfg.configOwner
		if cfg.CredentialSource == "" {
			cfg.CredentialSource = "legacy config path"
		}
	}

	// Soft agentcookie integration: if the agentcookie daemon manages this
	// CLI's secrets, it writes a marker file alongside the config file. When
	// the marker is present AND credentials came from the config (not from a
	// direct env var override that wins above), upgrade AuthSource to
	// "agentcookie" so doctor / auth-status can surface the bus state. When
	// the marker is absent, behavior is identical to pre-agentcookie: no
	// import, no network, no error. agentcookie itself is never imported
	// here — the contract is purely on-disk.
	if cfg.AuthSource == "config" {
		marker := filepath.Join(filepath.Dir(cfg.Path), ".agentcookie-managed")
		if _, err := os.Stat(marker); err == nil {
			cfg.AuthSource = "agentcookie"
			cfg.markAgentcookieManaged()
		}
	}

	// Base URL override (used by printing-press verify to point at mock/test servers)
	if v := os.Getenv("PRINTING_PRESS_OAUTH2_BASE_URL"); v != "" {
		cfg.BaseURL = v
	}
	if v := os.Getenv("PRINTING_PRESS_OAUTH2_TOKEN_URL"); v != "" {
		cfg.TokenURL = v
	}
	return cfg, nil
}

func resolveConfigPath(configPath string) (string, bool, error) {
	if strings.TrimSpace(configPath) != "" {
		return configPath, true, nil
	}
	if path := os.Getenv("PRINTING_PRESS_OAUTH2_CONFIG"); path != "" {
		return path, true, nil
	}
	dir, err := cliutil.ConfigDir()
	if err != nil {
		return "", false, err
	}
	return filepath.Join(dir, "config.toml"), false, nil
}

func LegacyConfigPath() (string, error) {
	home, err := os.UserHomeDir()
	if err != nil {
		return "", fmt.Errorf("resolve legacy config path: %w", err)
	}
	return filepath.Join(home, ".config", "printing-press-oauth2-pp-cli", "config.toml"), nil
}

func readConfigFile(path string, cfg *Config, owner string) error {
	data, err := os.ReadFile(path)
	if err != nil {
		return err
	}
	return parseConfigData(data, cfg, path, owner)
}

func parseConfigData(data []byte, cfg *Config, path string, owner string) error {
	if err := toml.Unmarshal(data, cfg); err != nil {
		return fmt.Errorf("parsing %s %s: %w", owner, path, err)
	}
	cfg.configOwner = owner
	return nil
}
func FileHasCredentialFields(path string) (bool, error) {
	var cfg Config
	if err := readConfigFile(path, &cfg, "credential probe"); err != nil {
		return false, err
	}
	return cfg.hasCredentialFields(), nil
}

func (c *Config) AuthHeader() string {
	if c.AuthHeaderVal != "" {
		return c.AuthHeaderVal
	}
	// Under OAuth2 (and bearer_token specs running OAuth grants such as
	// client_credentials or device_code) the configured env vars hold flow
	// inputs, not a usable bearer; the minted AccessToken must win. Sending
	// the client_id as Authorization: Bearer surfaces as token_rejected at
	// the API.
	if c.AccessToken != "" {
		if c.AuthSource == "" || strings.HasPrefix(c.AuthSource, "env:") {
			c.AuthSource = "oauth2"
		}
		return "Bearer " + c.AccessToken
	}
	return ""
}

func applyAuthFormat(format string, replacements map[string]string) string {
	if format == "" {
		return ""
	}
	for key, value := range replacements {
		format = strings.ReplaceAll(format, "{"+key+"}", value)
	}
	if strings.Contains(format, "{") {
		return ""
	}
	return format
}

func (c *Config) AgentcookieManagedByExternalStore() bool {
	if c.AgentcookieManaged || c.AuthSource == "agentcookie" || c.CredentialSource == "agentcookie" {
		return true
	}
	if c.Path == "" {
		return false
	}
	marker := filepath.Join(filepath.Dir(c.Path), ".agentcookie-managed")
	if _, err := os.Stat(marker); err == nil {
		return true
	}
	return false
}

func (c *Config) markAgentcookieManaged() {
	c.AgentcookieManaged = true
	c.CredentialSource = "agentcookie"
}

func (c *Config) hasCredentialFields() bool {
	if c.AuthHeaderVal != "" ||
		c.AccessToken != "" ||
		c.RefreshToken != "" ||
		c.ClientID != "" ||
		c.ClientSecret != "" {
		return true
	}
	if c.PrintingPressOauth2ClientId != "" {
		return true
	}
	if c.PrintingPressOauth2ClientSecret != "" {
		return true
	}
	return false
}

func (c *Config) clearCredentialFields() {
	c.AuthHeaderVal = ""
	c.AccessToken = ""
	c.RefreshToken = ""
	c.TokenExpiry = time.Time{}
	c.ClientID = ""
	c.ClientSecret = ""
	c.PrintingPressOauth2ClientId = ""
	c.PrintingPressOauth2ClientSecret = ""
}

func (c *Config) credentials() *cliutil.Credentials {
	return &cliutil.Credentials{
		AuthHeaderVal:                   c.AuthHeaderVal,
		AccessToken:                     c.AccessToken,
		RefreshToken:                    c.RefreshToken,
		TokenExpiry:                     c.TokenExpiry,
		ClientID:                        c.ClientID,
		ClientSecret:                    c.ClientSecret,
		PrintingPressOauth2ClientId:     c.PrintingPressOauth2ClientId,
		PrintingPressOauth2ClientSecret: c.PrintingPressOauth2ClientSecret,
	}
}

func (c *Config) applyCredentials(creds *cliutil.Credentials) {
	if creds == nil {
		return
	}
	c.AuthHeaderVal = creds.AuthHeaderVal
	c.AccessToken = creds.AccessToken
	c.RefreshToken = creds.RefreshToken
	c.TokenExpiry = creds.TokenExpiry
	c.ClientID = creds.ClientID
	c.ClientSecret = creds.ClientSecret
	c.PrintingPressOauth2ClientId = creds.PrintingPressOauth2ClientId
	c.PrintingPressOauth2ClientSecret = creds.PrintingPressOauth2ClientSecret
}

func (c *Config) saveCredentialsFirst() error {
	if c.AgentcookieManagedByExternalStore() {
		c.markAgentcookieManaged()
		return nil
	}
	persisted := c.configForSave()
	if err := cliutil.SaveCredentials(persisted.credentials()); err != nil {
		return err
	}
	c.CredentialSource = "credentials file"
	return nil
}

func (c *Config) SaveTokens(clientID, clientSecret, accessToken, refreshToken string, expiry time.Time) error {
	c.ClientID = clientID
	c.ClientSecret = clientSecret
	c.AccessToken = accessToken
	c.RefreshToken = refreshToken
	c.TokenExpiry = expiry
	delete(c.envOverrides, "ClientID")
	delete(c.envOverrides, "ClientSecret")
	delete(c.envOverrides, "AccessToken")
	delete(c.envOverrides, "RefreshToken")
	delete(c.envOverrides, "TokenExpiry")
	c.updateFileConfigField("ClientID")
	c.updateFileConfigField("ClientSecret")
	c.updateFileConfigField("AccessToken")
	c.updateFileConfigField("RefreshToken")
	c.updateFileConfigField("TokenExpiry")
	if err := c.saveCredentialsFirst(); err != nil {
		return err
	}
	return c.save()
}

func (c *Config) ClearTokens() error {
	// AuthHeader() falls back to the env-var-derived fields when AuthHeaderVal
	// and AccessToken are empty, so dropping the working credential requires
	// zeroing every emitted credential field, not just the OAuth trio.
	// ClientID/ClientSecret persist to disk via SaveTokens for the oauth2
	// and oauth2-cc flows, so logout must wipe them too; otherwise
	// `auth login` can re-mint a new access token unattended.
	c.AuthHeaderVal = ""
	c.AccessToken = ""
	c.RefreshToken = ""
	c.TokenExpiry = time.Time{}
	c.ClientID = ""
	c.ClientSecret = ""
	delete(c.envOverrides, "AuthHeaderVal")
	delete(c.envOverrides, "AccessToken")
	delete(c.envOverrides, "RefreshToken")
	delete(c.envOverrides, "TokenExpiry")
	delete(c.envOverrides, "ClientID")
	delete(c.envOverrides, "ClientSecret")
	c.updateFileConfigField("AuthHeaderVal")
	c.updateFileConfigField("AccessToken")
	c.updateFileConfigField("RefreshToken")
	c.updateFileConfigField("TokenExpiry")
	c.updateFileConfigField("ClientID")
	c.updateFileConfigField("ClientSecret")
	c.PrintingPressOauth2ClientId = ""
	delete(c.envOverrides, "PrintingPressOauth2ClientId")
	c.updateFileConfigField("PrintingPressOauth2ClientId")
	c.PrintingPressOauth2ClientSecret = ""
	delete(c.envOverrides, "PrintingPressOauth2ClientSecret")
	c.updateFileConfigField("PrintingPressOauth2ClientSecret")
	if c.AgentcookieManagedByExternalStore() {
		c.markAgentcookieManaged()
		// save() persists the full config (credential fields included) for
		// agentcookie-managed stores, so the zeroed fields must be written
		// back; returning early would leave the secrets on disk.
		return c.save()
	}
	if err := cliutil.RemoveCredentials(); err != nil {
		return err
	}
	return c.save()
}

func (c *Config) markEnvOverride(field string) {
	if c.envOverrides == nil {
		c.envOverrides = map[string]bool{}
	}
	c.envOverrides[field] = true
}

// cloneStringMap returns an independent copy of m (nil stays nil). The fileConfig
// snapshot must not share reference-type map fields (such as Headers) with the
// live config, or a later mutation to one would silently track in the other.
func cloneStringMap(m map[string]string) map[string]string {
	if m == nil {
		return nil
	}
	out := make(map[string]string, len(m))
	for k, v := range m {
		out[k] = v
	}
	return out
}

func (c *Config) snapshotFileConfig() {
	snapshot := *c
	snapshot.envOverrides = nil
	snapshot.fileConfig = nil
	// *c is a shallow copy: map fields are reference types, so the snapshot would
	// share them with c and silently track later mutations, defeating the
	// isolation this snapshot exists to provide. Clone them.
	snapshot.Headers = cloneStringMap(c.Headers)
	c.fileConfig = &snapshot
}

func (c *Config) configForSave() Config {
	out := *c
	if c.fileConfig != nil {
		if c.envOverrides["PrintingPressOauth2ClientId"] {
			out.PrintingPressOauth2ClientId = c.fileConfig.PrintingPressOauth2ClientId
		}
		if c.envOverrides["PrintingPressOauth2ClientSecret"] {
			out.PrintingPressOauth2ClientSecret = c.fileConfig.PrintingPressOauth2ClientSecret
		}
	}
	out.envOverrides = nil
	out.fileConfig = nil
	return out
}

func (c *Config) updateFileConfigField(field string) {
	if c.fileConfig == nil || c.envOverrides[field] {
		return
	}
	switch field {
	case "AuthHeaderVal":
		c.fileConfig.AuthHeaderVal = c.AuthHeaderVal
	case "AccessToken":
		c.fileConfig.AccessToken = c.AccessToken
	case "RefreshToken":
		c.fileConfig.RefreshToken = c.RefreshToken
	case "TokenExpiry":
		c.fileConfig.TokenExpiry = c.TokenExpiry
	case "ClientID":
		c.fileConfig.ClientID = c.ClientID
	case "ClientSecret":
		c.fileConfig.ClientSecret = c.ClientSecret
	case "PrintingPressOauth2ClientId":
		c.fileConfig.PrintingPressOauth2ClientId = c.PrintingPressOauth2ClientId
	case "PrintingPressOauth2ClientSecret":
		c.fileConfig.PrintingPressOauth2ClientSecret = c.PrintingPressOauth2ClientSecret
	}
}

func (c *Config) save() error {
	persisted := c.configForSave()
	var persist any = persisted
	if !c.AgentcookieManagedByExternalStore() {
		persist = persisted.persisted()
	}
	data, err := toml.Marshal(persist)
	if err != nil {
		return fmt.Errorf("marshaling config: %w", err)
	}
	if err := cliutil.AtomicWritePrivateFile(c.Path, data, 0o600, 0o700); err != nil {
		return err
	}
	c.scrubLegacyCredentials()
	if !c.AgentcookieManagedByExternalStore() {
		persisted.clearCredentialFields()
	}
	c.fileConfig = &persisted
	c.fileConfig.envOverrides = nil
	c.fileConfig.fileConfig = nil
	// persisted shares its map fields with c (configForSave shallow-copies *c),
	// so isolate the stored fileConfig the same way snapshotFileConfig does;
	// otherwise later mutations to c's maps leak into the on-disk snapshot.
	c.fileConfig.Headers = cloneStringMap(c.fileConfig.Headers)
	return nil
}
func (c *Config) scrubLegacyCredentials() {
	if c.legacySourcePath == "" || c.legacySourcePath == c.Path {
		return
	}
	if c.AgentcookieManagedByExternalStore() {
		return
	}
	data, err := os.ReadFile(c.legacySourcePath)
	if err != nil {
		if !os.IsNotExist(err) {
			fmt.Fprintf(os.Stderr, "warning: cannot read legacy config to scrub credentials: %v\n", err)
		}
		return
	}
	var legacy Config
	if err := toml.Unmarshal(data, &legacy); err != nil {
		fmt.Fprintf(os.Stderr, "warning: cannot parse legacy config to scrub credentials: %v\n", err)
		return
	}
	legacy.clearCredentialFields()
	scrubbed := legacy.persisted()
	scrubbedData, err := toml.Marshal(scrubbed)
	if err != nil {
		fmt.Fprintf(os.Stderr, "warning: cannot marshal scrubbed legacy config: %v\n", err)
		return
	}
	if err := cliutil.AtomicWritePrivateFile(c.legacySourcePath, scrubbedData, 0o600, 0o700); err != nil {
		fmt.Fprintf(os.Stderr, "warning: cannot write scrubbed legacy config: %v\n", err)
	}
}

type persistedConfig struct {
	BaseURL  string            `toml:"base_url"`
	Headers  map[string]string `toml:"headers,omitempty"`
	TokenURL string            `toml:"token_url,omitempty"`
}

func (c *Config) persisted() persistedConfig {
	return persistedConfig{
		BaseURL:  c.BaseURL,
		Headers:  c.Headers,
		TokenURL: c.TokenURL,
	}
}

// Ensure strings import is used
var _ = strings.ReplaceAll
