//! Proxy Configuration
//!
//! Configuration management for HeliosDB Proxy.

use crate::{ProxyError, Result};
use serde::{Deserialize, Serialize};
use std::path::Path;
use std::time::Duration;

// =============================================================================
// POOL MODE TYPES
// =============================================================================

/// Connection pooling mode
///
/// Determines when connections are returned to the pool.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum PoolingMode {
    /// Session mode: 1:1 client-to-backend mapping
    #[default]
    Session,
    /// Transaction mode: Return after COMMIT/ROLLBACK
    Transaction,
    /// Statement mode: Return after each statement
    Statement,
}

/// Prepared statement handling mode
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum PreparedStatementMode {
    /// Disable prepared statements
    #[default]
    Disable,
    /// Track and recreate on new connections
    Track,
    /// Use protocol-level named statements
    Named,
}

/// Pool mode configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PoolModeConfig {
    /// Default pooling mode
    #[serde(default)]
    pub mode: PoolingMode,
    /// Maximum connections per node
    #[serde(default = "default_pool_mode_max_size")]
    pub max_pool_size: u32,
    /// Minimum idle connections
    #[serde(default = "default_pool_mode_min_idle")]
    pub min_idle: u32,
    /// Idle timeout (seconds)
    #[serde(default = "default_pool_mode_idle_timeout")]
    pub idle_timeout_secs: u64,
    /// Max connection lifetime (seconds)
    #[serde(default = "default_pool_mode_max_lifetime")]
    pub max_lifetime_secs: u64,
    /// Acquire timeout (seconds)
    #[serde(default = "default_pool_mode_acquire_timeout")]
    pub acquire_timeout_secs: u64,
    /// Reset query to run when returning connection to pool
    #[serde(default = "default_reset_query")]
    pub reset_query: String,
    /// Prepared statement mode
    #[serde(default)]
    pub prepared_statement_mode: PreparedStatementMode,
}

fn default_pool_mode_max_size() -> u32 {
    100
}

fn default_pool_mode_min_idle() -> u32 {
    10
}

fn default_pool_mode_idle_timeout() -> u64 {
    600
}

fn default_pool_mode_max_lifetime() -> u64 {
    3600
}

fn default_pool_mode_acquire_timeout() -> u64 {
    5
}

fn default_reset_query() -> String {
    "DISCARD ALL".to_string()
}

impl Default for PoolModeConfig {
    fn default() -> Self {
        Self {
            mode: PoolingMode::default(),
            max_pool_size: default_pool_mode_max_size(),
            min_idle: default_pool_mode_min_idle(),
            idle_timeout_secs: default_pool_mode_idle_timeout(),
            max_lifetime_secs: default_pool_mode_max_lifetime(),
            acquire_timeout_secs: default_pool_mode_acquire_timeout(),
            reset_query: default_reset_query(),
            prepared_statement_mode: PreparedStatementMode::default(),
        }
    }
}

impl PoolModeConfig {
    /// Create config for session mode
    pub fn session_mode() -> Self {
        Self {
            mode: PoolingMode::Session,
            prepared_statement_mode: PreparedStatementMode::Named,
            ..Default::default()
        }
    }

    /// Create config for transaction mode
    pub fn transaction_mode() -> Self {
        Self {
            mode: PoolingMode::Transaction,
            prepared_statement_mode: PreparedStatementMode::Track,
            ..Default::default()
        }
    }

    /// Create config for statement mode
    pub fn statement_mode() -> Self {
        Self {
            mode: PoolingMode::Statement,
            prepared_statement_mode: PreparedStatementMode::Disable,
            ..Default::default()
        }
    }

    /// Get idle timeout as Duration
    pub fn idle_timeout(&self) -> Duration {
        Duration::from_secs(self.idle_timeout_secs)
    }

    /// Get max lifetime as Duration
    pub fn max_lifetime(&self) -> Duration {
        Duration::from_secs(self.max_lifetime_secs)
    }

