// Package config parses Tapir's runtime configuration from environment
// variables into a typed struct. No secrets are baked into code: the gateway
// key and OAuth client secret are read from the environment (later resolved via
// op/ESO), and the OAuth refresh token is never held here — it lives behind the
// SecretStore port, addressed by the opaque TokenRef.
//
// Defaults target the homelab snapshot in docs/homelab-integration.md; every
// value is overridable so the same binary runs standalone or in-cluster.
package config

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

// Config is Tapir's fully-resolved runtime configuration.
type Config struct {
	// UserID is the Tapir user the run operates as. Stage 0 has exactly one.
	// It must be a UUID: it keys the UUID user_id/video_id columns (data-model).
	UserID string

	// GatewayURL is the OpenAI-compatible LiteLLM base URL (".../v1").
	GatewayURL string
	// GatewayKey authorizes the gateway. Read from env, never committed.
	GatewayKey string
	// SummarizerModel is the alias in host/name form, e.g. "koala/phi4-mini".
	SummarizerModel string
	// SummarizerTimeout bounds a single completion call. Thinking models are
	// slow, so the default is generous.
	SummarizerTimeout time.Duration

	// DBDSN is the Postgres DSN for the store sink.
	DBDSN string

	// YouTube OAuth app credentials (the registered client), read from env.
	YTClientID     string
	YTClientSecret string
	// YTTokenRef is the opaque SecretStore reference under which the YouTube
	// refresh token is persisted/resolved. Not the token itself.
	YTTokenRef string

	// SecretsFile is the path to the local file-backed SecretStore (0600). A
	// Stage-0 stand-in for op/ESO, swappable behind the SecretStore port.
	SecretsFile string

	// OAuthRedirectAddr is the host:port the `auth` command's local listener
	// binds for the OAuth redirect, e.g. "localhost:8080".
	OAuthRedirectAddr string

	// PollInterval, when > 0, makes `run` loop on that cadence; 0 means run once.
	PollInterval time.Duration

	// HTTPAddr is the listen address for `tapir serve` (the Stage-0 web UI).
	HTTPAddr string

	// Dex OIDC (web login, ADR-011). When OIDCIssuer is empty, `serve` falls back
	// to the allow-all StubAuth (local dev). When set, serve uses Dex with
	// single-user allowlist authz.
	OIDCIssuer      string
	DexClientID     string
	DexClientSecret string
	OIDCRedirectURL string
	SessionSecret   string
	AllowedSubject  string
}

// DexConfigured reports whether Dex OIDC login is wired (issuer present). When
// false, `serve` uses StubAuth (dev only).
func (c Config) DexConfigured() bool { return strings.TrimSpace(c.OIDCIssuer) != "" }

// Defaults (see docs/homelab-integration.md). All overridable via env.
const (
	defaultGatewayURL        = "http://koala:30401/v1"
	defaultSummarizerModel   = "koala/phi4-mini"
	defaultSummarizerTimeout = 5 * time.Minute
	defaultYTTokenRef        = "youtube/refresh_token"
	defaultOAuthRedirectAddr = "localhost:8080"
	defaultHTTPAddr          = ":8080"
)

