package config

import (
	"crypto/rand"
	"encoding/hex"
	"fmt"
	"log"
	"os"
	"path/filepath"
	"runtime"
	"strings"

	"github.com/BurntSushi/toml"
)

// DatabaseConfig holds database connection settings.
type DatabaseConfig struct {
	Driver string `toml:"driver"`
	DSN    string `toml:"dsn"`
}

// StorageConfig holds file storage paths.
type StorageConfig struct {
	BaseDir   string `toml:"base_dir"`
	AttachDir string `toml:"attach_dir"`
}

// WebConfig holds web server settings.
type WebConfig struct {
	Addr string `toml:"addr"`
	// SecretKey 是 Web 会话 cookie 的签名密钥。留空时首次启动自动生成
	// 随机密钥并持久化到配置文件；也可通过环境变量 MAILGO_SECRET_KEY
	// 覆盖（覆盖值不落盘，适合容器部署）。
	SecretKey string `toml:"secret_key"`
	// CookieSecure 控制会话 cookie 是否仅通过 HTTPS 传输（Secure 标志）。
	// 默认 true；仅当应用直接以 HTTP 提供服务（本地调试、内网明文）时
	// 才应改为 false。
	CookieSecure bool `toml:"cookie_secure"`
	// ProtocolLogKeepDays SMTP/IMAP/POP3 协议调用日志保留天数，
	// 超过该天数的记录会被后台任务自动清理。
	ProtocolLogKeepDays int `toml:"protocol_log_keep_days"`
}

// SecretKeyEnvVar 是覆盖会话签名密钥的环境变量名。
const SecretKeyEnvVar = "MAILGO_SECRET_KEY"

// InsecureLegacySecretKey 是旧版本硬编码在源码中的会话签名密钥。
// 源码公开意味着该密钥完全不可信，任何出现都必须替换。
const InsecureLegacySecretKey = "mail-go-secret-key-change-in-production"

// MinSecretKeyLen 是允许的最短会话密钥长度（字节）。
const MinSecretKeyLen = 16

// secretKeyRandomBytes 是自动生成密钥的随机字节数（hex 编码后 64 字符）。
const secretKeyRandomBytes = 32

// SMTPConfig holds SMTP server settings.
type SMTPConfig struct {
	Addr           string `toml:"addr"`
	TLSAddr        string `toml:"tls_addr"`
	SubmissionAddr string `toml:"submission_addr"`
	Domain         string `toml:"domain"`
	TLSCert        string `toml:"tls_cert"`
	TLSKey         string `toml:"tls_key"`
	MaxMessage     int64  `toml:"max_message_bytes"`
}

// IMAPConfig holds IMAP server settings.
type IMAPConfig struct {
	Addr    string `toml:"addr"`
	TLSAddr string `toml:"tls_addr"`
	TLSCert string `toml:"tls_cert"`
	TLSKey  string `toml:"tls_key"`
}

// POP3Config holds POP3 server settings.
type POP3Config struct {
	Addr    string `toml:"addr"`
	TLSAddr string `toml:"tls_addr"`
	TLSCert string `toml:"tls_cert"`
	TLSKey  string `toml:"tls_key"`
}

// AuthConfig holds external authentication settings (OAuth2, LDAP).
type AuthConfig struct {
	// OAuth2 configuration
	OAuth2Enabled      bool   `toml:"oauth2_enabled"`
	OAuth2Provider     string `toml:"oauth2_provider"` // google, github, gitlab
	OAuth2ClientID     string `toml:"oauth2_client_id"`
	OAuth2ClientSecret string `toml:"oauth2_client_secret"`
	OAuth2RedirectURL  string `toml:"oauth2_redirect_url"`

	// LDAP configuration
	LDAPEnabled      bool   `toml:"ldap_enabled"`
	LDAPServer       string `toml:"ldap_server"`  // e.g. ldap://localhost:389
	LDAPBindDN       string `toml:"ldap_bind_dn"` // e.g. cn=admin,dc=example,dc=com
	LDAPBindPassword string `toml:"ldap_bind_password"`
	LDAPSearchBase   string `toml:"ldap_search_base"`   // e.g. ou=users,dc=example,dc=com
	LDAPSearchFilter string `toml:"ldap_search_filter"` // e.g. (uid=%s)
	LDAPUseTLS       bool   `toml:"ldap_use_tls"`
}