    /// Get acquire timeout as Duration
    pub fn acquire_timeout(&self) -> Duration {
        Duration::from_secs(self.acquire_timeout_secs)
    }
}

// =============================================================================
// MAIN PROXY CONFIG
// =============================================================================

/// Proxy configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProxyConfig {
    /// Listen address for client connections
    pub listen_address: String,
    /// Admin API address
    pub admin_address: String,
    /// Enable TR (Transaction Replay)
    pub tr_enabled: bool,
    /// TR mode
    pub tr_mode: TrMode,
    /// Connection pool configuration
    pub pool: PoolConfig,
    /// Pool mode configuration (Session/Transaction/Statement)
    #[serde(default)]
    pub pool_mode: PoolModeConfig,
    /// Load balancer configuration
    pub load_balancer: LoadBalancerConfig,
    /// Health check configuration
    pub health: HealthConfig,
    /// Backend nodes
    pub nodes: Vec<NodeConfig>,
    /// TLS configuration
    pub tls: Option<TlsConfig>,
    /// Write timeout during failover (seconds)
    /// When primary is unavailable, wait this long for a new primary before returning error
    #[serde(default = "default_write_timeout_secs")]
    pub write_timeout_secs: u64,
    /// Plugin system configuration. Only consumed when the `wasm-plugins`
    /// feature is enabled; on a feature-off build, values are parsed and
    /// ignored so existing configs don't break.
    #[serde(default)]
    pub plugins: PluginToml,
}

fn default_write_timeout_secs() -> u64 {
    30 // 30 seconds default write timeout during failover
}

impl Default for ProxyConfig {
    fn default() -> Self {
        Self {
            listen_address: "0.0.0.0:5432".to_string(),
            admin_address: "0.0.0.0:9090".to_string(),
            tr_enabled: true,
            tr_mode: TrMode::Session,
            pool: PoolConfig::default(),
            pool_mode: PoolModeConfig::default(),
            load_balancer: LoadBalancerConfig::default(),
            health: HealthConfig::default(),
            nodes: Vec::new(),
            tls: None,
            write_timeout_secs: default_write_timeout_secs(),
            plugins: PluginToml::default(),
        }
    }
}

// =============================================================================
// PLUGIN SYSTEM CONFIG (TOML-friendly shape)
// =============================================================================

/// Plugin-system configuration, in a TOML-friendly shape.
///
/// Always present on `ProxyConfig` so existing configs round-trip, but only
/// consumed when the `wasm-plugins` feature is enabled. When
/// `plugins.enabled` is `false` (the default), plugin loading is skipped
/// entirely and every plugin-hook call site becomes a zero-cost no-op.
///
/// Converted to `crate::plugins::PluginRuntimeConfig` at startup via a
/// feature-gated `From` impl in `src/plugins/config.rs`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginToml {
    /// Enable the plugin subsystem. Defaults to `false` — plugins are
    /// strictly opt-in.
    #[serde(default)]
    pub enabled: bool,
    /// Directory to scan at startup for `.wasm` plugin files.
    #[serde(default = "default_plugin_dir")]
    pub plugin_dir: String,
    /// Watch `plugin_dir` for file changes and reload plugins hot.
    #[serde(default)]
    pub hot_reload: bool,
    /// Memory limit per plugin instance, in megabytes.
    #[serde(default = "default_plugin_memory_mb")]
    pub memory_limit_mb: usize,
    /// Execution timeout per hook call, in milliseconds.
    #[serde(default = "default_plugin_timeout_ms")]
    pub timeout_ms: u64,
    /// Maximum number of concurrently-loaded plugins.
    #[serde(default = "default_plugin_max")]
    pub max_plugins: usize,
    /// Enable per-call CPU-cycle (fuel) metering to bound plugin runtime.
    #[serde(default = "default_true")]
    pub fuel_metering: bool,
    /// Fuel units allowed per hook call when `fuel_metering = true`.
    #[serde(default = "default_plugin_fuel")]
    pub fuel_limit: u64,
    /// Optional Ed25519 trust-root directory. When set, every loaded
    /// .wasm requires a sidecar .sig that verifies against one of
    /// the *.pub files in this directory. When omitted, signatures
    /// are not checked (preserves the dev-loop ergonomic of dropping
    /// unsigned .wasm files in the plugin dir).
    #[serde(default)]
    pub trust_root: Option<String>,
}

