// Package config loads and generates settings.ini, mirroring email_server/settings_loader.py.
package config

import (
	"os"
	"path/filepath"

	"gopkg.in/ini.v1"
)

// defaultKV is one key/value pair with the comment line Python renders above it.
type defaultKV struct {
	Key     string
	Value   string
	Comment string
}

// defaults mirrors settings_loader.py's DEFAULTS table section-by-section, in order.
// The [Attachments] section does not exist in the Python defaults (a bug: it crashes
// attachment storage there) — it is added here deliberately, per the approved plan.
var defaults = []struct {
	Section string
	Keys    []defaultKV
}{
	{"Server", []defaultKV{
		{"", "", "Server configuration for SMTP ports and hostname"},
		{"", "", "Plain SMTP port for internal/whitelisted IPs"},
		{"", "", "(standard port 25 — the process needs CAP_NET_BIND_SERVICE or root to bind"},
		{"", "", "it directly; not an issue in the Docker deployment, which runs as root)"},
		{"SMTP_PORT", "25", ""},
		{"", "", "Direct/implicit-TLS SMTP port for authenticated users (like SMTPS, not STARTTLS)"},
		{"SMTP_TLS_PORT", "465", ""},
		{"", "", "Server hostname for HELO/EHLO identification"},
		{"HOSTNAME", "mail.example.com", ""},
		{"", "", "Override HELO hostname"},
		{"helo_hostname", "mail.example.com", ""},
		{"", "", `IP address to bind to (0.0.0.0 = all interfaces), on Windows must use specific IP`},
		{"BIND_IP", "0.0.0.0", ""},
		{"", "", "HTTP port for the admin web UI (overridden by the -port flag if given)"},
		{"WEB_HTTP_PORT", "5000", ""},
		{"", "", "HTTPS port for the admin web UI (self-signed by default, or the Let's Encrypt cert when enabled)"},
		{"WEB_HTTPS_PORT", "5001", ""},
		{"", "", "Port the [LetsEncryptHTTP] HTTP-01 challenge server binds while an obtain/renew is"},
		{"", "", "actually running (never left listening otherwise). Must be 80 for a real Let's"},
		{"", "", "Encrypt HTTP-01 challenge to validate - the CA always connects on port 80. Only"},
		{"", "", "change this if you're proxying/forwarding port 80 to a different port on this host."},
		{"HTTP_LETSENCRYPT_PORT", "80", ""},
		{"", "", `Custom server banner (to make it empty use "" must be double quotes)`},
		{"server_banner", "", ""},
		{"", "", "Time zone for the server"},
		{"TIME_ZONE", "Europe/London", ""},
		{"", "", `Comma-separated CIDRs and/or the word "cloudflare" -- only these peers'`},
		{"", "", "X-Forwarded-For/X-Real-IP/CF-Connecting-IP headers are trusted for the real"},
		{"", "", "client IP (audit logs, lockouts). Leave blank if nothing proxies this server."},
		{"trusted_proxies", "", ""},
	}},
	{"Database", []defaultKV{
		{"", "", "Database configuration"},
		{"DATABASE_URL", "sqlite:///server_data/smtp_server.db", ""},
	}},
	{"Logging", []defaultKV{
		{"", "", "Logging configuration"},
		{"", "", "Log level: DEBUG, INFO, WARNING, ERROR, CRITICAL"},
		{"LOG_LEVEL", "INFO", ""},
		{"", "", "Hide verbose aiosmtpd-equivalent INFO messages when LOG_LEVEL = INFO"},
		{"hide_info_aiosmtpd", "true", ""},
	}},
	{"Relay", []defaultKV{
		{"", "", "Timeout in seconds for external SMTP connections"},
		{"RELAY_TIMEOUT", "30", ""},
	}},
	{"TLS", []defaultKV{
		{"", "", "TLS/SSL certificate configuration"},
		{"", "", "The 'custom' certificate: self-signed on first run, or your own uploaded cert/key"},
		{"TLS_CERT_FILE", "ssl_certs/server.crt", ""},
		{"TLS_KEY_FILE", "ssl_certs/server.key", ""},
		{"", "", "Which certificate each TLS-serving listener uses: custom, letsencrypt_dns"},
		{"", "", "(the [LetsEncrypt] DNS-01 cert), or letsencrypt_http (the [LetsEncryptHTTP]"},
		{"", "", "HTTP-01 cert). Independent per listener - e.g. run the HTTP-01 cert on"},
		{"", "", "SMTP/IMAP while the web UI keeps a DNS-01 or custom cert, or vice versa."},
		{"smtp_tls_cert", "custom", ""},
		{"imap_tls_cert", "custom", ""},
		{"web_https_cert", "custom", ""},
	}},
	{"DKIM", []defaultKV{
		{"", "", "DKIM signing configuration"},
		{"", "", "RSA key size for DKIM keys (1024, 2048, 4096)"},
		{"DKIM_KEY_SIZE", "2048", ""},
		{"", "", "Provide Public IP address of server, used for SPF in case detection fails"},
		{"SPF_SERVER_IP", "192.168.1.1", ""},
	}},
	{"Attachments", []defaultKV{
		{"", "", "Directory where stored message attachments are written (fixed: missing in the Python defaults)"},
		{"attachments_path", "server_data/attachments", ""},
	}},
	{"Auth", []defaultKV{
		{"", "", "Admin dashboard login / passkey (WebAuthn) configuration"},
		{"", "", "Must match the domain the admin dashboard is actually accessed at — passkeys are bound to this"},
		{"rp_id", "localhost", ""},
		{"", "", "Display name shown in the authenticator/passkey prompt"},
		{"rp_display_name", "mailgoserver", ""},
		{"", "", `Full origin (scheme+host+port) the dashboard is served at, e.g. "https://mail.example.com"`},
		{"rp_origin", "http://localhost:5000", ""},
		{"", "", "Require every admin account (global or domain-scoped) to set up TOTP/passkey MFA"},
		{"enforce_admin_mfa", "false", ""},
		{"", "", "Require every mailbox's self-service webmail login to have TOTP/passkey MFA"},
		{"", "", "(overridable per-domain or per-mailbox — see the Domains/Mailboxes edit pages)"},
		{"enforce_mailbox_mfa", "false", ""},
		{"", "", "Failed login attempts (for one account, from any IP) allowed within the window below"},
		{"", "", "before that account is temporarily locked out, independent of the per-IP rate limit"},
		{"login_attempt_limit", "8", ""},
		{"login_attempt_window_minutes", "15", ""},
	}},
	{"Security", []defaultKV{
		{"", "", "Generated automatically on first run if this file doesn't exist yet — used to sign"},
		{"", "", "CSRF tokens. Back this up like any other secret; don't share it or commit it."},
		{"app_secret_path", "server_data/app_secret.key", ""},
		{"", "", "Automatically block an IP for a while after too many failed SMTP/IMAP login"},
		{"", "", "attempts within the window below (see the Blacklist page in the admin dashboard)"},
		{"abuse_detection_enabled", "true", ""},
		{"abuse_failure_threshold", "8", ""},
		{"abuse_detection_window_minutes", "10", ""},
		{"", "", "First-offense block duration; doubles on each repeat offense up to the cap below"},
		{"abuse_blacklist_base_hours", "12", ""},
		{"abuse_blacklist_max_hours", "168", ""},
	}},
	{"IMAP", []defaultKV{
		{"", "", "IMAP server configuration for mailbox retrieval (Thunderbird, etc.)"},
		{"", "", "Plain IMAP port (STARTTLS not offered, matching the SMTP plain-port design)"},
		{"IMAP_PORT", "143", ""},
		{"", "", "Implicit-TLS IMAP port (IMAPS)"},
		{"IMAP_TLS_PORT", "993", ""},
	}},
	{"Mailstore", []defaultKV{
		{"", "", "Local mailbox storage configuration"},
		{"", "", "Directory where encrypted message blobs are written"},
		{"mailstore_path", "server_data/mailstore", ""},
		{"", "", "Path to the server-held master key that wraps every mailbox's encryption key"},
		{"", "", "(generated on first run if missing; back this file up separately from the database -"},
		{"", "", "losing it makes all stored mail unrecoverable, even for admins)"},
		{"master_key_path", "server_data/mailstore_master.key", ""},
		{"", "", "Default per-mailbox storage quota in bytes (5 GiB)"},
		{"default_quota_bytes", "5368709120", ""},
		{"", "", "Minimum length for IMAP/SMTP app passwords (floor of 25 enforced regardless of this value)"},
		{"app_password_min_length", "25", ""},
		{"", "", "Reject messages scoring at or above this built-in heuristic spam score"},
		{"spam_reject_score", "5", ""},
	}},
	{"Rspamd", []defaultKV{
		{"", "", "Optional rspamd integration for spam scoring (off by default; the built-in"},
		{"", "", "heuristic score above always runs regardless of this setting)"},
		{"enabled", "false", ""},
		{"", "", "rspamd controller/worker URL"},
		{"url", "http://127.0.0.1:11333", ""},
		{"", "", "Reject messages rspamd scores at or above this threshold"},
		{"reject_score", "15", ""},
	}},
	{"LetsEncrypt", []defaultKV{
		{"", "", "Let's Encrypt (ACME, DNS-01) automatic certificate configuration. This obtains a"},
		{"", "", "separate certificate from [LetsEncryptHTTP] below - assign each independently to"},
		{"", "", "the SMTP/IMAP/web-UI listeners via [TLS]'s *_cert settings. Leave 'enabled' false"},
		{"", "", "to not obtain this one."},
		{"enabled", "false", ""},
		{"", "", "Use Let's Encrypt's staging directory (untrusted certs, no rate limits) for testing"},
		{"staging", "false", ""},
		{"", "", "Contact email for the ACME account"},
		{"contact_email", "", ""},
		{"", "", "Comma-separated domains to request, e.g. mail.example.com,*.mail.example.com"},
		{"domains", "", ""},
		{"", "", "DNS provider used to solve the DNS-01 challenge: cloudflare, route53, digitalocean, gcloud"},
		{"dns_provider", "", ""},
		{"", "", "-- Cloudflare --"},
		{"cloudflare_api_token", "", ""},
		{"", "", "-- AWS Route53 (leave access key/secret blank to use the host's default AWS credential chain) --"},
		{"route53_access_key_id", "", ""},
		{"route53_secret_access_key", "", ""},
		{"route53_region", "", ""},
		{"route53_hosted_zone_id", "", ""},
		{"", "", "-- DigitalOcean --"},
		{"digitalocean_api_token", "", ""},
		{"", "", "-- Google Cloud DNS --"},
		{"gcloud_project", "", ""},
		{"", "", "Path to an uploaded service-account JSON key; leave blank to use Application Default Credentials"},
		{"gcloud_service_account_json_path", "", ""},
	}},
	{"LetsEncryptHTTP", []defaultKV{
		{"", "", "Let's Encrypt (ACME, HTTP-01) automatic certificate configuration - an"},
		{"", "", "alternative to [LetsEncrypt] above for domains you don't manage DNS for. Needs"},
		{"", "", "no DNS provider, only port 80 reachable from the internet; that port is only"},
		{"", "", "ever bound for the few seconds an obtain/renew is actually running, never left"},
		{"", "", "listening otherwise. Produces a separate certificate from [LetsEncrypt] - assign"},
		{"", "", "each independently to the SMTP/IMAP/web-UI listeners via [TLS]'s *_cert settings."},
		{"", "", "(Both sections share the same underlying ACME account, registered once using"},
		{"", "", "whichever of the two 'contact_email' values is obtained with first.)"},
		{"enabled", "false", ""},
		{"", "", "Use Let's Encrypt's staging directory (untrusted certs, no rate limits) for testing"},
		{"staging", "false", ""},
		{"", "", "Contact email for the ACME account"},
		{"contact_email", "", ""},
		{"", "", "Comma-separated domains to request"},
		{"domains", "", ""},
		{"", "", "Also request this certificate for the server's public IP address (RFC 8738 IP"},
		{"", "", "identifier), so clients connecting by bare IP get a trusted cert too. Note: some"},
		{"", "", "CAs reject an order mixing a domain name and an IP - check this page's status"},
		{"", "", "after enabling this if the domain-only cert stops working."},
		{"include_ip", "false", ""},
		{"", "", "IP address to request the cert for. Leave blank to autodetect this host's WAN IP"},
		{"", "", "on every obtain/renew."},
		{"ip_override", "", ""},
	}},
}