// BanConfig holds IP ban settings for failed login attempts.
type BanConfig struct {
	MaxFailAttempts int `toml:"max_fail_attempts"` // Default: 5
	BanDurationMin  int `toml:"ban_duration_min"`  // Default: 30 (minutes)
}

// CaddyConfig holds settings for importing TLS certificates from a local Caddy.
type CaddyConfig struct {
	// DataDir is the Caddy data directory (the one containing the
	// "certificates/" subdirectory), used by the one-click certificate
	// import in the admin panel. Leave empty to auto-detect common
	// locations such as /var/lib/caddy/.local/share/caddy.
	DataDir string `toml:"data_dir"`
}

// OutboundConfig holds outbound (external) mail delivery settings.
type OutboundConfig struct {
	Hostname       string `toml:"hostname"`        // EHLO 主机名，留空使用 [smtp] domain
	PollInterval   int    `toml:"poll_interval"`   // 队列扫描间隔（秒）
	MaxAttempts    int    `toml:"max_attempts"`    // 单封邮件最大投递尝试次数
	RetryBaseMin   int    `toml:"retry_base_min"`  // 重试退避基数（分钟），指数增长
	MaxRecipients  int    `toml:"max_recipients"`  // 单封邮件最大外部收件人数
	MaxPerMin      int    `toml:"max_per_min"`     // 每用户每分钟最大外发数
	MaxPerDay      int    `toml:"max_per_day"`     // 每用户每日最大外发数，0 表示禁用外部投递
	ConnectTimeout int    `toml:"connect_timeout"` // 连接远程 MX 超时（秒）

	// Workers 并发投递 worker 数：多 goroutine 并行发送队列中的邮件。
	// 0 或 1 表示串行（旧行为）。
	Workers int `toml:"workers"`
	// BatchSize 每次扫描最多取出的待投递邮件数。
	BatchSize int `toml:"batch_size"`
	// MaxConcurrentPerDomain 同一收件域（或中继）的最大并发连接数，
	// 防止对单个 MX 域并发过多而被判定为滥发；0 表示不限制。
	MaxConcurrentPerDomain int `toml:"max_concurrent_per_domain"`

	// Smarthost relay: when relay_host is non-empty, all external mail is
	// delivered through this relay instead of direct MX delivery. Useful when
	// the server IP is listed in PBL/blocklists (residential/dynamic IPs).
	RelayHost     string `toml:"relay_host"`     // 中继服务器地址，留空则直投 MX
	RelayPort     int    `toml:"relay_port"`     // 465 = 隐式 TLS，其他端口先尝试 STARTTLS
	RelayUser     string `toml:"relay_user"`     // 中继认证用户名（AUTH PLAIN）
	RelayPassword string `toml:"relay_password"` // 中继认证密码
	RelayStartTLS bool   `toml:"relay_starttls"` // 非 465 端口是否使用 STARTTLS
	// RelayTLSInsecure 是否跳过中继服务器的 TLS 证书验证。
	// 默认 false（验证证书），避免凭据被中间人截获；仅当使用自签证书的
	// 内网中继且明确知晓风险时才设为 true。
	RelayTLSInsecure bool `toml:"relay_tls_insecure"`

	// IP family and source address binding for outbound connections.
	IPFamily string `toml:"ip_family"` // ipv4（默认，PTR/SPF 最可靠）| ipv6 | auto
	SourceIP string `toml:"source_ip"` // 出站源地址绑定（如静态 IPv6），留空由内核选择
}

// Config is the top-level configuration structure.
type Config struct {
	Database DatabaseConfig `toml:"database"`
	Storage  StorageConfig  `toml:"storage"`
	Web      WebConfig      `toml:"web"`
	SMTP     SMTPConfig     `toml:"smtp"`
	IMAP     IMAPConfig     `toml:"imap"`
	POP3     POP3Config     `toml:"pop3"`
	Auth     AuthConfig     `toml:"auth"`
	Ban      BanConfig      `toml:"ban"`
	Caddy    CaddyConfig    `toml:"caddy"`
	Outbound OutboundConfig `toml:"outbound"`
}

// isWindows returns true if the current OS is Windows.
func isWindows() bool {
	return runtime.GOOS == "windows"
}

// etcDir returns the etc directory based on the current OS.
func etcDir() string {
	if isWindows() {
		return WinEtcDir
	}
	return LinuxEtcDir
}

