// SPDX-License-Identifier: GPL-3.0-or-later

// Package config provides configuration management for the Private Archive client backend.
package config

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

	"github.com/spf13/viper"
)

// Loader handles loading configuration from files and environment.
type Loader struct {
	viper      *viper.Viper
	configDir  string
	configName string
}

// NewLoader creates a new configuration loader.
func NewLoader() *Loader {
	return &Loader{
		viper:      viper.New(),
		configName: "config",
	}
}

// WithConfigDir sets the directory to search for config files.
func (l *Loader) WithConfigDir(dir string) *Loader {
	l.configDir = dir
	return l
}

// WithConfigName sets the config file name (without extension).
func (l *Loader) WithConfigName(name string) *Loader {
	l.configName = name
	return l
}

// Load loads configuration from file and environment.
// Priority: Environment > Config file > Defaults
func (l *Loader) Load() (*Config, error) {
	// Start with defaults
	cfg := DefaultConfig()

	// Set up viper
	l.viper.SetConfigName(l.configName)
	l.viper.SetConfigType("yaml")

	// Environment variable handling. Set up BEFORE the config search paths so that
	// an env-overridden paths.config_dir is resolvable and can itself be added as a
	// search path below.
	l.viper.SetEnvPrefix("PRIVATEBACKUP")
	l.viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
	l.viper.AutomaticEnv()

	// Bind specific environment variables to config keys
	// This is needed because viper's AutomaticEnv doesn't work well with nested structs
	envBindings := map[string]string{
		"server.address":               "PRIVATEBACKUP_SERVER_ADDRESS",
		"server.timeout":               "PRIVATEBACKUP_SERVER_TIMEOUT",
		"server.health_check_interval": "PRIVATEBACKUP_SERVER_HEALTH_CHECK_INTERVAL",
		"tor.data_dir":                 "PRIVATEBACKUP_TOR_DATA_DIR",
		"tor.connect_timeout":          "PRIVATEBACKUP_TOR_CONNECT_TIMEOUT",
		"tor.circuit_timeout":          "PRIVATEBACKUP_TOR_CIRCUIT_TIMEOUT",
		"tor.external_socks":           "PRIVATEBACKUP_TOR_EXTERNAL_SOCKS",
		"api.listen":                   "PRIVATEBACKUP_API_LISTEN",
		"api.auth_enabled":             "PRIVATEBACKUP_API_AUTH_ENABLED",
		"api.auth_token":               "PRIVATEBACKUP_API_AUTH_TOKEN",
		"performance.gomaxprocs":       "PRIVATEBACKUP_PERFORMANCE_GOMAXPROCS",
		"performance.max_connections":  "PRIVATEBACKUP_PERFORMANCE_MAX_CONNECTIONS",
		"performance.compression":      "PRIVATEBACKUP_PERFORMANCE_COMPRESSION",
		"paths.config_dir":             "PRIVATEBACKUP_PATHS_CONFIG_DIR",
		"paths.key_file":               "PRIVATEBACKUP_PATHS_KEY_FILE",
		"paths.cache_dir":              "PRIVATEBACKUP_PATHS_CACHE_DIR",
		"paths.log_file":               "PRIVATEBACKUP_PATHS_LOG_FILE",
		"paths.web_dir":                "PRIVATEBACKUP_PATHS_WEB_DIR",
		"updates.auto_update":          "PRIVATEBACKUP_UPDATES_AUTO_UPDATE",
		"updates.channel":              "PRIVATEBACKUP_UPDATES_CHANNEL",
		"updates.manifest_url":         "PRIVATEBACKUP_UPDATES_MANIFEST_URL",
		"updates.poll_interval":        "PRIVATEBACKUP_UPDATES_POLL_INTERVAL",
		// backup.* must be bound too — without this, AutomaticEnv drops the nested
		// backup keys on Unmarshal and documented settings like backup.auto_prune
		// and backup.mode are silently ignored by the daemon (which uses Load()).
		"backup.mode":                  "PRIVATEBACKUP_BACKUP_MODE",
		"backup.auto_prune":            "PRIVATEBACKUP_BACKUP_AUTO_PRUNE",
		"backup.missing_path_behavior": "PRIVATEBACKUP_BACKUP_MISSING_PATH_BEHAVIOR",
	}

	for key, env := range envBindings {
		_ = l.viper.BindEnv(key, env)
	}

	// Add config search paths in priority order (viper uses the first match):
	//  1. WithConfigDir, when set explicitly.
	//  2. The env-overridden paths.config_dir. The deployed daemon points
	//     PRIVATEBACKUP_PATHS_CONFIG_DIR at the directory holding its mounted
	//     config.yaml but starts without a --config flag (so WithConfigDir is
	//     unset); without searching this dir the file is never read and
	//     server.address silently falls back to the default onion — which masked
	//     the bug where backup.auto_prune:false from the file had no effect.
	//     Resolvable here because the env binding above is already in place.
	//  3. The built-in default config dir.
	//  4. The current working directory.
	if l.configDir != "" {
		l.viper.AddConfigPath(l.configDir)
	}
	if envConfigDir := expandPath(l.viper.GetString("paths.config_dir")); envConfigDir != "" {
		l.viper.AddConfigPath(envConfigDir)
	}
	l.viper.AddConfigPath(cfg.Paths.ConfigDir)
	l.viper.AddConfigPath(".")

	// Try to read config file (not an error if not found)
	if err := l.viper.ReadInConfig(); err != nil {
		if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
			return nil, fmt.Errorf("error reading config file: %w", err)
		}
		// Config file not found is OK - use defaults
	}

	// Unmarshal into config struct
	if err := l.viper.Unmarshal(cfg); err != nil {
		return nil, fmt.Errorf("error unmarshaling config: %w", err)
	}

	// Viper's Unmarshal silently drops nested []string fields inside slices-of-structs
	// (known limitation in v1.x). Re-unmarshal the schedules key directly.
	if err := l.viper.UnmarshalKey("schedules", &cfg.Schedules); err != nil {
		return nil, fmt.Errorf("error unmarshaling schedules config: %w", err)
	}

	// The same viper nested-struct limitation silently drops the backup.* keys
	// (auto_prune, mode, retention, paths) on the main Unmarshal above — only the
	// explicitly env-bound sections (server, tor, api, paths, updates) survive it.
	// As a result the documented backup settings in the config file were ignored
	// (e.g. auto_prune: false had no effect). Re-unmarshal backup directly so the
	// config file is honored. Overlays the file's keys onto the defaults, leaving
	// unset fields at their DefaultConfig values.
	if err := l.viper.UnmarshalKey("backup", &cfg.Backup); err != nil {
		return nil, fmt.Errorf("error unmarshaling backup config: %w", err)
	}

	// Expand paths
	cfg.Paths.ConfigDir = expandPath(cfg.Paths.ConfigDir)
	cfg.Paths.KeyFile = expandPath(cfg.Paths.KeyFile)
	cfg.Paths.CacheDir = expandPath(cfg.Paths.CacheDir)
	cfg.Paths.LogFile = expandPath(cfg.Paths.LogFile)
	cfg.Tor.DataDir = expandPath(cfg.Tor.DataDir)

	return cfg, nil
}

