// Package config loads, validates, and hot-reloads YAML configuration for the
// Command Center. Phase 0 owns one file (system.yaml); subsequent phases add
// more files in the same directory.
package config

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"

	"gopkg.in/yaml.v3"
)

// SystemFileName is the basename of the Phase 0 root config file inside the
// declarative config directory.
const SystemFileName = "system.yaml"

// SystemConfig is the typed representation of `system.yaml`. Subsequent phases
// add their own sibling structs (FiltersConfig, NotificationRulesConfig, etc.)
// loaded by their own loaders; this struct should stay minimal.
type SystemConfig struct {
	DevMode  bool           `yaml:"dev_mode"`
	Listen   ListenConfig   `yaml:"listen"`
	Database DatabaseConfig `yaml:"database"`
	Secrets  SecretsConfig  `yaml:"secrets"`
	Logging  LoggingConfig  `yaml:"logging"`
	Auth     AuthConfig     `yaml:"auth"`
}

// AuthConfig controls the Phase 2 WebAuthn relying-party identity, session
// lifetime, and rate limits. Smart defaults apply in dev_mode; production
// deployments should set RPID and RPOrigins explicitly.
type AuthConfig struct {
	// RPID is the WebAuthn Relying Party identifier — the host portion
	// matched against the browser's origin. Defaults to "localhost" in
	// dev_mode; production should set this to the Tailscale hostname (e.g.
	// "cc.tailnet.ts.net").
	RPID string `yaml:"rp_id"`

	// RPDisplayName is shown to the operator inside the WebAuthn UI.
	RPDisplayName string `yaml:"rp_display_name"`

	// RPOrigins is the list of allowed origins for assertion responses.
	// In dev_mode the http://127.0.0.1:<port> and http://localhost:<port>
	// pair is generated automatically when this list is empty.
	RPOrigins []string `yaml:"rp_origins"`

	// SessionLifetimeSeconds defaults to 30 days when zero. Sessions slide
	// forward on every activity.
	SessionLifetimeSeconds int `yaml:"session_lifetime_seconds"`

	// LoginRateLimit{Burst,WindowSeconds} cap brute-force attempts against
	// the /api/auth/webauthn/login/* endpoints per source IP.
	LoginRateLimitBurst         int `yaml:"login_rate_limit_burst"`
	LoginRateLimitWindowSeconds int `yaml:"login_rate_limit_window_seconds"`

	// RecoveryRateLimit{Burst,WindowSeconds} is the stricter limit on
	// /api/auth/recovery.
	RecoveryRateLimitBurst         int `yaml:"recovery_rate_limit_burst"`
	RecoveryRateLimitWindowSeconds int `yaml:"recovery_rate_limit_window_seconds"`
}

// ListenConfig controls how the HTTP server binds. The Tailscale interface
// detection logic lives in internal/server.
type ListenConfig struct {
	Port                      int    `yaml:"port"`
	TailscaleInterfacePattern string `yaml:"tailscale_interface_pattern"`
	ReadTimeoutSeconds        int    `yaml:"read_timeout_seconds"`
	WriteTimeoutSeconds       int    `yaml:"write_timeout_seconds"`
}

// DatabaseConfig contains file paths for SQLite and DuckDB. Both files are
// created on first run if missing.
type DatabaseConfig struct {
	SQLitePath string `yaml:"sqlite_path"`
	DuckDBPath string `yaml:"duckdb_path"`
}

// SecretsConfig controls the age identity discovery order. See DECISIONS.md D5.
type SecretsConfig struct {
	AgeIdentityFile string `yaml:"age_identity_file"`
}

// LoggingConfig configures the global zerolog logger.
type LoggingConfig struct {
	Level  string `yaml:"level"`
	Format string `yaml:"format"`
}

// Defaults returns a SystemConfig with sensible defaults for any field the
// YAML file omits. The caller can use this both at first-time setup and as a
// merge base for partially-specified config files.
func Defaults() SystemConfig {
	return SystemConfig{
		DevMode: false,
		Listen: ListenConfig{
			Port:                      8443,
			TailscaleInterfacePattern: "tailscale*",
			ReadTimeoutSeconds:        15,
			WriteTimeoutSeconds:       30,
		},
		Database: DatabaseConfig{
			SQLitePath: "/var/lib/command-center/command-center.db",
			DuckDBPath: "/var/lib/command-center/command-center.duckdb",
		},
		Secrets: SecretsConfig{},
		Logging: LoggingConfig{Level: "info", Format: "json"},
		Auth: AuthConfig{
			RPDisplayName:                  "Seedbox Command Center",
			SessionLifetimeSeconds:         30 * 24 * 3600,
			LoginRateLimitBurst:            5,
			LoginRateLimitWindowSeconds:    5 * 60,
			RecoveryRateLimitBurst:         3,
			RecoveryRateLimitWindowSeconds: 60 * 60,
		},
	}
}