// baseDir returns the base data directory based on the current OS.
func baseDir() string {
	if isWindows() {
		return WinBaseDir
	}
	return LinuxBaseDir
}

// defaultDSN returns the default database DSN based on the current OS.
func defaultDSN() string {
	if isWindows() {
		return DefaultDSNWin
	}
	return DefaultDSNLinux
}

// defaultConfig returns a fully populated Config with default values.
func defaultConfig() *Config {
	bd := baseDir()
	return &Config{
		Database: DatabaseConfig{
			Driver: DefaultDBDriver,
			DSN:    defaultDSN(),
		},
		Storage: StorageConfig{
			BaseDir:   bd,
			AttachDir: filepath.Join(bd, "attachments"),
		},
		Web: WebConfig{
			Addr:                DefaultWebPort,
			CookieSecure:        true,
			ProtocolLogKeepDays: DefaultProtocolLogKeepDays,
		},
		SMTP: SMTPConfig{
			Addr:           fmt.Sprintf(":%d", DefaultSMTPPort),
			TLSAddr:        fmt.Sprintf(":%d", DefaultSMTPTLSPort),
			SubmissionAddr: fmt.Sprintf(":%d", DefaultSMTPSubmitPort),
			Domain:         "localhost",
			MaxMessage:     64 * 1024 * 1024, // 64MB
		},
		IMAP: IMAPConfig{
			Addr:    fmt.Sprintf(":%d", DefaultIMAPPort),
			TLSAddr: fmt.Sprintf(":%d", DefaultIMAPTLSPort),
		},
		POP3: POP3Config{
			Addr:    fmt.Sprintf(":%d", DefaultPOP3Port),
			TLSAddr: fmt.Sprintf(":%d", DefaultPOP3TLSPort),
		},
		Auth: AuthConfig{
			OAuth2Enabled: false,
			LDAPEnabled:   false,
		},
		Ban: BanConfig{
			MaxFailAttempts: 5,
			BanDurationMin:  30,
		},
		// Caddy: 留空则自动探测常见数据目录，无需配置
		Caddy: CaddyConfig{},
		Outbound: OutboundConfig{
			PollInterval:           15, // 15 秒扫描一次队列
			MaxAttempts:            12, // 最多尝试 12 次
			RetryBaseMin:           5,  // 5/10/20/40/... 分钟指数退避
			MaxRecipients:          50, // 单封最多 50 个外部收件人
			MaxPerMin:              30, // 每用户每分钟 30 封
			MaxPerDay:              500,
			ConnectTimeout:         30,  // 连接远程 MX 超时 30 秒
			RelayPort:              587, // smarthost 默认提交端口
			RelayStartTLS:          true,
			IPFamily:               "ipv4",
			Workers:                DefaultOutboundWorkers,
			BatchSize:              DefaultOutboundBatchSize,
			MaxConcurrentPerDomain: DefaultMaxConcurrentPerDomain,
		},
	}
}

// configFilePath returns the full path to the configuration file.
func configFilePath() string {
	return filepath.Join(etcDir(), ConfigFileName)
}

