//! Network configuration for the `LastID` SDK.
//!
//! Provides fine-grained control over HTTP client behavior including:
//! - Proxy configuration for corporate networks
//! - Granular timeout settings
//! - Connection pool tuning
//! - Correlation ID headers for request tracing

#[cfg(feature = "json-schema")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

use crate::constants::{
    DEFAULT_CONNECT_TIMEOUT_SECONDS, DEFAULT_CORRELATION_ID_HEADER,
    DEFAULT_POOL_IDLE_TIMEOUT_SECONDS, DEFAULT_POOL_MAX_IDLE_PER_HOST,
    DEFAULT_READ_TIMEOUT_SECONDS, DEFAULT_TIMEOUT_SECONDS,
};

/// Network configuration for HTTP client behavior.
///
/// Controls proxy settings, timeouts, and connection pooling.
/// All fields have sensible defaults suitable for most deployments.
///
/// # Example
///
/// ```toml
/// [network]
/// proxy_url = "http://proxy.corp.example.com:8080"
/// connect_timeout_seconds = 10
/// read_timeout_seconds = 30
/// correlation_id_header = "X-Correlation-ID"
/// ```
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
#[serde(default)]
pub struct NetworkConfig {
    /// HTTP proxy URL for corporate networks.
    ///
    /// When set, all HTTP requests will be routed through this proxy.
    /// Supports HTTP and HTTPS proxy URLs.
    ///
    /// # Example
    ///
    /// ```text
    /// http://proxy.corp.example.com:8080
    /// ```
    #[serde(default)]
    pub proxy_url: Option<String>,

    /// HTTPS proxy URL (if different from HTTP proxy).
    ///
    /// When set, HTTPS requests use this proxy while HTTP requests
    /// use `proxy_url`. If not set, `proxy_url` is used for all requests.
    #[serde(default)]
    pub https_proxy_url: Option<String>,

    /// Hostnames to bypass proxy (comma-separated).
    ///
    /// Requests to these hosts will not use the proxy.
    ///
    /// # Example
    ///
    /// ```text
    /// localhost,127.0.0.1,.internal.corp
    /// ```
    #[serde(default)]
    pub no_proxy: Option<String>,

    /// TCP connection timeout in seconds.
    ///
    /// Maximum time to wait for establishing a TCP connection.
    /// Default: 10 seconds
    #[serde(default = "default_connect_timeout")]
    pub connect_timeout_seconds: u64,

    /// HTTP response read timeout in seconds.
    ///
    /// Maximum time to wait for reading the response body after
    /// headers are received. Default: 30 seconds
    #[serde(default = "default_read_timeout")]
    pub read_timeout_seconds: u64,

    /// Overall request timeout in seconds.
    ///
    /// Maximum total time for a complete request/response cycle.
    /// Default: 30 seconds
    #[serde(default = "default_timeout")]
    pub request_timeout_seconds: u64,

    /// Connection pool idle timeout in seconds.
    ///
    /// How long idle connections are kept in the pool before closing.
    /// Default: 30 seconds
    #[serde(default = "default_pool_idle_timeout")]
    pub pool_idle_timeout_seconds: u64,

    /// Maximum idle connections per host.
    ///
    /// Maximum number of idle connections kept open to each host.
    /// Default: 5
    #[serde(default = "default_pool_max_idle")]
    pub pool_max_idle_per_host: usize,

    /// Custom header name for correlation IDs.
    ///
    /// When set, the SDK will add this header to all requests
    /// with a unique correlation ID for request tracing.
    ///
    /// Default: "X-Request-ID"
    #[serde(default = "default_correlation_header")]
    pub correlation_id_header: String,

    /// Enable correlation ID generation.
    ///
    /// When true, the SDK generates and attaches correlation IDs
    /// to all HTTP requests for tracing purposes.
    /// Default: true
    #[serde(default = "default_true")]
    pub enable_correlation_ids: bool,
}

const fn default_connect_timeout() -> u64 {
    DEFAULT_CONNECT_TIMEOUT_SECONDS
}

const fn default_read_timeout() -> u64 {
    DEFAULT_READ_TIMEOUT_SECONDS
}

const fn default_timeout() -> u64 {
    DEFAULT_TIMEOUT_SECONDS
}

const fn default_pool_idle_timeout() -> u64 {
    DEFAULT_POOL_IDLE_TIMEOUT_SECONDS
}

const fn default_pool_max_idle() -> usize {
    DEFAULT_POOL_MAX_IDLE_PER_HOST
}

fn default_correlation_header() -> String {
    DEFAULT_CORRELATION_ID_HEADER.to_string()
}

const fn default_true() -> bool {
    true
}

impl Default for NetworkConfig {
    fn default() -> Self {
        Self {
            proxy_url: None,
            https_proxy_url: None,
            no_proxy: None,
            connect_timeout_seconds: default_connect_timeout(),
            read_timeout_seconds: default_read_timeout(),
            request_timeout_seconds: default_timeout(),
            pool_idle_timeout_seconds: default_pool_idle_timeout(),
            pool_max_idle_per_host: default_pool_max_idle(),
            correlation_id_header: default_correlation_header(),
            // Disabled by default in WASM builds - browsers block custom headers
            // unless the server explicitly allows them via CORS (Access-Control-Allow-Headers).
            // The X-Request-ID header is not in the CORS safe list, so enabling this
            // in browser contexts causes preflight failures.
            enable_correlation_ids: !cfg!(target_arch = "wasm32"),
        }
    }
}

