package config

import (
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"os"
	"strings"

	"gopkg.in/yaml.v3"
)

// Config is the top-level configuration structure.
// Contains ONLY infrastructure/customer identity.
// App-specific config lives in per-app app.yaml files.
// OffsiteConfig is the hub-served offsite target descriptor (SLICE 1/2). Non-secret; the transient password
// is fetched once via the hub consume endpoint (never in config). Mirrors hub offsite.Descriptor.
type OffsiteConfig struct {
	Enabled         bool   `yaml:"enabled"`
	Type            string `yaml:"type"`      // "shared" | "dedicated"
	Host            string `yaml:"host"`
	User            string `yaml:"user"`
	Port            int    `yaml:"port"`      // 23
	RepoPath        string `yaml:"repo_path"` // /home/<repo>
	QuotaGB         int    `yaml:"quota_gb"`
	BoxType         string `yaml:"box_type"`
	HostFingerprint string `yaml:"host_fingerprint"` // SHA256:… — verified before pinning (no blind TOFU)
}

type Config struct {
	Customer       CustomerConfig       `yaml:"customer"`
	Infrastructure InfrastructureConfig `yaml:"infrastructure"`
	Paths          PathsConfig          `yaml:"paths"`
	Web            WebConfig            `yaml:"web"`
	Git            GitConfig            `yaml:"git"`
	Stacks         StacksConfig         `yaml:"stacks"`
	Backup         BackupConfig         `yaml:"backup"`
	Monitoring     MonitoringConfig     `yaml:"monitoring"`
	Hub            HubConfig            `yaml:"hub"`
	SelfUpdate     SelfUpdateConfig     `yaml:"self_update"`
	Notifications  NotificationsConfig  `yaml:"notifications"`
	Logging        LoggingConfig        `yaml:"logging"`
	Assets         AssetsConfig         `yaml:"assets"`
	System         SystemConfig         `yaml:"system"`
	LocalAPI       LocalAPIConfig       `yaml:"local_api"`
	Quiesce        QuiesceConfig        `yaml:"quiesce"`
	MailRelay      MailRelayConfig      `yaml:"mail_relay"`
	Offsite        OffsiteConfig        `yaml:"offsite"`
}

// MailRelayConfig tunes the in-controller SMTP shim (app email → shim → hub → Resend).
// The shim only runs when hub.enabled AND the global app-email toggle is on; it binds to
// the app Docker network ONLY (never published to host/internet). ShimHost is the DNS
// name injected into apps' SMTP_HOST (the controller's container name on the app network).
type MailRelayConfig struct {
	// Enabled is a hard kill-switch: false disables the shim regardless of the runtime
	// toggle. nil/true → the runtime app-email toggle decides. (Operational override only.)
	Enabled          *bool    `yaml:"enabled"`
	PlainListen      string   `yaml:"plain_listen"`        // plaintext+STARTTLS, default ":2525"
	TLSListen        string   `yaml:"tls_listen"`          // implicit-TLS, default ":2465"
	PlainNoTLSListen string   `yaml:"plain_no_tls_listen"` // plaintext-only, no STARTTLS, default ":2526"
	ShimHost         string   `yaml:"shim_host"`           // app-network DNS name of the controller; default "felhom-controller"
	FromDomains      []string `yaml:"from_domains"`        // From-header allowlist; default ["felhom.eu"]
}

// HardEnabled reports the operational kill-switch (default on unless explicitly false).
func (m MailRelayConfig) HardEnabled() bool {
	return m.Enabled == nil || *m.Enabled
}

// LocalAPIConfig is the in-guest controller's handle on the host agent's per-guest local API
// (doc 03 §6, slice 8A). The agent mints the token + serves a self-signed leaf; the controller
// reaches it over the bridge, pinning the leaf SHA-256. Seeded from bootstrap.json at first run.
type LocalAPIConfig struct {
	Endpoint    string `yaml:"endpoint"`    // host bridge IP:port, e.g. "192.168.0.162:8443"
	Fingerprint string `yaml:"fingerprint"` // agent leaf-cert SHA-256 (hex) to pin
	Token       string `yaml:"token"`       // per-guest bearer; SECRET
}

// QuiesceConfig tunes the slice-8B app-consistent backup loop (doc 03 §8): poll the agent's
// /backup/due, and when due stop the app stacks around the agent vzdump for a clean-shutdown
// backup. Runs only when the local API is configured (a provisioned guest). MaxQuiesce bounds the
// app downtime — the controller unquiesces no matter what once it elapses.
type QuiesceConfig struct {
	Enabled      *bool  `yaml:"enabled"`              // nil/true → on when local API configured; false → off
	PollInterval string `yaml:"poll_interval"`        // /backup/due check cadence (default "5m")
	StatusPoll   string `yaml:"status_poll_interval"` // /backup/status poll while quiesced (default "10s")
	MaxQuiesce   string `yaml:"max_quiesce_duration"` // hard downtime bound (default "30m")
}

