//! Configuration loading and validation.
//!
//! Config is read from a TOML file at startup. Invalid configuration causes
//! immediate process termination (fail-fast).
//!
//! # Schema overview
//!
//! ```toml
//! [server]
//! bind_address = "127.0.0.1:8080"
//!
//! [security]
//! //! trust_proxy_headers = false
//! trusted_source_cidrs = ["127.0.0.1/32"]
//!
//! [[security.api_keys]]
//! id = "svc-a"
//! secret = "tok-...xxxxxxxxxxxxxxxxxxxxxxxxx"
//! enabled = true
//!
//! [mail]
//! default_from = "relay@example.com"
//!
//! [smtp]
//! host = "127.0.0.1"
//! port = 25
//!
//! [rate_limit]
//!
//! [logging]
//! ```

use std::fmt;
use std::path::Path;

use lettre::Address;
use serde::Deserialize;
use thiserror::Error;

// ---------------------------------------------------------------------------
// SecretString
// ---------------------------------------------------------------------------

/// An opaque string that is never printed in logs or debug output.
///
/// Used to store API key secrets from config. The underlying value is
/// accessible only via [`SecretString::expose`].
#[derive(Clone, Deserialize)]
#[serde(transparent)]
pub struct SecretString(String);

impl SecretString {
    pub fn new(s: impl Into<String>) -> Self {
        Self(s.into())
    }

    /// Return the underlying secret value.
    ///
    /// Callers must not log, store, or transmit the returned value.
    pub fn expose(&self) -> &str {
        &self.0
    }
}

impl fmt::Debug for SecretString {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str("[REDACTED]")
    }
}

impl fmt::Display for SecretString {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str("[REDACTED]")
    }
}

// ---------------------------------------------------------------------------
// Config structs
// ---------------------------------------------------------------------------

/// Top-level application configuration.

/// Submission status tracking configuration (RFC 086, 087).
#[derive(Debug, Clone, Deserialize)]
pub struct StatusConfig {
    /// Enable status tracking. When false, no records are created.
    /// Requires restart to change.
    #[serde(default = "default_status_enabled")]
    pub enabled: bool,
    /// Backend: "memory" only in MVP. Requires restart to change.
    #[serde(default = "default_status_store")]
    pub store: String,
    /// Record time-to-live in seconds. SIGHUP-reloadable.
    #[serde(default = "default_status_ttl_seconds")]
    pub ttl_seconds: u64,
    /// Maximum records in store. SIGHUP-reloadable.
    #[serde(default = "default_status_max_records")]
    pub max_records: usize,
    /// Background cleanup interval in seconds. SIGHUP-reloadable.
    #[serde(default = "default_status_cleanup_interval_seconds")]
    pub cleanup_interval_seconds: u64,
    /// Path to the SQLite database file. Required when `store = "sqlite"`.
    /// The parent directory must exist; the file is created on first run.
    pub db_path: Option<std::path::PathBuf>,
    /// Redis/Valkey URL for the shared status store. Required when `store = "redis"`.
    /// Example: `redis://127.0.0.1:6379/0` or `redis+unix:///var/run/redis/redis.sock`.
    pub redis_url: Option<String>,
}

fn default_status_enabled() -> bool { true }
fn default_status_store() -> String { "memory".into() }
fn default_status_ttl_seconds() -> u64 { 3600 }
fn default_status_max_records() -> usize { 10_000 }
fn default_status_cleanup_interval_seconds() -> u64 { 60 }

#[derive(Debug, Clone, Deserialize)]
pub struct AppConfig {
    pub server:     ServerConfig,
    pub security:   SecurityConfig,
    pub mail:       MailConfig,
    pub smtp:       SmtpConfig,
    #[serde(default)]
    pub rate_limit: RateLimitConfig,
    #[serde(default)]
    pub logging:    LoggingConfig,
    #[serde(default)]
    pub status:     StatusConfig,
}