impl NetworkConfig {
    /// Merge another configuration into this one.
    ///
    /// Non-default values from `other` override values in `self`.
    pub fn merge(&mut self, other: &Self) {
        if other.proxy_url.is_some() {
            self.proxy_url.clone_from(&other.proxy_url);
        }
        if other.https_proxy_url.is_some() {
            self.https_proxy_url.clone_from(&other.https_proxy_url);
        }
        if other.no_proxy.is_some() {
            self.no_proxy.clone_from(&other.no_proxy);
        }
        if other.connect_timeout_seconds != default_connect_timeout() {
            self.connect_timeout_seconds = other.connect_timeout_seconds;
        }
        if other.read_timeout_seconds != default_read_timeout() {
            self.read_timeout_seconds = other.read_timeout_seconds;
        }
        if other.request_timeout_seconds != default_timeout() {
            self.request_timeout_seconds = other.request_timeout_seconds;
        }
        if other.pool_idle_timeout_seconds != default_pool_idle_timeout() {
            self.pool_idle_timeout_seconds = other.pool_idle_timeout_seconds;
        }
        if other.pool_max_idle_per_host != default_pool_max_idle() {
            self.pool_max_idle_per_host = other.pool_max_idle_per_host;
        }
        if other.correlation_id_header != default_correlation_header() {
            self.correlation_id_header
                .clone_from(&other.correlation_id_header);
        }
        if !other.enable_correlation_ids {
            self.enable_correlation_ids = false;
        }
    }

    /// Validate the network configuration.
    ///
    /// # Errors
    ///
    /// Returns an error if any timeout is zero.
    pub fn validate(&self) -> Result<(), crate::error::LastIDError> {
        if self.connect_timeout_seconds == 0 {
            return Err(crate::error::LastIDError::config(
                "connect_timeout_seconds must be > 0",
            ));
        }
        if self.read_timeout_seconds == 0 {
            return Err(crate::error::LastIDError::config(
                "read_timeout_seconds must be > 0",
            ));
        }
        if self.request_timeout_seconds == 0 {
            return Err(crate::error::LastIDError::config(
                "request_timeout_seconds must be > 0",
            ));
        }
        if self.pool_max_idle_per_host == 0 {
            return Err(crate::error::LastIDError::config(
                "pool_max_idle_per_host must be > 0",
            ));
        }
        if self.correlation_id_header.is_empty() && self.enable_correlation_ids {
            return Err(crate::error::LastIDError::config(
                "correlation_id_header cannot be empty when correlation IDs are enabled",
            ));
        }
        Ok(())
    }
}

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

    #[test]
    fn test_default_network_config() {
        let config = NetworkConfig::default();

        assert!(config.proxy_url.is_none());
        assert_eq!(
            config.connect_timeout_seconds,
            DEFAULT_CONNECT_TIMEOUT_SECONDS
        );
        assert_eq!(config.read_timeout_seconds, DEFAULT_READ_TIMEOUT_SECONDS);
        assert_eq!(config.request_timeout_seconds, DEFAULT_TIMEOUT_SECONDS);
        assert_eq!(
            config.pool_idle_timeout_seconds,
            DEFAULT_POOL_IDLE_TIMEOUT_SECONDS
        );
        assert_eq!(
            config.pool_max_idle_per_host,
            DEFAULT_POOL_MAX_IDLE_PER_HOST
        );
        assert_eq!(config.correlation_id_header, DEFAULT_CORRELATION_ID_HEADER);
        assert!(config.enable_correlation_ids);
    }

    #[test]
    fn test_validate_valid_config() {
        let config = NetworkConfig::default();
        assert!(config.validate().is_ok());
    }

    #[test]
    fn test_validate_zero_connect_timeout() {
        let mut config = NetworkConfig::default();
        config.connect_timeout_seconds = 0;
        assert!(config.validate().is_err());
    }

    #[test]
    fn test_validate_zero_read_timeout() {
        let mut config = NetworkConfig::default();
        config.read_timeout_seconds = 0;
        assert!(config.validate().is_err());
    }

    #[test]
    fn test_validate_empty_correlation_header() {
        let mut config = NetworkConfig::default();
        config.correlation_id_header = String::new();
        config.enable_correlation_ids = true;
        assert!(config.validate().is_err());
    }

    #[test]
    fn test_validate_empty_header_when_disabled() {
        let mut config = NetworkConfig::default();
        config.correlation_id_header = String::new();
        config.enable_correlation_ids = false;
        assert!(config.validate().is_ok());
    }

    #[test]
    fn test_merge() {
        let mut base = NetworkConfig::default();

        let other = NetworkConfig {
            proxy_url: Some("http://proxy.example.com:8080".into()),
            connect_timeout_seconds: 20,
            ..Default::default()
        };

        base.merge(&other);

        assert_eq!(base.proxy_url, Some("http://proxy.example.com:8080".into()));
        assert_eq!(base.connect_timeout_seconds, 20);
        // Should keep defaults for non-overridden fields
        assert_eq!(base.read_timeout_seconds, DEFAULT_READ_TIMEOUT_SECONDS);
    }

    #[test]
    fn test_from_toml() {
        let toml = r#"
            proxy_url = "http://proxy.corp.example.com:8080"
            connect_timeout_seconds = 15
            read_timeout_seconds = 45
            pool_max_idle_per_host = 10
            correlation_id_header = "X-Trace-ID"
        "#;

        let config: NetworkConfig = toml::from_str(toml).expect("Valid TOML");

        assert_eq!(
            config.proxy_url,
            Some("http://proxy.corp.example.com:8080".into())
        );
        assert_eq!(config.connect_timeout_seconds, 15);
        assert_eq!(config.read_timeout_seconds, 45);
        assert_eq!(config.pool_max_idle_per_host, 10);
        assert_eq!(config.correlation_id_header, "X-Trace-ID");
    }
}
