// This file implements credential resolution (US-012): API key lookup from
// environment variables and a config file (per provider), plus an OAuth token
// source that refreshes short-lived tokens on expiry. The resolver satisfies
// the LoopConfig.GetAPIKey shape (func(ctx, provider) string) so the agent loop
// can obtain a fresh key per request.
//
// Security (FR: secret values are not written to logs): secret values are never logged or embedded in
// error messages. Errors and String()/redaction helpers reference credentials
// by key name / provider only.
package provider

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"strings"
	"sync"
	"time"
)

// APIKeyConfig is the on-disk config-file shape: a map of provider name to API
// key. It is parsed from JSON and holds only static keys (OAuth lives in
// TokenSource). Values are secrets and must not be logged.
type APIKeyConfig struct {
	// Keys maps provider name → API key.
	Keys map[string]string `json:"keys"`
}

// LoadAPIKeyConfig parses an APIKeyConfig from JSON bytes (e.g. a config file).
func LoadAPIKeyConfig(data []byte) (*APIKeyConfig, error) {
	var cfg APIKeyConfig
	if err := json.Unmarshal(data, &cfg); err != nil {
		return nil, fmt.Errorf("auth: parse api key config: %w", err)
	}
	if cfg.Keys == nil {
		cfg.Keys = make(map[string]string)
	}
	return &cfg, nil
}

// LoadAPIKeyConfigFile reads and parses an APIKeyConfig from a file path. A
// missing file is not an error — it returns an empty config so env/OAuth can
// still resolve keys.
func LoadAPIKeyConfigFile(path string) (*APIKeyConfig, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		if os.IsNotExist(err) {
			return &APIKeyConfig{Keys: make(map[string]string)}, nil
		}
		return nil, fmt.Errorf("auth: read api key config %q: %w", path, err)
	}
	return LoadAPIKeyConfig(data)
}

// envAPIKey returns the API key for a provider from the environment. It derives
// the candidate variable names from the provider registry (the single source of
// truth: LookupProviderSpec(provider).EnvVars, in precedence order), then falls
// back to a generic <PROVIDER>_API_KEY when the provider is unknown or none of
// its registered vars are set. Returns "" when no value is present.
func envAPIKey(provider string) string {
	if spec, ok := LookupProviderSpec(provider); ok {
		for _, name := range spec.EnvVars {
			if v := os.Getenv(name); v != "" {
				return v
			}
		}
	}
	// Generic fallback for unknown providers or when no registered var is set.
	generic := strings.ToUpper(provider) + "_API_KEY"
	return os.Getenv(generic)
}

// TokenSource yields an access token, refreshing it when expired. It models an
// OAuth credential whose access token is short-lived (FR-15: getApiKey refreshes
// on expiry). It is safe for concurrent use.
type TokenSource struct {
	mu           sync.Mutex
	accessToken  string
	expiry       time.Time
	refreshToken string
	// Refresh exchanges the current refresh token for a new access token. It
	// returns the new access token, its expiry, and (optionally) a rotated
	// refresh token. Required for a TokenSource to refresh; nil means the token
	// is static and never refreshed.
	Refresh func(ctx context.Context, refreshToken string) (OAuthToken, error)
	// Now is injectable for testing; defaults to time.Now.
	Now func() time.Time
	// Leeway refreshes the token this long before its actual expiry to avoid
	// racing the boundary. Defaults to 30s.
	Leeway time.Duration
}

// OAuthToken is the result of an OAuth exchange/refresh. Values are secrets.
type OAuthToken struct {
	AccessToken  string
	RefreshToken string
	Expiry       time.Time
}

// NewTokenSource builds a TokenSource seeded with an initial token and a refresh
// function. refresh may be nil for a static (never-expiring) token.
func NewTokenSource(initial OAuthToken, refresh func(ctx context.Context, refreshToken string) (OAuthToken, error)) *TokenSource {
	return &TokenSource{
		accessToken:  initial.AccessToken,
		expiry:       initial.Expiry,
		refreshToken: initial.RefreshToken,
		Refresh:      refresh,
	}
}

func (t *TokenSource) now() time.Time {
	if t.Now != nil {
		return t.Now()
	}
	return time.Now()
}

