//! Configuration module for the `LastID` SDK.
//!
//! Supports multiple configuration sources with field-level precedence:
//! Environment variables > Explicit config > Discovered TOML > Defaults
//!
//! # Platform-Specific Behavior
//!
//! - **Native (non-WASM)**: Discovers config from filesystem paths
//!   (`./lastid.toml`, `~/.config/lastid/config.toml`,
//!   `/etc/lastid/config.toml`)
//! - **WASM**: Uses `localStorage` via gloo-storage (key: `lastid-sdk-config`)
//!   or requires explicit configuration via builder
//!
//! # Security
//!
//! All endpoint URLs must use HTTPS in production (SEC-001). HTTP is only
//! allowed for `localhost` or `127.0.0.1` during development.
//!
//! # JSON Schema Generation
//!
//! Enable the `json-schema` feature to generate JSON Schema for config
//! validation:
//!
//! ```toml
//! [dependencies]
//! lastid-sdk = { version = "0.1", features = ["json-schema"] }
//! ```
//!
//! Then use the `SDKConfig::json_schema()` method to generate the schema.

mod builder;
mod cache;
mod loader;
mod network;
mod polling;
mod retry;
#[cfg(feature = "websocket")]
mod websocket;

pub use builder::SDKConfigBuilder;
pub use cache::CacheConfig;
pub use network::NetworkConfig;
pub use polling::PollingConfig;
pub use retry::RetryPolicy;
#[cfg(feature = "json-schema")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[cfg(feature = "websocket")]
pub use websocket::{WebSocketConfig, WebSocketConfigBuilder};

use crate::constants::DEFAULT_CLOCK_SKEW_SECONDS;
use crate::error::LastIDError;
use crate::types::EndpointUrl;

/// SDK configuration with field-level precedence.
///
/// Configuration can be loaded from multiple sources:
/// 1. Environment variables (highest priority)
/// 2. Explicit configuration via builder
/// 3. Auto-discovered TOML files
/// 4. Default values (lowest priority)
///
/// # TOML Example
///
/// ```toml
/// idp_endpoint = "https://human.lastid.co"
/// client_id = "my-app"
/// trust_registry_endpoint = "https://human.lastid.co/v1/trust-registry"
/// timeout_seconds = 30
///
/// [polling]
/// initial_interval_ms = 2000
/// max_interval_ms = 30000
/// timeout_seconds = 300
///
/// [retry]
/// max_attempts = 3
/// initial_delay_ms = 1000
///
/// [cache]
/// enabled = true
/// ttl_seconds = 60
/// ```
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
#[serde(default)]
pub struct SDKConfig {
    /// IDP endpoint URL (required)
    pub idp_endpoint: String,

    /// OAuth client ID for authentication (required for authenticated requests)
    #[serde(default)]
    pub client_id: String,

    /// Trust registry endpoint (defaults to `{idp_endpoint}/v1/trust-registry`)
    #[serde(default)]
    pub trust_registry_endpoint: Option<String>,

    /// Public base URL for generating callback URLs
    #[serde(default)]
    pub public_base_url: Option<String>,

    /// Polling configuration
    #[serde(default)]
    pub polling: PollingConfig,

    /// Retry policy for transient failures
    #[serde(default)]
    pub retry: RetryPolicy,

    /// Trust registry cache configuration
    #[serde(default)]
    pub cache: CacheConfig,

    /// Network configuration (proxy, timeouts, pooling)
    #[serde(default)]
    pub network: NetworkConfig,

    /// HTTP request timeout in seconds
    #[serde(default = "default_timeout")]
    pub timeout_seconds: u64,

    /// Clock skew tolerance in seconds (SEC-005)
    ///
    /// Maximum allowed difference between local and server clocks
    /// when validating credential timestamps (exp, nbf claims).
    /// Default: 60 seconds
    #[serde(default = "default_clock_skew")]
    pub clock_skew_seconds: u64,

