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

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

	"github.com/pelletier/go-toml/v2"
)

type Config struct {
	BaseURL       string            `toml:"base_url"`
	AuthHeaderVal string            `toml:"auth_header"`
	Headers       map[string]string `toml:"headers,omitempty"`
	AuthSource    string            `toml:"-"`
	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:"-"`
	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 := configPath
	if path == "" {
		path = os.Getenv("PRINTING_PRESS_OAUTH2_CONFIG")
	}
	if path == "" {
		home, _ := os.UserHomeDir()
		path = filepath.Join(home, ".config", "printing-press-oauth2-pp-cli", "config.toml")
	}
	cfg.Path = path

	// Try to load config file
	data, err := os.ReadFile(path)
	if err == nil {
		if err := toml.Unmarshal(data, cfg); err != nil {
			return nil, fmt.Errorf("parsing config %s: %w", path, err)
		}
	}

	// Env var overrides
	if v := os.Getenv("PRINTING_PRESS_OAUTH2_CLIENT_ID"); v != "" {
		cfg.PrintingPressOauth2ClientId = v
		cfg.AuthSource = "env:PRINTING_PRESS_OAUTH2_CLIENT_ID"
	}
	if v := os.Getenv("PRINTING_PRESS_OAUTH2_CLIENT_SECRET"); v != "" {
		cfg.PrintingPressOauth2ClientSecret = v
		cfg.AuthSource = "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.AuthHeaderVal != "" || cfg.AccessToken != "") {
		cfg.AuthSource = "config"
	}
	if cfg.AuthSource == "" && cfg.PrintingPressOauth2ClientId != "" {
		cfg.AuthSource = "config"
	}
	if cfg.AuthSource == "" && cfg.PrintingPressOauth2ClientSecret != "" {
		cfg.AuthSource = "config"
	}

	// 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 (c *Config) AuthHeader() string {
	if c.AuthHeaderVal != "" {
		return c.AuthHeaderVal
	}
	// Under OAuth2 (and bearer_token specs running the client_credentials
	// grant) the configured env vars hold client credentials (client_id /
	// client_secret), 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 != "" {
		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) 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
	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 = ""
	c.PrintingPressOauth2ClientId = ""
	c.PrintingPressOauth2ClientSecret = ""
	return c.save()
}

func (c *Config) save() error {
	dir := filepath.Dir(c.Path)
	if err := os.MkdirAll(dir, 0o700); err != nil {
		return fmt.Errorf("creating config dir: %w", err)
	}
	data, err := toml.Marshal(c)
	if err != nil {
		return fmt.Errorf("marshaling config: %w", err)
	}
	return os.WriteFile(c.Path, data, 0o600)
}

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