// defaultTokenLeeway is how far before an OAuth token's expiry it is treated as
// already expired, so a refresh happens before a request rather than mid-flight.
const defaultTokenLeeway = 30 * time.Second

func (t *TokenSource) leeway() time.Duration {
	if t.Leeway > 0 {
		return t.Leeway
	}
	return defaultTokenLeeway
}

// expired reports whether the access token is missing or within leeway of its
// expiry. A zero expiry means "never expires" (static token).
func (t *TokenSource) expired() bool {
	if t.accessToken == "" {
		return true
	}
	if t.expiry.IsZero() {
		return false
	}
	return !t.now().Before(t.expiry.Add(-t.leeway()))
}

// Token returns a valid access token, refreshing it when expired. It errors if
// a refresh is needed but no Refresh func is set, or if Refresh fails. The
// returned error never contains the token value.
func (t *TokenSource) Token(ctx context.Context) (string, error) {
	t.mu.Lock()
	defer t.mu.Unlock()
	if !t.expired() {
		return t.accessToken, nil
	}
	if t.Refresh == nil {
		return "", fmt.Errorf("auth: token expired and no refresh function configured")
	}
	tok, err := t.Refresh(ctx, t.refreshToken)
	if err != nil {
		return "", fmt.Errorf("auth: token refresh failed: %w", err)
	}
	t.accessToken = tok.AccessToken
	t.expiry = tok.Expiry
	if tok.RefreshToken != "" {
		t.refreshToken = tok.RefreshToken
	}
	return t.accessToken, nil
}

// CredentialStore resolves API keys per provider from three layers, in order:
// OAuth token source (if registered), environment variable, config file. It
// implements the LoopConfig.GetAPIKey shape via GetAPIKey.
//
// It is safe for concurrent use.
type CredentialStore struct {
	mu        sync.RWMutex
	config    *APIKeyConfig
	sources   map[string]*TokenSource // provider → OAuth token source
	overrides map[string]string       // provider → explicit key (highest static priority)
}

// NewCredentialStore builds a store over an optional config file. A nil config
// is treated as empty.
func NewCredentialStore(config *APIKeyConfig) *CredentialStore {
	if config == nil {
		config = &APIKeyConfig{Keys: make(map[string]string)}
	}
	return &CredentialStore{
		config:    config,
		sources:   make(map[string]*TokenSource),
		overrides: make(map[string]string),
	}
}

// SetOverride records an explicit API key for a provider that wins over the
// environment variable and config file (but not a live OAuth token, which is
// auto-refreshed). It is the seam for a CLI --api-key flag: an empty key is
// ignored so a bare flag does not clobber env/config resolution.
func (c *CredentialStore) SetOverride(provider, key string) {
	if key == "" {
		return
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	c.overrides[provider] = key
}

// RegisterOAuth registers an OAuth TokenSource for a provider. Once registered,
// GetAPIKey prefers the (auto-refreshing) OAuth token over static keys.
func (c *CredentialStore) RegisterOAuth(provider string, src *TokenSource) {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.sources[provider] = src
}

// GetAPIKey resolves the API key for a provider. Resolution order: OAuth token
// (refreshed on expiry) → explicit override (--api-key) → environment variable
// → config file. Returns "" when no credential is available. This matches
// LoopConfig.GetAPIKey so it can be assigned directly.
//
// On OAuth refresh failure it falls back to override/env/config rather than
// returning a secret-bearing error; the empty return lets the caller fall back
// to a static key. It never logs secret values.
func (c *CredentialStore) GetAPIKey(ctx context.Context, provider string) string {
	c.mu.RLock()
	src := c.sources[provider]
	override := c.overrides[provider]
	cfgKey := ""
	if c.config != nil {
		cfgKey = c.config.Keys[provider]
	}
	c.mu.RUnlock()

	if src != nil {
		if tok, err := src.Token(ctx); err == nil && tok != "" {
			return tok
		}
		// Refresh failed → fall through to static layers.
	}
	if override != "" {
		return override
	}
	if env := envAPIKey(provider); env != "" {
		return env
	}
	return cfgKey
}

// HasCredential reports whether any credential (OAuth/env/config) is available
// for a provider, without exposing the value.
func (c *CredentialStore) HasCredential(ctx context.Context, provider string) bool {
	return c.GetAPIKey(ctx, provider) != ""
}