    /// WebSocket configuration for real-time status updates
    #[cfg(feature = "websocket")]
    #[serde(default)]
    pub websocket: WebSocketConfig,
}

const fn default_clock_skew() -> u64 {
    DEFAULT_CLOCK_SKEW_SECONDS
}

const fn default_timeout() -> u64 {
    30
}

impl Default for SDKConfig {
    fn default() -> Self {
        Self {
            idp_endpoint: String::new(),
            client_id: String::new(),
            trust_registry_endpoint: None,
            public_base_url: None,
            polling: PollingConfig::default(),
            retry: RetryPolicy::default(),
            cache: CacheConfig::default(),
            network: NetworkConfig::default(),
            timeout_seconds: default_timeout(),
            clock_skew_seconds: default_clock_skew(),
            #[cfg(feature = "websocket")]
            websocket: WebSocketConfig::default(),
        }
    }
}

impl SDKConfig {
    /// Create a new configuration builder.
    #[must_use]
    pub fn builder() -> SDKConfigBuilder {
        SDKConfigBuilder::new()
    }

    /// Get the effective trust registry endpoint.
    ///
    /// Returns the configured endpoint or defaults to
    /// `{idp_endpoint}/v1/trust-registry`.
    #[must_use]
    pub fn effective_trust_registry_endpoint(&self) -> String {
        self.trust_registry_endpoint
            .clone()
            .unwrap_or_else(|| format!("{}/v1/trust-registry", self.idp_endpoint))
    }

    /// Validate the configuration.
    ///
    /// # Security (SEC-001)
    ///
    /// Endpoint URLs must use HTTPS in production. HTTP is only allowed
    /// for `localhost` or `127.0.0.1` during development.
    ///
    /// # Errors
    ///
    /// Returns `LastIDError::Config` if validation fails.
    /// Returns `LastIDError::InsecureUrl` if a non-localhost HTTP URL is used.
    pub fn validate(&self) -> Result<(), LastIDError> {
        if self.idp_endpoint.is_empty() {
            return Err(LastIDError::config("idp_endpoint is required"));
        }

        // FR-009: Early validation that client_id is non-empty
        if self.client_id.is_empty() {
            return Err(LastIDError::config(
                "client_id is required for authenticated requests",
            ));
        }

        // SEC-001: Enforce HTTPS (localhost exception for development)
        // Validate using EndpointUrl newtype to ensure secure URLs
        EndpointUrl::new(&self.idp_endpoint)?;

        // Validate trust_registry_endpoint if provided
        if let Some(ref endpoint) = self.trust_registry_endpoint
            && !endpoint.is_empty()
        {
            EndpointUrl::new(endpoint)?;
        }

        if self.timeout_seconds == 0 {
            return Err(LastIDError::config("timeout_seconds must be > 0"));
        }

        self.polling.validate()?;
        self.retry.validate()?;
        self.cache.validate()?;
        self.network.validate()?;
        #[cfg(feature = "websocket")]
        self.websocket.validate()?;

        Ok(())
    }

    /// Merge another configuration into this one.
    ///
    /// Non-empty values from `other` override values in `self`.
    pub fn merge(&mut self, other: &Self) {
        if !other.idp_endpoint.is_empty() {
            self.idp_endpoint.clone_from(&other.idp_endpoint);
        }

        if !other.client_id.is_empty() {
            self.client_id.clone_from(&other.client_id);
        }

        if other.trust_registry_endpoint.is_some() {
            self.trust_registry_endpoint
                .clone_from(&other.trust_registry_endpoint);
        }

        if other.timeout_seconds != default_timeout() {
            self.timeout_seconds = other.timeout_seconds;
        }

        self.polling.merge(&other.polling);
        self.retry.merge(&other.retry);
        self.cache.merge(&other.cache);
        self.network.merge(&other.network);
        #[cfg(feature = "websocket")]
        self.websocket.merge(&other.websocket);
    }