fn default_plugin_dir() -> String {
    "/etc/heliosproxy/plugins".to_string()
}
fn default_plugin_memory_mb() -> usize {
    64
}
fn default_plugin_timeout_ms() -> u64 {
    100
}
fn default_plugin_max() -> usize {
    20
}
fn default_true() -> bool {
    true
}
fn default_plugin_fuel() -> u64 {
    1_000_000
}

impl Default for PluginToml {
    fn default() -> Self {
        Self {
            enabled: false,
            plugin_dir: default_plugin_dir(),
            hot_reload: false,
            memory_limit_mb: default_plugin_memory_mb(),
            timeout_ms: default_plugin_timeout_ms(),
            max_plugins: default_plugin_max(),
            fuel_metering: true,
            fuel_limit: default_plugin_fuel(),
            trust_root: None,
        }
    }
}

impl ProxyConfig {
    /// Get write timeout as Duration
    pub fn write_timeout(&self) -> Duration {
        Duration::from_secs(self.write_timeout_secs)
    }

    /// Load configuration from file
    pub fn from_file(path: &str) -> Result<Self> {
        let path = Path::new(path);

        if !path.exists() {
            return Err(ProxyError::Config(format!(
                "Configuration file not found: {}",
                path.display()
            )));
        }

        let contents = std::fs::read_to_string(path)
            .map_err(|e| ProxyError::Config(format!("Failed to read config: {}", e)))?;

        let config: Self = toml::from_str(&contents)
            .map_err(|e| ProxyError::Config(format!("Failed to parse config: {}", e)))?;

        config.validate()?;

        Ok(config)
    }

    /// Add a node from host:port string
    pub fn add_node(&mut self, host_port: &str, role: &str) -> Result<()> {
        let parts: Vec<&str> = host_port.rsplitn(2, ':').collect();
        if parts.len() != 2 {
            return Err(ProxyError::Config(format!(
                "Invalid host:port format: {}",
                host_port
            )));
        }

        let port: u16 = parts[0].parse()
            .map_err(|_| ProxyError::Config(format!("Invalid port: {}", parts[0])))?;

        let host = parts[1].to_string();

        let role = match role {
            "primary" => NodeRole::Primary,
            "standby" => NodeRole::Standby,
            "replica" => NodeRole::ReadReplica,
            _ => return Err(ProxyError::Config(format!("Unknown role: {}", role))),
        };

        self.nodes.push(NodeConfig {
            host,
            port,
            http_port: default_http_port(),
            role,
            weight: 100,
            enabled: true,
            name: None,
        });

        Ok(())
    }

    /// Validate configuration
    pub fn validate(&self) -> Result<()> {
        // Must have at least one node
        if self.nodes.is_empty() {
            return Err(ProxyError::Config("No backend nodes configured".to_string()));
        }

        // Must have a primary node
        let has_primary = self.nodes.iter().any(|n| n.role == NodeRole::Primary);
        if !has_primary {
            return Err(ProxyError::Config("No primary node configured".to_string()));
        }

        // Validate pool config
        if self.pool.max_connections < self.pool.min_connections {
            return Err(ProxyError::Config(
                "max_connections must be >= min_connections".to_string(),
            ));
        }

        Ok(())
    }