// LoadFromFile loads configuration from a specific file.
func (l *Loader) LoadFromFile(path string) (*Config, error) {
	// Start with defaults
	cfg := DefaultConfig()

	// Set up viper for the specific file
	l.viper.SetConfigFile(path)

	// Read the config file
	if err := l.viper.ReadInConfig(); err != nil {
		return nil, fmt.Errorf("error reading config file: %w", err)
	}

	// Unmarshal into config struct
	if err := l.viper.Unmarshal(cfg); err != nil {
		return nil, fmt.Errorf("error unmarshaling config: %w", err)
	}

	// Viper's Unmarshal silently drops nested []string fields inside slices-of-structs
	// (known limitation in v1.x). Re-unmarshal the schedules key directly.
	if err := l.viper.UnmarshalKey("schedules", &cfg.Schedules); err != nil {
		return nil, fmt.Errorf("error unmarshaling schedules config: %w", err)
	}

	// The same viper nested-struct limitation silently drops the backup.* keys
	// (auto_prune, mode, retention, paths) on the main Unmarshal above — only the
	// explicitly env-bound sections (server, tor, api, paths, updates) survive it.
	// As a result the documented backup settings in the config file were ignored
	// (e.g. auto_prune: false had no effect). Re-unmarshal backup directly so the
	// config file is honored. Overlays the file's keys onto the defaults, leaving
	// unset fields at their DefaultConfig values.
	if err := l.viper.UnmarshalKey("backup", &cfg.Backup); err != nil {
		return nil, fmt.Errorf("error unmarshaling backup config: %w", err)
	}

	// Expand paths
	cfg.Paths.ConfigDir = expandPath(cfg.Paths.ConfigDir)
	cfg.Paths.KeyFile = expandPath(cfg.Paths.KeyFile)
	cfg.Paths.CacheDir = expandPath(cfg.Paths.CacheDir)
	cfg.Paths.LogFile = expandPath(cfg.Paths.LogFile)
	cfg.Tor.DataDir = expandPath(cfg.Tor.DataDir)

	return cfg, nil
}