// GenerateSettingsIni writes settings.ini with default values and comments if it does
// not already exist. Mirrors settings_loader.generate_settings_ini: never overwrites or
// merges into an existing file.
func GenerateSettingsIni(path string) error {
	if _, err := os.Stat(path); err == nil {
		return nil
	}
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		return err
	}

	cfg := ini.Empty()
	for _, sec := range defaults {
		section, err := cfg.NewSection(sec.Section)
		if err != nil {
			return err
		}
		for _, kv := range sec.Keys {
			if kv.Key == "" {
				// Comment-only line, e.g. a section header comment. No trailing
				// newline: ini.v1's writer splits Comment on "\n" and indexes
				// line[0] unconditionally, so a trailing separator produces an
				// empty final line and panics.
				if section.Comment != "" {
					section.Comment += "\n"
				}
				section.Comment += kv.Comment
				continue
			}
			key, err := section.NewKey(kv.Key, kv.Value)
			if err != nil {
				return err
			}
			if kv.Comment != "" {
				key.Comment = kv.Comment
			}
		}
	}
	return cfg.SaveTo(path)
}

// Load reads settings.ini at path, generating it with defaults first if missing.
// Mirrors settings_loader.load_settings: always regenerate-if-missing, then read fresh.
// Existing values are never touched (GenerateSettingsIni's "never overwritten or merged
// into" guarantee still holds), but any key added to the defaults table by a later
// version of this program — like [TLS]'s *_cert routing settings or the whole
// [LetsEncryptHTTP] section — is backfilled onto an older, already-existing file, so it
// shows up (and actually saves) on the generic Settings page instead of silently
// behaving as if unset until the file is regenerated from scratch.
func Load(path string) (*ini.File, error) {
	if err := GenerateSettingsIni(path); err != nil {
		return nil, err
	}
	cfg, err := ini.Load(path)
	if err != nil {
		return nil, err
	}
	if err := backfillMissingDefaults(cfg, path); err != nil {
		return nil, err
	}
	return cfg, nil
}