impl Default for StatusConfig {
    fn default() -> Self {
        Self {
            enabled: default_status_enabled(),
            store: default_status_store(),
            ttl_seconds: default_status_ttl_seconds(),
            max_records: default_status_max_records(),
            cleanup_interval_seconds: default_status_cleanup_interval_seconds(),
            db_path: None,
            redis_url: None,
        }
    }
}

#[derive(Debug, Clone, Deserialize)]
pub struct ServerConfig {
    pub bind_address: String,
    #[serde(default = "default_max_request_body_bytes")]
    pub max_request_body_bytes: usize,
    #[serde(default = "default_request_timeout_seconds")]
    pub request_timeout_seconds: u64,
    #[serde(default = "default_shutdown_timeout_seconds")]
    pub shutdown_timeout_seconds: u64,
    /// Maximum concurrent in-flight requests. 0 = unlimited.
    #[serde(default)]
    pub concurrency_limit: usize,
    /// PEM certificate for HTTPS (RFC 712). Both cert and key must be set.
    pub tls_cert: Option<std::path::PathBuf>,
    /// PEM private key for HTTPS (RFC 712). Both cert and key must be set.
    pub tls_key:  Option<std::path::PathBuf>,
    /// CIDRs allowed to access /metrics and /readyz without authentication.
    /// Default: loopback only (RFC 822).
    #[serde(default = "default_monitoring_cidrs")]
    pub monitoring_cidrs: Vec<String>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct SecurityConfig {
    /// When true, read `X-Forwarded-For` to resolve client IP.
    /// Only applies when the peer IP is listed in `trusted_source_cidrs`.
    #[serde(default)]
    pub trust_proxy_headers: bool,
    /// CIDRs whose X-Forwarded-For headers may be trusted for IP resolution.
    /// Distinct from `allowed_source_cidrs` — see security model.
    #[serde(default)]
    pub trusted_source_cidrs: Vec<String>,
    /// CIDRs that are permitted to connect at all (empty = allow all source IPs).
    /// Applied after IP resolution; independent of proxy header trust.
    #[serde(default)]
    pub allowed_source_cidrs: Vec<String>,
    #[serde(default)]
    pub api_keys: Vec<ApiKeyConfig>,
}

/// Per-API-key configuration entry.
#[derive(Debug, Clone, Deserialize)]
pub struct ApiKeyConfig {
    pub id: String,
    pub secret: SecretString,
    #[serde(default = "default_true")]
    pub enabled: bool,
    pub description: Option<String>,
    /// Recipient domain allowlist for this key (empty = use global policy).
    #[serde(default)]
    pub allowed_recipient_domains: Vec<String>,
    /// Exact recipient address allowlist (empty = domain-level policy only).
    /// Takes precedence over `allowed_recipient_domains` when non-empty.
    #[serde(default)]
    pub allowed_recipients: Vec<String>,
    /// Per-key sustained rate (tokens/minute). None = inherit `[rate_limit].per_key_per_min`.
    pub rate_limit_per_min: Option<u32>,
    /// Per-key burst override. 0 = inherit `[rate_limit].per_key_burst`.
    #[serde(default)]
    pub burst: u32,
    /// Override global `[logging].mask_recipient` for this key (RFC 603).
    /// `None` = inherit global setting.
    pub mask_recipient: Option<bool>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct MailConfig {
    pub default_from: String,
    pub default_from_name: Option<String>,
    /// Global recipient domain allowlist (empty = allow all domains).
    #[serde(default)]
    pub allowed_recipient_domains: Vec<String>,
    #[serde(default = "default_max_subject_chars")]
    pub max_subject_chars: usize,
    #[serde(default = "default_max_body_bytes")]
    pub max_body_bytes: usize,
    /// Maximum number of recipients per request. Default 10.
    #[serde(default = "default_max_recipients")]
    pub max_recipients: usize,
    /// Maximum number of attachments per request (RFC 502).
    #[serde(default = "default_max_attachments")]
    pub max_attachments: usize,
    /// Maximum decoded size per attachment in bytes (RFC 502). Default 10 MiB.
    #[serde(default = "default_max_attachment_bytes")]
    pub max_attachment_bytes: usize,
    /// Maximum number of messages per `POST /v1/send-bulk` request (RFC 701).
    #[serde(default = "default_max_bulk_messages")]
    pub max_bulk_messages: usize,
    /// Allow HTML body in submissions (RFC 823 — default: false for attack surface reduction).
    #[serde(default)]
    pub allow_html_body: bool,
    /// Allow file attachments in submissions (RFC 823 — default: false).
    #[serde(default)]
    pub allow_attachments: bool,
    /// Allow bulk send via POST /v1/send-bulk (RFC 823 — default: false).
    #[serde(default)]
    pub allow_bulk_send: bool,
    /// Aggregate maximum decoded bytes across all attachments in one request (RFC 826).
    /// `None` = no aggregate limit (only per-attachment limit applies).
    pub max_total_attachment_bytes: Option<usize>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct SmtpConfig {
    #[serde(default = "default_smtp_mode")]
    pub mode: String,
    /// TLS mode: "none" (plain TCP), "starttls" (STARTTLS), or "tls" (implicit TLS).
    #[serde(default = "default_smtp_tls")]
    pub tls: String,
    #[serde(default = "default_smtp_host")]
    pub host: String,
    #[serde(default = "default_smtp_port")]
    pub port: u16,
    #[serde(default = "default_connect_timeout_seconds")]
    pub connect_timeout_seconds: u64,
    #[serde(default = "default_submission_timeout_seconds")]
    pub submission_timeout_seconds: u64,
    /// SMTP AUTH username. Must be set together with `auth_password` (RFC 301).
    pub auth_user: Option<String>,
    /// SMTP AUTH password. Never logged. Must be set together with `auth_user`.
    pub auth_password: Option<SecretString>,
    /// Command for pipe mode. Only used when `mode = "pipe"` (RFC 304).
    #[serde(default = "default_pipe_command")]
    pub pipe_command: String,
    /// Max concurrent SMTP submissions per bulk request (RFC 711).
    /// 0 = unlimited. Default: 5.
    #[serde(default = "default_bulk_concurrency")]
    pub bulk_concurrency: usize,
}

#[derive(Debug, Clone, Deserialize)]
pub struct RateLimitConfig {
    // Sustained rates (tokens/minute)
    #[serde(default = "default_global_per_min")]
    pub global_per_min: u32,
    #[serde(default = "default_per_ip_per_min")]
    pub per_ip_per_min: u32,
    /// Default per-key rate. Overridden by `ApiKeyConfig.rate_limit_per_min`.
    #[serde(default = "default_per_key_per_min")]
    pub per_key_per_min: u32,

    // Burst capacities (tokens a fresh bucket starts with)
    #[serde(default = "default_global_burst")]
    pub global_burst: u32,
    #[serde(default = "default_per_ip_burst")]
    pub per_ip_burst: u32,
    /// Default per-key burst. Overridden by `ApiKeyConfig.burst` when > 0.
    #[serde(default = "default_per_key_burst")]
    pub per_key_burst: u32,

    /// Legacy field — sets all three burst values if the per-tier fields are absent.
    /// Deprecated; use `global_burst`, `per_ip_burst`, `per_key_burst` instead.
    #[serde(default)]
    pub burst_size: u32,

    /// Maximum entries in the per-IP bucket map; LRU eviction above this.
    /// 0 = unlimited (not recommended in production).
    #[serde(default = "default_ip_table_size")]
    pub ip_table_size: usize,
}

impl RateLimitConfig {
    /// Effective global burst: per-tier value if set, else legacy `burst_size`, else default.
    pub fn effective_global_burst(&self) -> u32 {
        if self.global_burst > 0 { self.global_burst }
        else if self.burst_size > 0 { self.burst_size }
        else { default_global_burst() }
    }
    pub fn effective_per_ip_burst(&self) -> u32 {
        if self.per_ip_burst > 0 { self.per_ip_burst }
        else if self.burst_size > 0 { self.burst_size }
        else { default_per_ip_burst() }
    }
    pub fn effective_per_key_burst(&self) -> u32 {
        if self.per_key_burst > 0 { self.per_key_burst }
        else if self.burst_size > 0 { self.burst_size }
        else { default_per_key_burst() }
    }
}

#[derive(Debug, Clone, Deserialize)]
pub struct LoggingConfig {
    /// Output format: `"text"` (default) or `"json"`.
    #[serde(default = "default_log_format")]
    pub format: String,
    #[serde(default = "default_log_level")]
    pub level: String,
    /// When true, mask the recipient address in audit log entries.
    #[serde(default)]
    pub mask_recipient: bool,
}

// ---------------------------------------------------------------------------
// Default value functions
// ---------------------------------------------------------------------------

fn default_max_request_body_bytes() -> usize { 1_048_576 }
fn default_request_timeout_seconds() -> u64 { 30 }
fn default_shutdown_timeout_seconds() -> u64 { 30 }
fn default_true() -> bool { true }
fn default_max_subject_chars() -> usize { 255 }
fn default_max_body_bytes() -> usize { 65_536 }
fn default_smtp_mode() -> String { "smtp".into() }
fn default_smtp_host() -> String { "127.0.0.1".into() }
fn default_smtp_port() -> u16 { 25 }
fn default_connect_timeout_seconds() -> u64 { 5 }
fn default_submission_timeout_seconds() -> u64 { 30 }
fn default_bulk_concurrency() -> usize { 5 }
fn default_global_per_min() -> u32 { 60 }
fn default_per_ip_per_min() -> u32 { 20 }
#[allow(dead_code)]
fn default_burst_size() -> u32 { 5 }
fn default_max_recipients() -> usize { 10 }
fn default_max_attachments() -> usize { 5 }
fn default_max_attachment_bytes() -> usize { 10 * 1024 * 1024 } // 10 MiB
fn default_pipe_command() -> String { "/usr/sbin/sendmail".into() }
fn default_smtp_tls() -> String { "none".into() }
fn default_global_burst() -> u32 { 10 }
fn default_per_ip_burst() -> u32 { 5 }
fn default_per_key_burst() -> u32 { 5 }
fn default_per_key_per_min() -> u32 { 30 }
fn default_ip_table_size() -> usize { 10_000 }
fn default_log_format() -> String { "text".into() }
fn default_log_level() -> String { "info".into() }

// ---------------------------------------------------------------------------
// Config error
// ---------------------------------------------------------------------------

#[derive(Debug, Error)]
pub enum ConfigError {
    #[error("cannot read config file: {0}")]
    Io(#[from] std::io::Error),

    #[error("config parse error: {0}")]
    Parse(#[from] toml::de::Error),

    #[error("invalid server.bind_address: must be host:port (e.g. 127.0.0.1:8080)")]
    InvalidBindAddress,

    #[error("invalid mail.default_from: must be a valid email address")]
    InvalidDefaultFrom,
    #[error("no api_keys are configured")]
    NoApiKeys,

    #[error("no api_keys entries have enabled = true")]
    NoEnabledApiKeys,

    #[error("invalid CIDR: {0}")]
    InvalidCidr(String),

    #[error("configuration error: {0}")]
    Validation(String),

    #[error("invalid smtp.port: must be 1-65535")]
    InvalidSmtpPort,

    #[error("invalid rate_limit values: all per_min values must be > 0")]
    InvalidRateLimit,

    #[error("invalid logging.level: must be trace, debug, info, warn, or error")]
    InvalidLogLevel,

    #[error("invalid logging.format: must be 'text' or 'json'")]
    InvalidLogFormat,
}

// ---------------------------------------------------------------------------
// Default impls (needed for #[serde(default)] on AppConfig fields)
// ---------------------------------------------------------------------------

pub mod validate;
pub use validate::{load, load_from_str, validate_config};

fn default_max_bulk_messages() -> usize { 10 }

fn default_monitoring_cidrs() -> Vec<String> { vec!["127.0.0.1/32".into(), "::1/128".into()] }

// ---------------------------------------------------------------------------
// RFC 811: SIGHUP reload boundary helpers
// ---------------------------------------------------------------------------

/// Returns a list of field names that changed and require a restart.
///
/// Reloadable fields: mail.*, security.api_keys, status.ttl_seconds,
/// status.max_records, status.cleanup_interval_seconds, logging.*
pub fn restart_required_changes(old: &AppConfig, new: &AppConfig) -> Vec<String> {
    let mut changed = Vec::new();

    macro_rules! check {
        ($field:expr, $label:literal) => {
            if $field { changed.push($label.into()); }
        };
    }

    check!(old.server.bind_address        != new.server.bind_address,        "server.bind_address");
    check!(old.server.request_timeout_seconds != new.server.request_timeout_seconds, "server.request_timeout_seconds");
    check!(old.server.max_request_body_bytes  != new.server.max_request_body_bytes,  "server.max_request_body_bytes");
    check!(old.server.concurrency_limit   != new.server.concurrency_limit,    "server.concurrency_limit");
    check!(old.smtp.host                  != new.smtp.host,                   "smtp.host");
    check!(old.smtp.port                  != new.smtp.port,                   "smtp.port");
    check!(old.smtp.mode                  != new.smtp.mode,                   "smtp.mode");
    check!(old.rate_limit.global_per_min  != new.rate_limit.global_per_min,   "rate_limit.global_per_min");
    check!(old.rate_limit.per_ip_per_min  != new.rate_limit.per_ip_per_min,   "rate_limit.per_ip_per_min");
    check!(old.rate_limit.per_key_per_min != new.rate_limit.per_key_per_min,  "rate_limit.per_key_per_min");
    check!(old.security.trust_proxy_headers   != new.security.trust_proxy_headers,   "security.trust_proxy_headers");
    check!(old.security.trusted_source_cidrs  != new.security.trusted_source_cidrs,  "security.trusted_source_cidrs");
    check!(old.security.allowed_source_cidrs  != new.security.allowed_source_cidrs,  "security.allowed_source_cidrs");
    check!(old.status.enabled             != new.status.enabled,              "status.enabled");
    check!(old.status.store               != new.status.store,                "status.store");
    check!(old.status.db_path             != new.status.db_path,              "status.db_path");
    check!(old.status.redis_url           != new.status.redis_url,            "status.redis_url");

    changed
}

/// Build a merged config that applies only reloadable fields from `new`,
/// keeping restart-required fields from `current`.
pub fn merge_reloadable(current: &AppConfig, new: &AppConfig) -> AppConfig {
    AppConfig {
        // Restart-required: keep current values
        server:     current.server.clone(),
        smtp:       current.smtp.clone(),
        rate_limit: current.rate_limit.clone(),
        // Reloadable: take new values
        security:   new.security.clone(),
        mail:       new.mail.clone(),
        logging:    new.logging.clone(),
        status: StatusConfig {
            // Non-reloadable status fields kept from current
            enabled:  current.status.enabled,
            store:    current.status.store.clone(),
            db_path:  current.status.db_path.clone(),
            redis_url: current.status.redis_url.clone(),
            // Reloadable status fields from new
            ttl_seconds:              new.status.ttl_seconds,
            max_records:              new.status.max_records,
            cleanup_interval_seconds: new.status.cleanup_interval_seconds,
        },
    }
}