// SaveDefault creates a default config file if one doesn't exist.
func SaveDefault(path string) error {
	// Check if file already exists
	if _, err := os.Stat(path); err == nil {
		return fmt.Errorf("config file already exists: %s", path)
	}

	// Create directory if needed
	dir := filepath.Dir(path)
	if err := os.MkdirAll(dir, 0700); err != nil {
		return fmt.Errorf("failed to create config directory: %w", err)
	}

	// Generate default config content
	content := `# Private Archive Client Backend Configuration
# See SPEC.md for detailed documentation

# Server connection
server:
  # The .onion address of the backup server (required)
  address: ""
  # Request timeout
  timeout: 5m
  # Health check interval
  health_check_interval: 5m

# Embedded Tor configuration
tor:
  # Tor data directory (cached state for faster reconnection)
  data_dir: ~/.config/privatebackup/tor
  # Connection timeout
  connect_timeout: 2m
  # Circuit timeout
  circuit_timeout: 10m
  # External SOCKS5 proxy (optional, use instead of embedded Tor)
  # If set, connects through this proxy instead of starting embedded Tor.
  # Example: external_socks: "127.0.0.1:9050"
  external_socks: ""

# Local API server
api:
  # Listen address (localhost only by default)
  listen: "127.0.0.1:62774"
  # Enable bearer token auth (for remote access)
  auth_enabled: false
  # auth_token: "your-secret-token"

# Performance settings (optimized for minimal user impact)
performance:
  # Limit CPU cores (1 = minimal impact on user's machine)
  gomaxprocs: 1
  # Maximum concurrent server connections (prevents server overload)
  max_connections: 2
  # Compression level (max = best compression, slower but smaller uploads)
  compression: max

# Backup defaults
backup:
  exclude_caches: true
  default_excludes:
    - "*.tmp"
    - "*.temp"
    - ".cache/**"
    - "**/node_modules/**"
    - "**/.git/objects/**"

# File paths
paths:
  config_dir: ~/.config/privatebackup
  key_file: ~/.config/privatebackup/privatebackup-key.pem
  cache_dir: ~/.config/privatebackup/cache
  log_file: ""  # stdout by default
  # web_dir: ""   # leave empty for API-only; set to a dist/ path to serve the web UI

# Automatic updates (recommended: leave enabled).
# Disabling means new server protocol versions may break compatibility
# until you manually install a new release.
updates:
  auto_update: true
  channel: stable
  # manifest_url: ""  # leave blank for the official endpoint
  poll_interval: 6h
`

	if err := os.WriteFile(path, []byte(content), 0600); err != nil {
		return fmt.Errorf("failed to write config file: %w", err)
	}

	return nil
}

// expandPath expands ~ and environment variables in paths.
func expandPath(path string) string {
	if path == "" {
		return ""
	}

	// Expand ~
	if strings.HasPrefix(path, "~/") {
		home, err := os.UserHomeDir()
		if err == nil {
			path = filepath.Join(home, path[2:])
		}
	}

	// Expand environment variables
	path = os.ExpandEnv(path)

	return path
}