// ResolvedAuth returns the AuthConfig with dev_mode-aware defaults applied:
// when RPID is empty and DevMode is true, RPID becomes "localhost" and
// RPOrigins (if empty) become the local 127.0.0.1 + localhost pair at the
// configured Listen.Port.
func (c *SystemConfig) ResolvedAuth() AuthConfig {
	a := c.Auth
	if a.RPID == "" && c.DevMode {
		a.RPID = "localhost"
	}
	if len(a.RPOrigins) == 0 && c.DevMode {
		port := c.Listen.Port
		if port == 0 {
			port = 8443
		}
		a.RPOrigins = []string{
			fmt.Sprintf("http://127.0.0.1:%d", port),
			fmt.Sprintf("http://localhost:%d", port),
		}
	}
	return a
}

// LoadFromDir reads `system.yaml` from dir, applies defaults to unset fields,
// validates the result, and returns it. The file must exist; an absent file
// is an error rather than a "fall back to defaults" — operators should always
// see their full intended config in a tracked file.
func LoadFromDir(dir string) (*SystemConfig, error) {
	path := filepath.Join(dir, SystemFileName)
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, fmt.Errorf("config: read %s: %w", path, err)
	}
	return LoadFromBytes(data)
}

// LoadFromBytes parses a system.yaml document, merges it on top of Defaults(),
// and validates the result.
func LoadFromBytes(data []byte) (*SystemConfig, error) {
	cfg := Defaults()
	if err := yaml.Unmarshal(data, &cfg); err != nil {
		return nil, fmt.Errorf("config: yaml parse: %w", err)
	}
	if err := cfg.Validate(); err != nil {
		return nil, err
	}
	return &cfg, nil
}

// Validate enforces the constraints documented in DECISIONS.md and PROJECT.md.
// Hot-reload calls Validate before publishing a new config; a failure leaves
// the previous config in effect.
func (c *SystemConfig) Validate() error {
	var errs []string

	if c.Listen.Port <= 0 || c.Listen.Port > 65535 {
		errs = append(errs, fmt.Sprintf("listen.port out of range: %d", c.Listen.Port))
	}
	if c.Listen.TailscaleInterfacePattern == "" {
		errs = append(errs, "listen.tailscale_interface_pattern must not be empty")
	}
	if c.Listen.ReadTimeoutSeconds < 0 {
		errs = append(errs, "listen.read_timeout_seconds must not be negative")
	}
	if c.Listen.WriteTimeoutSeconds < 0 {
		errs = append(errs, "listen.write_timeout_seconds must not be negative")
	}
	if c.Database.SQLitePath == "" {
		errs = append(errs, "database.sqlite_path must not be empty")
	}
	if c.Database.DuckDBPath == "" {
		errs = append(errs, "database.duckdb_path must not be empty")
	}
	switch c.Logging.Level {
	case "", "trace", "debug", "info", "warn", "warning", "error":
	default:
		errs = append(errs, "logging.level must be one of trace|debug|info|warn|error")
	}
	if c.Logging.Format != "" && c.Logging.Format != "json" {
		// Phase 0 only supports JSON; allow empty (defaulted to json) but
		// reject anything explicitly non-json so misconfiguration is loud.
		errs = append(errs, "logging.format must be 'json' or unset (Phase 0)")
	}

	// Auth: in production (dev_mode=false), RPID and RPOrigins are required.
	// In dev_mode they can be auto-derived from Listen.Port.
	if !c.DevMode {
		if c.Auth.RPID == "" {
			errs = append(errs, "auth.rp_id must be set when dev_mode is false")
		}
		if len(c.Auth.RPOrigins) == 0 {
			errs = append(errs, "auth.rp_origins must list at least one allowed origin when dev_mode is false")
		}
	}
	if c.Auth.SessionLifetimeSeconds < 0 {
		errs = append(errs, "auth.session_lifetime_seconds must not be negative")
	}

	if len(errs) > 0 {
		return errors.New("config: invalid system.yaml: " + joinErrs(errs))
	}
	return nil
}

func joinErrs(errs []string) string {
	out := ""
	for i, e := range errs {
		if i > 0 {
			out += "; "
		}
		out += e
	}
	return out
}