// backfillMissingDefaults adds any defaults-table key not already present in cfg,
// leaving every existing key's value untouched, and saves to path only if it actually
// added something.
func backfillMissingDefaults(cfg *ini.File, path string) error {
	changed := false
	for _, sec := range defaults {
		section, err := cfg.NewSection(sec.Section) // no-op if the section already exists
		if err != nil {
			return err
		}
		for _, kv := range sec.Keys {
			if kv.Key == "" || section.HasKey(kv.Key) {
				continue
			}
			key, err := section.NewKey(kv.Key, kv.Value)
			if err != nil {
				return err
			}
			if kv.Comment != "" {
				key.Comment = kv.Comment
			}
			changed = true
		}
	}
	if !changed {
		return nil
	}
	return cfg.SaveTo(path)
}

// AbsoluteSQLitePath converts a "sqlite:///relative/path" database URL into an absolute
// filesystem path resolved against root, mirroring app.py's _get_absolute_database_url.
func AbsoluteSQLitePath(databaseURL, root string) string {
	const prefix = "sqlite:///"
	if len(databaseURL) < len(prefix) || databaseURL[:len(prefix)] != prefix {
		return databaseURL
	}
	rel := databaseURL[len(prefix):]
	if filepath.IsAbs(rel) {
		return rel
	}
	return filepath.Join(root, rel)
}