// mergeDefaults overlays default values onto the loaded config for any zero/empty fields.
func mergeDefaults(cfg *Config, defaults *Config) *Config {
	if cfg.Database.Driver == "" {
		cfg.Database.Driver = defaults.Database.Driver
	}
	if cfg.Database.DSN == "" {
		cfg.Database.DSN = defaults.Database.DSN
	}
	if cfg.Storage.BaseDir == "" {
		cfg.Storage.BaseDir = defaults.Storage.BaseDir
	}
	if cfg.Storage.AttachDir == "" {
		cfg.Storage.AttachDir = defaults.Storage.AttachDir
	}
	if cfg.Web.Addr == "" {
		cfg.Web.Addr = defaults.Web.Addr
	}
	if cfg.Web.ProtocolLogKeepDays == 0 {
		cfg.Web.ProtocolLogKeepDays = defaults.Web.ProtocolLogKeepDays
	}
	if cfg.SMTP.Addr == "" {
		cfg.SMTP.Addr = defaults.SMTP.Addr
	}
	if cfg.SMTP.TLSAddr == "" {
		cfg.SMTP.TLSAddr = defaults.SMTP.TLSAddr
	}
	if cfg.SMTP.SubmissionAddr == "" {
		cfg.SMTP.SubmissionAddr = defaults.SMTP.SubmissionAddr
	}
	if cfg.SMTP.Domain == "" {
		cfg.SMTP.Domain = defaults.SMTP.Domain
	}
	if cfg.SMTP.MaxMessage == 0 {
		cfg.SMTP.MaxMessage = defaults.SMTP.MaxMessage
	}
	if cfg.IMAP.Addr == "" {
		cfg.IMAP.Addr = defaults.IMAP.Addr
	}
	if cfg.IMAP.TLSAddr == "" {
		cfg.IMAP.TLSAddr = defaults.IMAP.TLSAddr
	}
	if cfg.POP3.Addr == "" {
		cfg.POP3.Addr = defaults.POP3.Addr
	}
	if cfg.POP3.TLSAddr == "" {
		cfg.POP3.TLSAddr = defaults.POP3.TLSAddr
	}
	// Auth defaults: no merging needed since booleans default to false
	// and string fields are intentionally empty when disabled
	if cfg.Ban.MaxFailAttempts == 0 {
		cfg.Ban.MaxFailAttempts = defaults.Ban.MaxFailAttempts
	}
	if cfg.Ban.BanDurationMin == 0 {
		cfg.Ban.BanDurationMin = defaults.Ban.BanDurationMin
	}
	if cfg.Outbound.PollInterval == 0 {
		cfg.Outbound.PollInterval = defaults.Outbound.PollInterval
	}
	if cfg.Outbound.MaxAttempts == 0 {
		cfg.Outbound.MaxAttempts = defaults.Outbound.MaxAttempts
	}
	if cfg.Outbound.RetryBaseMin == 0 {
		cfg.Outbound.RetryBaseMin = defaults.Outbound.RetryBaseMin
	}
	if cfg.Outbound.MaxRecipients == 0 {
		cfg.Outbound.MaxRecipients = defaults.Outbound.MaxRecipients
	}
	if cfg.Outbound.MaxPerMin == 0 {
		cfg.Outbound.MaxPerMin = defaults.Outbound.MaxPerMin
	}
	if cfg.Outbound.MaxPerDay == 0 {
		cfg.Outbound.MaxPerDay = defaults.Outbound.MaxPerDay
	}
	if cfg.Outbound.ConnectTimeout == 0 {
		cfg.Outbound.ConnectTimeout = defaults.Outbound.ConnectTimeout
	}
	if cfg.Outbound.Workers == 0 {
		cfg.Outbound.Workers = defaults.Outbound.Workers
	}
	if cfg.Outbound.BatchSize == 0 {
		cfg.Outbound.BatchSize = defaults.Outbound.BatchSize
	}
	if cfg.Outbound.MaxConcurrentPerDomain == 0 {
		cfg.Outbound.MaxConcurrentPerDomain = defaults.Outbound.MaxConcurrentPerDomain
	}
	if cfg.Outbound.RelayPort == 0 {
		cfg.Outbound.RelayPort = defaults.Outbound.RelayPort
	}
	if cfg.Outbound.IPFamily == "" {
		cfg.Outbound.IPFamily = defaults.Outbound.IPFamily
	}
	return cfg
}

// generateSecretKey generates a cryptographically random session key,
// hex-encoded (64 characters for 32 random bytes).
func generateSecretKey() (string, error) {
	buf := make([]byte, secretKeyRandomBytes)
	if _, err := rand.Read(buf); err != nil {
		return "", fmt.Errorf("生成随机会话密钥失败: %w", err)
	}
	return hex.EncodeToString(buf), nil
}

// ensureSecretKey guarantees that cfg holds a trustworthy session key:
// an empty value or the known insecure legacy default is replaced with a
// freshly generated random key. The caller is responsible for persisting
// the updated config.
func ensureSecretKey(cfg *Config) error {
	if cfg.Web.SecretKey != "" && cfg.Web.SecretKey != InsecureLegacySecretKey {
		return nil
	}
	key, err := generateSecretKey()
	if err != nil {
		return err
	}
	if cfg.Web.SecretKey == InsecureLegacySecretKey {
		log.Printf("检测到不安全的旧默认会话密钥，已自动更换为随机密钥（所有已登录会话将失效）")
	} else {
		log.Printf("已生成随机会话密钥并写入配置文件（Web 会话签名密钥，请妥善备份）")
	}
	cfg.Web.SecretKey = key
	return nil
}