    /// Get primary node
    pub fn primary_node(&self) -> Option<&NodeConfig> {
        self.nodes.iter().find(|n| n.role == NodeRole::Primary && n.enabled)
    }

    /// Get standby nodes
    pub fn standby_nodes(&self) -> Vec<&NodeConfig> {
        self.nodes.iter()
            .filter(|n| n.role == NodeRole::Standby && n.enabled)
            .collect()
    }

    /// Get all enabled nodes
    pub fn enabled_nodes(&self) -> Vec<&NodeConfig> {
        self.nodes.iter().filter(|n| n.enabled).collect()
    }
}

/// TR (Transaction Replay) mode
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TrMode {
    /// No transaction replay
    None,
    /// Re-establish session only
    Session,
    /// Re-execute SELECT queries
    Select,
    /// Full transaction replay
    Transaction,
}

impl Default for TrMode {
    fn default() -> Self {
        TrMode::Session
    }
}

/// Connection pool configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PoolConfig {
    /// Minimum connections per node
    pub min_connections: usize,
    /// Maximum connections per node
    pub max_connections: usize,
    /// Connection idle timeout (seconds)
    pub idle_timeout_secs: u64,
    /// Maximum connection lifetime (seconds)
    pub max_lifetime_secs: u64,
    /// Connection acquire timeout (seconds)
    pub acquire_timeout_secs: u64,
    /// Test connection before use
    pub test_on_acquire: bool,
}

impl Default for PoolConfig {
    fn default() -> Self {
        Self {
            min_connections: 2,
            max_connections: 100,
            idle_timeout_secs: 300,
            max_lifetime_secs: 1800,
            acquire_timeout_secs: 30,
            test_on_acquire: true,
        }
    }
}

impl PoolConfig {
    /// Get idle timeout as Duration
    pub fn idle_timeout(&self) -> Duration {
        Duration::from_secs(self.idle_timeout_secs)
    }

    /// Get max lifetime as Duration
    pub fn max_lifetime(&self) -> Duration {
        Duration::from_secs(self.max_lifetime_secs)
    }

    /// Get acquire timeout as Duration
    pub fn acquire_timeout(&self) -> Duration {
        Duration::from_secs(self.acquire_timeout_secs)
    }
}

/// Load balancer configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoadBalancerConfig {
    /// Routing strategy for read queries
    pub read_strategy: Strategy,
    /// Enable read/write splitting
    pub read_write_split: bool,
    /// Latency threshold for unhealthy marking (ms)
    pub latency_threshold_ms: u64,
}

impl Default for LoadBalancerConfig {
    fn default() -> Self {
        Self {
            read_strategy: Strategy::RoundRobin,
            read_write_split: true,
            latency_threshold_ms: 100,
        }
    }
}

/// Load balancing strategy
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Strategy {
    /// Round-robin across nodes
    RoundRobin,
    /// Weighted round-robin
    WeightedRoundRobin,
    /// Route to least loaded node
    LeastConnections,
    /// Route to lowest latency node
    LatencyBased,
    /// Random selection
    Random,
}

/// Health check configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthConfig {
    /// Check interval (seconds)
    pub check_interval_secs: u64,
    /// Check timeout (seconds)
    pub check_timeout_secs: u64,
    /// Failures before marking unhealthy
    pub failure_threshold: u32,
    /// Successes before marking healthy
    pub success_threshold: u32,
    /// Health check query
    pub check_query: String,
}

impl Default for HealthConfig {
    fn default() -> Self {
        Self {
            check_interval_secs: 5,
            check_timeout_secs: 3,
            failure_threshold: 3,
            success_threshold: 2,
            check_query: "SELECT 1".to_string(),
        }
    }
}

impl HealthConfig {
    /// Get check interval as Duration
    pub fn check_interval(&self) -> Duration {
        Duration::from_secs(self.check_interval_secs)
    }