    /// Generate a JSON Schema for the SDK configuration.
    ///
    /// This can be used to validate TOML configuration files in IDEs or CI
    /// pipelines.
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use lastid_sdk::config::SDKConfig;
    ///
    /// # #[cfg(feature = "json-schema")]
    /// # fn example() {
    /// let schema = SDKConfig::json_schema();
    /// println!("{}", serde_json::to_string_pretty(&schema).unwrap());
    /// # }
    /// ```
    ///
    /// # Note
    ///
    /// This method is only available when the `json-schema` feature is enabled.
    #[cfg(feature = "json-schema")]
    #[must_use]
    #[allow(clippy::use_self)] // schema_for! macro doesn't support Self
    pub fn json_schema() -> schemars::schema::RootSchema {
        schemars::schema_for!(SDKConfig)
    }

    /// Generate a JSON Schema as a pretty-printed string.
    ///
    /// Convenience method for getting the schema as a string.
    ///
    /// # Note
    ///
    /// This method is only available when the `json-schema` feature is enabled.
    #[cfg(feature = "json-schema")]
    #[must_use]
    pub fn json_schema_string() -> String {
        serde_json::to_string_pretty(&Self::json_schema())
            .expect("schema serialization should not fail")
    }
}

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

    #[test]
    fn test_default_config() {
        let config = SDKConfig::default();
        assert!(config.idp_endpoint.is_empty());
        assert_eq!(config.timeout_seconds, 30);
        assert!(config.trust_registry_endpoint.is_none());
    }

    #[test]
    fn test_from_toml() {
        let toml = r#"
            idp_endpoint = "https://test.lastid.co"
            timeout_seconds = 60

            [polling]
            initial_interval_ms = 1000
            max_duration_seconds = 600

            [retry]
            max_attempts = 5

            [cache]
            enabled = false
        "#;

        let config = SDKConfig::from_toml(toml).unwrap();
        assert_eq!(config.idp_endpoint, "https://test.lastid.co");
        assert_eq!(config.timeout_seconds, 60);
        assert_eq!(config.polling.initial_interval_ms, 1000);
        assert_eq!(config.polling.max_duration_seconds, 600);
        assert_eq!(config.retry.max_attempts, 5);
        assert!(!config.cache.enabled);
    }

    #[test]
    fn test_effective_trust_registry_endpoint() {
        let mut config = SDKConfig::default();
        config.idp_endpoint = "https://human.lastid.co".into();

        assert_eq!(
            config.effective_trust_registry_endpoint(),
            "https://human.lastid.co/v1/trust-registry"
        );

        config.trust_registry_endpoint = Some("https://custom.endpoint.com".into());
        assert_eq!(
            config.effective_trust_registry_endpoint(),
            "https://custom.endpoint.com"
        );
    }

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

    #[test]
    fn test_validate_invalid_url() {
        let mut config = SDKConfig::default();
        config.idp_endpoint = "not-a-url".into();
        assert!(config.validate().is_err());
    }

    #[test]
    fn test_validate_http_remote_rejected() {
        // SEC-001: HTTP to remote hosts should be rejected
        let mut config = SDKConfig::default();
        config.idp_endpoint = "http://example.com".into();
        config.client_id = "test-client".into();
        let result = config.validate();
        assert!(result.is_err());
        // Should return InsecureUrl error
        let err_str = result.unwrap_err().to_string();
        assert!(err_str.contains("HTTP") || err_str.contains("HTTPS") || err_str.contains("https"));
    }

    #[test]
    fn test_validate_http_localhost_allowed() {
        // SEC-001: HTTP to localhost should be allowed for development
        let mut config = SDKConfig::default();
        config.idp_endpoint = "http://localhost:3000".into();
        config.client_id = "test-client".into();
        assert!(config.validate().is_ok());
    }

    #[test]
    fn test_validate_https_allowed() {
        let mut config = SDKConfig::default();
        config.idp_endpoint = "https://human.lastid.co".into();
        config.client_id = "test-client".into();
        assert!(config.validate().is_ok());
    }

    #[test]
    fn test_validate_empty_client_id() {
        // FR-009: Early validation that client_id is non-empty
        let mut config = SDKConfig::default();
        config.idp_endpoint = "https://human.lastid.co".into();
        // client_id is empty
        let result = config.validate();
        assert!(result.is_err());
        let err_str = result.unwrap_err().to_string();
        assert!(err_str.contains("client_id"));
    }

    #[test]
    fn test_validate_trust_registry_http_rejected() {
        // SEC-001: Trust registry endpoint should also enforce HTTPS
        let mut config = SDKConfig::default();
        config.idp_endpoint = "https://human.lastid.co".into();
        config.client_id = "test-client".into();
        config.trust_registry_endpoint = Some("http://example.com/registry".into());
        let result = config.validate();
        assert!(result.is_err());
    }

    #[test]
    fn test_builder() {
        let config = SDKConfig::builder()
            .idp_endpoint("https://test.lastid.co")
            .client_id("test-client")
            .timeout_seconds(60)
            .build()
            .unwrap();

        assert_eq!(config.idp_endpoint, "https://test.lastid.co");
        assert_eq!(config.client_id, "test-client");
        assert_eq!(config.timeout_seconds, 60);
    }

    #[test]
    fn test_merge() {
        let mut base = SDKConfig::default();
        base.idp_endpoint = "https://base.endpoint.com".into();
        base.timeout_seconds = 30;

        let override_config = SDKConfig {
            idp_endpoint: "https://override.endpoint.com".into(),
            timeout_seconds: 60,
            ..Default::default()
        };

        base.merge(&override_config);

        assert_eq!(base.idp_endpoint, "https://override.endpoint.com");
        assert_eq!(base.timeout_seconds, 60);
    }

    #[cfg(feature = "json-schema")]
    #[test]
    fn test_json_schema_generation() {
        let schema = SDKConfig::json_schema();

        // Verify the schema has the expected structure
        assert!(schema.schema.metadata.is_some() || schema.definitions.contains_key("SDKConfig"));

        // Verify we can serialize it to JSON
        let json = serde_json::to_string_pretty(&schema);
        assert!(json.is_ok(), "Schema should serialize to JSON");

        let json_str = json.unwrap();
        // Verify it contains expected field names
        assert!(json_str.contains("idp_endpoint"));
        assert!(json_str.contains("polling"));
        assert!(json_str.contains("retry"));
        assert!(json_str.contains("cache"));
    }

    #[cfg(feature = "json-schema")]
    #[test]
    fn test_json_schema_string() {
        let schema_str = SDKConfig::json_schema_string();
        assert!(!schema_str.is_empty());
        assert!(schema_str.contains("idp_endpoint"));
    }

    #[test]
    fn test_env_interpolation_missing_var() {
        let toml = r#"
            idp_endpoint = "${NONEXISTENT_VAR_12345_UNIQUE}"
        "#;

        // Should parse but with empty string (var doesn't exist)
        let config = SDKConfig::from_toml_with_env(toml).unwrap();
        assert_eq!(config.idp_endpoint, "");
    }

    #[test]
    fn test_env_interpolation_preserves_literal() {
        let toml = r#"
            idp_endpoint = "https://literal.example.com"
        "#;

        // No env vars - should preserve literal
        let config = SDKConfig::from_toml_with_env(toml).unwrap();
        assert_eq!(config.idp_endpoint, "https://literal.example.com");
    }

    #[test]
    fn test_env_interpolation_regex() {
        // Test the interpolation regex pattern directly
        let content = "value = \"${VAR1} and ${VAR2}\"";
        let result = SDKConfig::interpolate_env_vars(content);
        // Variables don't exist, so they become empty
        assert_eq!(result, "value = \" and \"");
    }
}