// QuiesceEnabled reports whether the quiesce loop should run (default on unless explicitly false).
func (q QuiesceConfig) QuiesceEnabled() bool {
	return q.Enabled == nil || *q.Enabled
}

type SystemConfig struct {
	ReservedMemoryMB int `yaml:"reserved_memory_mb"`
}

type CustomerConfig struct {
	ID             string `yaml:"id"`
	Name           string `yaml:"name"`
	Domain         string `yaml:"domain"`
	Email          string `yaml:"email"`
	TelegramChatID string `yaml:"telegram_chat_id"`
}

type InfrastructureConfig struct {
	CFTunnelToken string `yaml:"cf_tunnel_token"`
	CFAPIToken    string `yaml:"cf_api_token"`
}

type PathsConfig struct {
	StacksDir      string `yaml:"stacks_dir"`
	DataDir        string `yaml:"data_dir"`
	SystemDataPath string `yaml:"system_data_path"`
	HDDPath        string `yaml:"hdd_path"`
}

type WebConfig struct {
	Listen        string `yaml:"listen"`
	SetupListen   string `yaml:"setup_listen"` // Plain HTTP listener for setup wizard (only active during setup mode)
	PasswordHash  string `yaml:"password_hash"`
	SessionSecret string `yaml:"session_secret"`
}

type GitConfig struct {
	RepoURL      string `yaml:"repo_url"`
	Branch       string `yaml:"branch"`
	SyncInterval string `yaml:"sync_interval"`
	Username     string `yaml:"username"`
	Token        string `yaml:"token"`
}

type StacksConfig struct {
	Protected      []string `yaml:"protected"`
	UpdateWindow   string   `yaml:"update_window"`
	ComposeCommand string   `yaml:"compose_command"`
}

type BackupConfig struct {
	Enabled            bool            `yaml:"enabled"`
	ResticPasswordFile string          `yaml:"restic_password_file"`
	DBDumpSchedule     string          `yaml:"db_dump_schedule"`
	ResticSchedule     string          `yaml:"restic_schedule"`
	Retention          RetentionConfig `yaml:"retention"`
	PruneSchedule      string          `yaml:"prune_schedule"`
}

type RetentionConfig struct {
	KeepDaily   int `yaml:"keep_daily"`
	KeepWeekly  int `yaml:"keep_weekly"`
	KeepMonthly int `yaml:"keep_monthly"`
}

type MonitoringConfig struct {
	Enabled              bool             `yaml:"enabled"`
	HealthchecksBase     string           `yaml:"healthchecks_base"`
	PingUUIDs            PingUUIDsConfig  `yaml:"ping_uuids"`
	HealthCheckSchedule  string           `yaml:"health_check_schedule"`
	SystemHealthInterval string           `yaml:"system_health_interval"`
	Thresholds           ThresholdsConfig `yaml:"thresholds"`
}

type PingUUIDsConfig struct {
	Heartbeat       string `yaml:"heartbeat"`
	DBDump          string `yaml:"db_dump"`
	Backup          string `yaml:"backup"`
	SystemHealth    string `yaml:"system_health"`
	BackupIntegrity string `yaml:"backup_integrity"`
}

type ThresholdsConfig struct {
	DiskWarnPercent        int `yaml:"disk_warn_percent"`
	DiskCritPercent        int `yaml:"disk_crit_percent"`
	BackupMaxAgeHours      int `yaml:"backup_max_age_hours"`
	CPUWarnPercent         int `yaml:"cpu_warn_percent"`
	MemoryWarnPercent      int `yaml:"memory_warn_percent"`
	TemperatureWarnCelsius int `yaml:"temperature_warn_celsius"`
}

type SelfUpdateConfig struct {
	Enabled              bool   `yaml:"enabled"`
	CheckInterval        string `yaml:"check_interval"`
	Image                string `yaml:"image"`
	AutoUpdate           bool   `yaml:"auto_update"`
	AutoUpdateTime       string `yaml:"auto_update_time"`
	HealthTimeoutSeconds int    `yaml:"health_timeout_seconds"`
}

type NotificationsConfig struct {
	CustomerEvents []string `yaml:"customer_events"`
	OperatorEvents []string `yaml:"operator_events"`
}

type LoggingConfig struct {
	Level     string `yaml:"level"`
	File      string `yaml:"file"`
	MaxSizeMB int    `yaml:"max_size_mb"`
	MaxFiles  int    `yaml:"max_files"`
}