    /// Get check timeout as Duration
    pub fn check_timeout(&self) -> Duration {
        Duration::from_secs(self.check_timeout_secs)
    }
}

/// Backend node configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodeConfig {
    /// Node host
    pub host: String,
    /// Node port (PostgreSQL protocol)
    pub port: u16,
    /// Node HTTP API port (for SQL API forwarding)
    /// Defaults to 8080 if not specified
    #[serde(default = "default_http_port")]
    pub http_port: u16,
    /// Node role
    pub role: NodeRole,
    /// Weight for load balancing
    pub weight: u32,
    /// Whether node is enabled
    pub enabled: bool,
    /// Optional node name for logging
    pub name: Option<String>,
}

fn default_http_port() -> u16 {
    8080
}

impl NodeConfig {
    /// Get address string
    pub fn address(&self) -> String {
        format!("{}:{}", self.host, self.port)
    }

    /// Get display name
    pub fn display_name(&self) -> &str {
        self.name.as_deref().unwrap_or(&self.host)
    }
}

/// Node role
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum NodeRole {
    /// Primary node (accepts writes)
    Primary,
    /// Standby node (can be promoted)
    Standby,
    /// Read replica (read-only, cannot be promoted)
    #[serde(rename = "replica")]
    ReadReplica,
}