// Load reads the environment into a Config, applying defaults. It does not
// validate that required fields are present — call ValidateForAuth or
// ValidateForRun for the command being run, so each command demands only what
// it needs.
func Load() (Config, error) {
	c := Config{
		UserID:            os.Getenv("TAPIR_USER_ID"),
		GatewayURL:        envOr("TAPIR_GATEWAY_URL", defaultGatewayURL),
		GatewayKey:        os.Getenv("TAPIR_GATEWAY_KEY"),
		SummarizerModel:   envOr("TAPIR_SUMMARIZER_MODEL", defaultSummarizerModel),
		DBDSN:             os.Getenv("TAPIR_DB_DSN"),
		YTClientID:        os.Getenv("TAPIR_YT_CLIENT_ID"),
		YTClientSecret:    os.Getenv("TAPIR_YT_CLIENT_SECRET"),
		YTTokenRef:        envOr("TAPIR_YT_TOKEN_REF", defaultYTTokenRef),
		SecretsFile:       envOr("TAPIR_SECRETS_FILE", defaultSecretsFile()),
		OAuthRedirectAddr: envOr("TAPIR_OAUTH_REDIRECT_ADDR", defaultOAuthRedirectAddr),
		HTTPAddr:          envOr("TAPIR_HTTP_ADDR", defaultHTTPAddr),
		OIDCIssuer:        os.Getenv("TAPIR_OIDC_ISSUER"),
		DexClientID:       os.Getenv("TAPIR_DEX_CLIENT_ID"),
		DexClientSecret:   os.Getenv("TAPIR_DEX_CLIENT_SECRET"),
		OIDCRedirectURL:   os.Getenv("TAPIR_OIDC_REDIRECT_URL"),
		SessionSecret:     os.Getenv("TAPIR_SESSION_SECRET"),
		AllowedSubject:    os.Getenv("TAPIR_ALLOWED_SUBJECT"),
	}

	timeout, err := durationOr("TAPIR_SUMMARIZER_TIMEOUT", defaultSummarizerTimeout)
	if err != nil {
		return Config{}, err
	}
	c.SummarizerTimeout = timeout

	interval, err := durationOr("TAPIR_POLL_INTERVAL", 0)
	if err != nil {
		return Config{}, err
	}
	c.PollInterval = interval

	return c, nil
}

// ValidateForAuth checks the fields the `auth` command needs: the YouTube OAuth
// app credentials, a place to persist the token, and the redirect listener.
func (c Config) ValidateForAuth() error {
	return c.require(map[string]string{
		"TAPIR_YT_CLIENT_ID":     c.YTClientID,
		"TAPIR_YT_CLIENT_SECRET": c.YTClientSecret,
		"TAPIR_YT_TOKEN_REF":     c.YTTokenRef,
		"TAPIR_SECRETS_FILE":     c.SecretsFile,
	})
}

// ValidateForRun checks the fields the `run` command needs end to end.
func (c Config) ValidateForRun() error {
	return c.require(map[string]string{
		"TAPIR_USER_ID":          c.UserID,
		"TAPIR_GATEWAY_URL":      c.GatewayURL,
		"TAPIR_SUMMARIZER_MODEL": c.SummarizerModel,
		"TAPIR_DB_DSN":           c.DBDSN,
		"TAPIR_YT_CLIENT_ID":     c.YTClientID,
		"TAPIR_YT_CLIENT_SECRET": c.YTClientSecret,
		"TAPIR_YT_TOKEN_REF":     c.YTTokenRef,
		"TAPIR_SECRETS_FILE":     c.SecretsFile,
	})
}

// ValidateForServe checks the fields the `serve` command needs: a user to act
// as, the store DSN, and a listen address. Auth (Dex) config is validated by the
// auth layer the Conductor wires in, not here.
func (c Config) ValidateForServe() error {
	return c.require(map[string]string{
		"TAPIR_USER_ID":   c.UserID,
		"TAPIR_DB_DSN":    c.DBDSN,
		"TAPIR_HTTP_ADDR": c.HTTPAddr,
	})
}

func (c Config) require(fields map[string]string) error {
	var missing []string
	for name, val := range fields {
		if strings.TrimSpace(val) == "" {
			missing = append(missing, name)
		}
	}
	if len(missing) > 0 {
		return fmt.Errorf("missing required config: %s", strings.Join(sortedMissing(missing), ", "))
	}
	return nil
}

func sortedMissing(xs []string) []string {
	sort.Strings(xs)
	return xs
}

func envOr(key, fallback string) string {
	if v := os.Getenv(key); v != "" {
		return v
	}
	return fallback
}

func durationOr(key string, fallback time.Duration) (time.Duration, error) {
	v := os.Getenv(key)
	if v == "" {
		return fallback, nil
	}
	d, err := time.ParseDuration(v)
	if err != nil {
		return 0, fmt.Errorf("config: %s=%q: %w", key, v, err)
	}
	return d, nil
}

// defaultSecretsFile resolves to <user-config-dir>/tapir/secrets.json, falling
// back to a cwd-relative path when the config dir is unavailable.
func defaultSecretsFile() string {
	dir, err := os.UserConfigDir()
	if err != nil {
		return "tapir-secrets.json"
	}
	return filepath.Join(dir, "tapir", "secrets.json")
}