// applySecretKeyEnv lets the MAILGO_SECRET_KEY environment variable
// override the key loaded from the config file. The override value is
// never persisted to disk.
func applySecretKeyEnv(cfg *Config) *Config {
	if env := os.Getenv(SecretKeyEnvVar); env != "" {
		cfg.Web.SecretKey = env
	}
	return cfg
}

// ValidateSecretKey rejects session signing keys that are missing, too
// short, or the known insecure legacy default hardcoded in old versions.
func ValidateSecretKey(key string) error {
	if key == "" {
		return fmt.Errorf("Web 会话密钥为空，拒绝启动：请检查配置文件 [web].secret_key 或环境变量 %s", SecretKeyEnvVar)
	}
	if key == InsecureLegacySecretKey {
		return fmt.Errorf("Web 会话密钥为已知不安全的旧默认值，拒绝启动：请删除配置文件 [web].secret_key 后重启以自动生成随机密钥")
	}
	if len(key) < MinSecretKeyLen {
		return fmt.Errorf("Web 会话密钥过短（%d 字节，最少 %d）：请检查 %s 或 [web].secret_key",
			len(key), MinSecretKeyLen, SecretKeyEnvVar)
	}
	return nil
}

// writeConfig writes the configuration to the given file path.
// It creates the parent directories if they don't exist.
func writeConfig(path string, cfg *Config) error {
	dir := filepath.Dir(path)
	if err := os.MkdirAll(dir, 0755); err != nil {
		return fmt.Errorf("创建配置目录失败 %s: %w", dir, err)
	}

	f, err := os.Create(path)
	if err != nil {
		return fmt.Errorf("创建配置文件失败 %s: %w", path, err)
	}
	defer f.Close()

	enc := toml.NewEncoder(f)
	if err := enc.Encode(cfg); err != nil {
		return fmt.Errorf("写入配置文件失败: %w", err)
	}

	// 配置文件包含会话密钥、中继密码等敏感信息，收紧为仅属主可读写
	if err := os.Chmod(path, 0600); err != nil {
		return fmt.Errorf("设置配置文件权限失败 %s: %w", path, err)
	}
	return nil
}

// LoadConfig loads the configuration from disk.
// If the configuration file does not exist, it creates one with default values.
// If the file exists but has missing fields, they are filled with defaults and the file is updated.
func LoadConfig() (*Config, error) {
	return loadConfigFrom(configFilePath())
}

// loadConfigFrom implements LoadConfig against an explicit file path so it
// can be unit-tested with temporary directories.
func loadConfigFrom(path string) (*Config, error) {
	defaults := defaultConfig()

	// If config file doesn't exist, create it with defaults
	if _, err := os.Stat(path); os.IsNotExist(err) {
		if err := ensureSecretKey(defaults); err != nil {
			return nil, err
		}
		if mkErr := writeConfig(path, defaults); mkErr != nil {
			return nil, mkErr
		}
		return applySecretKeyEnv(defaults), nil
	}

	// Read existing config file
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, fmt.Errorf("读取配置文件失败 %s: %w", path, err)
	}

	cfg := &Config{}
	if err := toml.Unmarshal(data, cfg); err != nil {
		return nil, fmt.Errorf("解析配置文件失败: %w", err)
	}

	// relay_starttls 与 web.cookie_secure 默认值为 true for safety;
	// the raw file is checked because TOML decoding cannot distinguish
	// an absent bool from false.
	if !strings.Contains(string(data), "relay_starttls") {
		cfg.Outbound.RelayStartTLS = defaults.Outbound.RelayStartTLS
	}
	if !strings.Contains(string(data), "cookie_secure") {
		cfg.Web.CookieSecure = defaults.Web.CookieSecure
	}

	// 会话密钥缺失或不安全时补发随机密钥（随下面的写回一并落盘）
	if err := ensureSecretKey(cfg); err != nil {
		return nil, err
	}

	// Merge defaults for any missing fields
	merged := mergeDefaults(cfg, defaults)

	// Write back if any fields were filled in from defaults
	// (always write back to ensure the file has all fields)
	if writeErr := writeConfig(path, merged); writeErr != nil {
		return nil, writeErr
	}

	return applySecretKeyEnv(merged), nil
}