/// TLS configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TlsConfig {
    /// Enable TLS for client connections
    pub enabled: bool,
    /// Path to certificate file
    pub cert_path: String,
    /// Path to private key file
    pub key_path: String,
    /// Path to CA certificate (for client verification)
    pub ca_path: Option<String>,
    /// Require client certificates
    pub require_client_cert: bool,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_default_config() {
        let config = ProxyConfig::default();
        assert_eq!(config.listen_address, "0.0.0.0:5432");
        assert!(config.tr_enabled);
    }

    #[test]
    fn test_add_node() {
        let mut config = ProxyConfig::default();
        config.add_node("localhost:5432", "primary").unwrap();
        config.add_node("localhost:5433", "standby").unwrap();

        assert_eq!(config.nodes.len(), 2);
        assert!(config.primary_node().is_some());
        assert_eq!(config.standby_nodes().len(), 1);
    }

    #[test]
    fn test_validate_no_nodes() {
        let config = ProxyConfig::default();
        assert!(config.validate().is_err());
    }

    #[test]
    fn test_validate_no_primary() {
        let mut config = ProxyConfig::default();
        config.add_node("localhost:5432", "standby").unwrap();
        assert!(config.validate().is_err());
    }

    #[test]
    fn test_validate_success() {
        let mut config = ProxyConfig::default();
        config.add_node("localhost:5432", "primary").unwrap();
        assert!(config.validate().is_ok());
    }

    #[test]
    fn test_pool_config_durations() {
        let config = PoolConfig::default();
        assert_eq!(config.idle_timeout(), Duration::from_secs(300));
        assert_eq!(config.max_lifetime(), Duration::from_secs(1800));
    }

    #[test]
    fn test_pool_mode_default() {
        let config = PoolModeConfig::default();
        assert_eq!(config.mode, PoolingMode::Session);
        assert_eq!(config.max_pool_size, 100);
        assert_eq!(config.min_idle, 10);
        assert_eq!(config.reset_query, "DISCARD ALL");
    }

    #[test]
    fn test_pool_mode_session() {
        let config = PoolModeConfig::session_mode();
        assert_eq!(config.mode, PoolingMode::Session);
        assert_eq!(config.prepared_statement_mode, PreparedStatementMode::Named);
    }

    #[test]
    fn test_pool_mode_transaction() {
        let config = PoolModeConfig::transaction_mode();
        assert_eq!(config.mode, PoolingMode::Transaction);
        assert_eq!(config.prepared_statement_mode, PreparedStatementMode::Track);
    }

    #[test]
    fn test_pool_mode_statement() {
        let config = PoolModeConfig::statement_mode();
        assert_eq!(config.mode, PoolingMode::Statement);
        assert_eq!(config.prepared_statement_mode, PreparedStatementMode::Disable);
    }

    #[test]
    fn test_pool_mode_durations() {
        let config = PoolModeConfig::default();
        assert_eq!(config.idle_timeout(), Duration::from_secs(600));
        assert_eq!(config.max_lifetime(), Duration::from_secs(3600));
        assert_eq!(config.acquire_timeout(), Duration::from_secs(5));
    }

    #[test]
    fn test_proxy_config_has_pool_mode() {
        let config = ProxyConfig::default();
        assert_eq!(config.pool_mode.mode, PoolingMode::Session);
    }

    /// `plugins` defaults to `enabled = false` so adding the field to
    /// `ProxyConfig` doesn't spontaneously turn on the plugin subsystem
    /// for existing deployments.
    #[test]
    fn test_plugin_toml_default_is_disabled() {
        let config = ProxyConfig::default();
        assert!(!config.plugins.enabled);
        assert_eq!(config.plugins.plugin_dir, "/etc/heliosproxy/plugins");
        assert_eq!(config.plugins.memory_limit_mb, 64);
        assert_eq!(config.plugins.timeout_ms, 100);
    }

    /// Existing TOML configs (written before this field existed) must
    /// round-trip through `Deserialize` without failing. The `plugins`
    /// section is `#[serde(default)]`, so omitting it yields the default.
    #[test]
    fn test_proxy_config_toml_without_plugins_section_still_parses() {
        let toml_text = r#"
            listen_address = "0.0.0.0:5432"
            admin_address = "0.0.0.0:9090"
            tr_enabled = true
            tr_mode = "session"
            nodes = []

            [pool]
            min_connections = 2
            max_connections = 10
            idle_timeout_secs = 300
            max_lifetime_secs = 1800
            acquire_timeout_secs = 30
            test_on_acquire = true

            [load_balancer]
            read_strategy = "round_robin"
            read_write_split = true
            latency_threshold_ms = 100

            [health]
            check_interval_secs = 5
            check_timeout_secs = 3
            failure_threshold = 3
            success_threshold = 2
            check_query = "SELECT 1"
        "#;
        let config: ProxyConfig = toml::from_str(toml_text).expect("parse");
        assert!(!config.plugins.enabled);
    }

    /// A `[plugins]` section with overrides round-trips and populates the
    /// struct correctly.
    #[test]
    fn test_plugin_toml_overrides_parse() {
        let toml_text = r#"
            listen_address = "0.0.0.0:5432"
            admin_address = "0.0.0.0:9090"
            tr_enabled = true
            tr_mode = "session"
            nodes = []

            [pool]
            min_connections = 2
            max_connections = 10
            idle_timeout_secs = 300
            max_lifetime_secs = 1800
            acquire_timeout_secs = 30
            test_on_acquire = true

            [load_balancer]
            read_strategy = "round_robin"
            read_write_split = true
            latency_threshold_ms = 100

            [health]
            check_interval_secs = 5
            check_timeout_secs = 3
            failure_threshold = 3
            success_threshold = 2
            check_query = "SELECT 1"

            [plugins]
            enabled = true
            plugin_dir = "/tmp/helios-plugins"
            hot_reload = true
            memory_limit_mb = 128
            timeout_ms = 250
        "#;
        let config: ProxyConfig = toml::from_str(toml_text).expect("parse");
        assert!(config.plugins.enabled);
        assert_eq!(config.plugins.plugin_dir, "/tmp/helios-plugins");
        assert!(config.plugins.hot_reload);
        assert_eq!(config.plugins.memory_limit_mb, 128);
        assert_eq!(config.plugins.timeout_ms, 250);
        // Un-specified fields retain their defaults.
        assert_eq!(config.plugins.max_plugins, 20);
        assert!(config.plugins.fuel_metering);
    }
}