type AssetsConfig struct {
	SourceURL    string `yaml:"source_url"`    // Only used during build, not runtime
	SyncEnabled  bool   `yaml:"sync_enabled"`  // Download assets from Hub API
	SyncSchedule string `yaml:"sync_schedule"` // Daily sync time (HH:MM), default "05:00"
}

type HubConfig struct {
	Enabled      bool   `yaml:"enabled"`
	URL          string `yaml:"url"`
	APIKey       string `yaml:"api_key"`
	PushInterval string `yaml:"push_interval"`
}

// Load reads and parses the config file, applies defaults, and validates.
func Load(path string) (*Config, error) {
	cfg, err := loadAndParse(path)
	if err != nil {
		return nil, err
	}
	if err := validate(cfg); err != nil {
		return nil, fmt.Errorf("config validation: %w", err)
	}
	return cfg, nil
}

// LoadPermissive reads and parses the config file, applies defaults, but skips validation.
// Used during setup mode where customer.id and domain may not be set yet.
func LoadPermissive(path string) (*Config, error) {
	return loadAndParse(path)
}

// Default returns a Config with all defaults applied. Used when the config file
// is missing or unreadable and the controller needs to enter setup mode.
func Default() *Config {
	cfg := &Config{}
	applyDefaults(cfg)
	return cfg
}

func loadAndParse(path string) (*Config, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, fmt.Errorf("reading config file: %w", err)
	}

	// F-C2-1: parse the RAW bytes — do NOT os.ExpandEnv the whole file. A bcrypt password_hash
	// ($2a$10$…) is full of `$word` sequences that ExpandEnv silently replaces with (usually empty)
	// env values, corrupting the stored hash on load (a silent auth-integrity bug). Typed env
	// overrides are the sanctioned mechanism — applyEnvOverrides (e.g. FELHOM_WEB_PASSWORD_HASH),
	// applied after parse. No shipped controller.yaml relies on file-level ${VAR} interpolation.
	cfg := &Config{}
	if err := yaml.Unmarshal(data, cfg); err != nil {
		return nil, fmt.Errorf("parsing config file: %w", err)
	}

	applyDefaults(cfg)
	applyEnvOverrides(cfg)

	return cfg, nil
}

// LoadFromBytes parses YAML config from raw bytes (for validation without file I/O).
func LoadFromBytes(data []byte) (*Config, error) {
	// F-C2-1: parse RAW bytes — see loadAndParse. os.ExpandEnv would corrupt a bcrypt password_hash.
	cfg := &Config{}
	if err := yaml.Unmarshal(data, cfg); err != nil {
		return nil, fmt.Errorf("parsing config: %w", err)
	}
	applyDefaults(cfg)
	applyEnvOverrides(cfg)
	if err := validate(cfg); err != nil {
		return nil, err
	}
	return cfg, nil
}

// FileHash returns the SHA256 hex digest of the config file at the given path.
func FileHash(path string) (string, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return "", err
	}
	h := sha256.Sum256(data)
	return hex.EncodeToString(h[:]), nil
}

func applyDefaults(cfg *Config) {
	d := func(val *string, def string) {
		if *val == "" {
			*val = def
		}
	}
	di := func(val *int, def int) {
		if *val == 0 {
			*val = def
		}
	}

	d(&cfg.Paths.StacksDir, "/opt/docker/stacks")
	d(&cfg.Paths.DataDir, "/opt/docker/felhom-controller/data")
	d(&cfg.Paths.SystemDataPath, "/mnt/sys_drive")
	d(&cfg.Web.Listen, ":8080")
	d(&cfg.Web.SetupListen, ":8081")
	d(&cfg.Git.Branch, "main")
	d(&cfg.Git.SyncInterval, "15m")
	d(&cfg.Stacks.UpdateWindow, "03:00-05:00")
	d(&cfg.Backup.DBDumpSchedule, "02:30")
	d(&cfg.Backup.ResticSchedule, "03:00")
	d(&cfg.Backup.PruneSchedule, "weekly")
	di(&cfg.Backup.Retention.KeepDaily, 7)
	di(&cfg.Backup.Retention.KeepWeekly, 4)
	di(&cfg.Backup.Retention.KeepMonthly, 6)
	d(&cfg.Backup.ResticPasswordFile, "/opt/docker/felhom-controller/data/restic-password")
	d(&cfg.Monitoring.HealthchecksBase, "https://status.felhom.eu")
	d(&cfg.Monitoring.HealthCheckSchedule, "06:00")
	d(&cfg.Monitoring.SystemHealthInterval, "5m")
	di(&cfg.Monitoring.Thresholds.DiskWarnPercent, 80)
	di(&cfg.Monitoring.Thresholds.DiskCritPercent, 90)
	di(&cfg.Monitoring.Thresholds.BackupMaxAgeHours, 36)
	di(&cfg.Monitoring.Thresholds.CPUWarnPercent, 90)
	di(&cfg.Monitoring.Thresholds.MemoryWarnPercent, 85)
	di(&cfg.Monitoring.Thresholds.TemperatureWarnCelsius, 75)
	d(&cfg.Hub.PushInterval, "15m")
	d(&cfg.SelfUpdate.CheckInterval, "6h")
	d(&cfg.SelfUpdate.Image, "gitea.dooplex.hu/admin/felhom-controller")
	d(&cfg.SelfUpdate.AutoUpdateTime, "04:30")
	di(&cfg.SelfUpdate.HealthTimeoutSeconds, 60)
	d(&cfg.Logging.Level, "info")
	di(&cfg.Logging.MaxSizeMB, 10)
	di(&cfg.Logging.MaxFiles, 3)
	d(&cfg.Assets.SourceURL, "https://felhom.eu")
	d(&cfg.Assets.SyncSchedule, "05:00")
	di(&cfg.System.ReservedMemoryMB, 384)
	d(&cfg.Quiesce.PollInterval, "5m")
	d(&cfg.Quiesce.StatusPoll, "10s")
	d(&cfg.Quiesce.MaxQuiesce, "30m")
	d(&cfg.MailRelay.PlainListen, ":2525")
	d(&cfg.MailRelay.TLSListen, ":2465")
	d(&cfg.MailRelay.PlainNoTLSListen, ":2526")
	d(&cfg.MailRelay.ShimHost, "felhom-controller")
	if len(cfg.MailRelay.FromDomains) == 0 {
		cfg.MailRelay.FromDomains = []string{"felhom.eu"}
	}
}

