package config

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

	"github.com/spf13/cobra"
	"github.com/spf13/viper"
)

// outputWriter is used for logging so tests can capture output.
var outputWriter io.Writer = os.Stderr

func logf(format string, args ...interface{}) {
	_, _ = fmt.Fprintf(outputWriter, format+"\n", args...)
}

// Config holds the full application configuration.
type Config struct {
	PG struct {
		Host          string   `mapstructure:"host"`
		Port          int      `mapstructure:"port"`
		User          string   `mapstructure:"user"`
		Password      string   `mapstructure:"password"`
		Database      string   `mapstructure:"database"`
		SSLMode       string   `mapstructure:"sslmode"`
		ExcludeTables []string `mapstructure:"exclude_tables"`
	} `mapstructure:"pg"`
	Backup struct {
		Dir           string `mapstructure:"dir"`
		RetentionDays int    `mapstructure:"retention_days"`
		Cron          string `mapstructure:"cron"`
	} `mapstructure:"backup"`
	PQScheme               uint16 `mapstructure:"pq_scheme"`
	ClassicalScheme        uint16 `mapstructure:"classical_scheme"`
	PQPublicKeyPath        string `mapstructure:"pq_public_key_path"`
	ClassicalPublicKeyPath string `mapstructure:"classical_public_key_path"`
	Healthz                struct {
		Port int `mapstructure:"port"`
	} `mapstructure:"healthz"`
	Log struct {
		Level string `mapstructure:"level"`
	} `mapstructure:"log"`
	ShutdownTimeout time.Duration `mapstructure:"shutdown_timeout"`
}

// RegisterFlags adds all configuration flags to the provided Cobra command.
func RegisterFlags(cmd *cobra.Command) {
	flags := cmd.Flags()

	flags.String("pg-host", "", "PostgreSQL host")
	flags.Int("pg-port", 0, "PostgreSQL port")
	flags.String("pg-user", "", "PostgreSQL user")
	flags.String("pg-database", "", "PostgreSQL database name")
	flags.String("pg-sslmode", "", "PostgreSQL SSL mode")
	flags.StringSlice("pg-exclude-tables", nil, "PostgreSQL tables to exclude from backup")
	flags.String("backup-dir", "", "Backup directory")
	flags.Int("backup-retention-days", 0, "Backup retention period in days")
	flags.String("backup-cron", "", "Cron expression for backup schedule")
	flags.Uint16("pq-scheme", 0, "Post-quantum KEM scheme ID")
	flags.Uint16("classical-scheme", 0, "Classical KEM scheme ID")
	flags.String("pq-public-key-path", "", "Path to post-quantum public key")
	flags.String("classical-public-key-path", "", "Path to classical public key")
	flags.Int("healthz-port", 0, "Health check HTTP port")
	flags.String("log-level", "", "Log level")
	flags.Duration("shutdown-timeout", 0, "Graceful shutdown timeout")
}

// Load reads configuration from all sources in priority order:
// launch arguments (Cobra flags) → APP_* env variables → config file → defaults.
func Load(cmd *cobra.Command) (*Config, error) {
	// Use a fresh Viper instance so that successive calls do not leak state.
	v := viper.New()

	// Defaults (lowest priority).
	v.SetDefault("pg.port", 5432)
	v.SetDefault("pg.sslmode", "prefer")
	v.SetDefault("pg.exclude_tables", []string{"e2e_one_time_keys_json"})
	v.SetDefault("backup.retention_days", 180)
	v.SetDefault("backup.cron", "0 0 3 * * *")
	v.SetDefault("pq_scheme", uint16(0x0006))
	v.SetDefault("classical_scheme", uint16(0x0007))
	v.SetDefault("healthz.port", 8080)
	v.SetDefault("log.level", "info")
	v.SetDefault("shutdown_timeout", 30*time.Second)

	// Bind parsed Cobra flags to Viper keys.
	if cmd != nil {
		_ = v.BindPFlag("pg.host", cmd.Flags().Lookup("pg-host"))
		_ = v.BindPFlag("pg.port", cmd.Flags().Lookup("pg-port"))
		_ = v.BindPFlag("pg.user", cmd.Flags().Lookup("pg-user"))
		_ = v.BindPFlag("pg.database", cmd.Flags().Lookup("pg-database"))
		_ = v.BindPFlag("pg.sslmode", cmd.Flags().Lookup("pg-sslmode"))
		_ = v.BindPFlag("pg.exclude_tables", cmd.Flags().Lookup("pg-exclude-tables"))
		_ = v.BindPFlag("backup.dir", cmd.Flags().Lookup("backup-dir"))
		_ = v.BindPFlag("backup.retention_days", cmd.Flags().Lookup("backup-retention-days"))
		_ = v.BindPFlag("backup.cron", cmd.Flags().Lookup("backup-cron"))
		_ = v.BindPFlag("pq_scheme", cmd.Flags().Lookup("pq-scheme"))
		_ = v.BindPFlag("classical_scheme", cmd.Flags().Lookup("classical-scheme"))
		_ = v.BindPFlag("pq_public_key_path", cmd.Flags().Lookup("pq-public-key-path"))
		_ = v.BindPFlag("classical_public_key_path", cmd.Flags().Lookup("classical-public-key-path"))
		_ = v.BindPFlag("healthz.port", cmd.Flags().Lookup("healthz-port"))
		_ = v.BindPFlag("log.level", cmd.Flags().Lookup("log-level"))
		_ = v.BindPFlag("shutdown_timeout", cmd.Flags().Lookup("shutdown-timeout"))
	}

	// Config file search with logging.
	v.SetConfigName("config")
	v.SetConfigType("yaml")

	if envLoc := os.Getenv("APP_CONFIG_LOCATION"); envLoc != "" {
		v.SetConfigFile(envLoc)
		if err := v.ReadInConfig(); err == nil {
			logf("Config file found: %s (from APP_CONFIG_LOCATION)", v.ConfigFileUsed())
		} else {
			return nil, fmt.Errorf("config file specified in APP_CONFIG_LOCATION not found: %s", envLoc)
		}
	} else {
		v.AddConfigPath(".")

		homeDir, _ := os.UserHomeDir()
		appName := "synapse-backupper"
		userConfigPath := filepath.Join(homeDir, ".config", appName)
		v.AddConfigPath(userConfigPath)

		exePath, _ := os.Executable()
		exeDir := filepath.Dir(exePath)
		v.AddConfigPath(exeDir)

		etcPath := filepath.Join("/etc", appName)
		v.AddConfigPath(etcPath)

		if err := v.ReadInConfig(); err != nil {
			if _, ok := err.(viper.ConfigFileNotFoundError); ok {
				logf("Configuration file not found. Using defaults and env/args.")
			} else {
				return nil, fmt.Errorf("error reading config: %w", err)
			}
		} else {
			logf("Config file found: %s", v.ConfigFileUsed())
		}
	}

	// Environment variables.
	v.SetEnvPrefix("APP")
	v.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
	v.AutomaticEnv()
	// pg.password is intentionally not exposed as a CLI flag (CWE-214), but
	// must still be loadable from APP_PG_PASSWORD. Viper needs an explicit
	// BindEnv for a nested key that has no bound flag.
	_ = v.BindEnv("pg.password")

	var cfg Config
	if err := v.Unmarshal(&cfg); err != nil {
		return nil, fmt.Errorf("config unmarshal failed: %w", err)
	}

	return &cfg, nil
}