func applyEnvOverrides(cfg *Config) {
	envStr := func(key string, target *string) {
		if v := os.Getenv(key); v != "" {
			*target = v
		}
	}
	envStr("FELHOM_CUSTOMER_ID", &cfg.Customer.ID)
	envStr("FELHOM_CUSTOMER_DOMAIN", &cfg.Customer.Domain)
	envStr("FELHOM_WEB_LISTEN", &cfg.Web.Listen)
	envStr("FELHOM_WEB_PASSWORD_HASH", &cfg.Web.PasswordHash)
	envStr("FELHOM_PATHS_STACKS_DIR", &cfg.Paths.StacksDir)
	envStr("FELHOM_PATHS_HDD_PATH", &cfg.Paths.HDDPath)
	envStr("FELHOM_LOGGING_LEVEL", &cfg.Logging.Level)
	envStr("FELHOM_MONITORING_SYSTEM_HEALTH_INTERVAL", &cfg.Monitoring.SystemHealthInterval)
}

func validate(cfg *Config) error {
	var errs []string

	if cfg.Customer.ID == "" {
		errs = append(errs, "customer.id is required")
	}
	if cfg.Customer.Domain == "" {
		errs = append(errs, "customer.domain is required")
	}

	switch cfg.Logging.Level {
	case "debug", "info", "warn", "error":
	default:
		errs = append(errs, fmt.Sprintf("logging.level must be debug|info|warn|error, got %q", cfg.Logging.Level))
	}

	if cfg.Monitoring.Thresholds.DiskWarnPercent >= cfg.Monitoring.Thresholds.DiskCritPercent {
		errs = append(errs, "disk_warn_percent must be less than disk_crit_percent")
	}

	if len(errs) > 0 {
		return fmt.Errorf("validation errors:\n  - %s", strings.Join(errs, "\n  - "))
	}

	return nil
}

// IsProtectedStack checks if a stack name is in the protected list.
func (cfg *Config) IsProtectedStack(name string) bool {
	for _, p := range cfg.Stacks.Protected {
		if strings.EqualFold(p, name) {
			return true
		}
	}
	return false
}

// AppLogoURL returns the primary logo URL (SVG). Use AppLogoPNGURL as fallback.
func (cfg *Config) AppLogoURL(slug string) string {
	return fmt.Sprintf("/static/assets/%s-logo.svg", slug)
}

// AppLogoPNGURL returns the PNG fallback logo URL.
func (cfg *Config) AppLogoPNGURL(slug string) string {
	return fmt.Sprintf("/static/assets/%s-logo.png", slug)
}

// AppScreenshotURL returns the local URL for an app's screenshot.
func (cfg *Config) AppScreenshotURL(slug string, index int) string {
	return fmt.Sprintf("/static/assets/%s-screenshot-%d.webp", slug, index)
}

// AppPageURL returns the URL for an app's detail page.
// This links to the local controller-hosted app detail page.
func (cfg *Config) AppPageURL(slug string) string {
	return fmt.Sprintf("/apps/%s", slug)
}
