//! Error types for the `LastID` SDK.
//!
//! This module provides a comprehensive error hierarchy:
//! - [`LastIDError`]: Top-level SDK error type
//! - [`HttpError`]: HTTP client errors with retry classification
//! - [`TrustRegistryError`]: Trust registry specific errors

mod http;
mod trust_registry;

pub use http::HttpError;
pub use trust_registry::TrustRegistryError;

#[cfg(feature = "websocket")]
mod websocket;
use thiserror::Error;
#[cfg(feature = "websocket")]
pub use websocket::WebSocketError;

/// Top-level SDK error type.
///
/// All SDK operations return this error type, which encompasses configuration
/// errors, HTTP errors, trust registry errors, cryptographic errors, and more.
///
/// # Error Handling
///
/// Use pattern matching to handle specific error cases:
///
/// ```rust,no_run
/// use lastid_sdk::{HttpError, LastIDError};
///
/// fn handle_error(err: LastIDError) {
///     match err {
///         LastIDError::Http(HttpError::RateLimited {
///             retry_after_seconds,
///         }) => {
///             println!(
///                 "Rate limited, retry after {} seconds",
///                 retry_after_seconds
///             );
///         }
///         LastIDError::TrustRegistry(e) => {
///             println!("Trust registry error: {}", e);
///         }
///         LastIDError::Timeout(seconds) => {
///             println!("Request timed out after {} seconds", seconds);
///         }
///         _ => println!("Error: {}", err),
///     }
/// }
/// ```
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum LastIDError {
    /// Configuration error (missing or invalid config)
    #[error("Configuration error: {0}")]
    Config(String),

    /// HTTP request failed
    #[error("HTTP request failed: {0}")]
    Http(#[from] HttpError),

    /// Trust registry error
    #[error("Trust registry error: {0}")]
    TrustRegistry(#[from] TrustRegistryError),

    /// Cryptographic operation failed
    #[error("Cryptographic operation failed: {0}")]
    Crypto(String),

    /// Invalid credential (parsing or validation failed)
    #[error("Invalid credential: {0}")]
    InvalidCredential(String),

    /// Request timeout
    #[error("Request timeout after {0} seconds")]
    Timeout(u64),

    /// Policy validation failed
    #[error("Policy validation failed: {0}")]
    PolicyValidation(String),

    /// Serialization error
    #[error("Serialization error: {0}")]
    Serialization(#[from] serde_json::Error),

    /// IO error
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),

    /// TOML parsing error
    #[error("TOML parsing error: {0}")]
    TomlParse(#[from] toml::de::Error),

    /// WebSocket error
    #[cfg(feature = "websocket")]
    #[error("WebSocket error: {0}")]
    WebSocket(#[from] WebSocketError),

    /// Invalid DID format
    #[error("Invalid DID format: {0}")]
    InvalidDid(String),

    /// Invalid client ID (empty or whitespace-only)
    #[error("Client ID cannot be empty")]
    EmptyClientId,

    /// HTTP URL not allowed in production (SEC-001)
    #[error("HTTP URLs are not allowed; use HTTPS (except localhost for dev): {0}")]
    InsecureUrl(String),

    /// Clock skew exceeded tolerance (SEC-005)
    #[error("Clock skew exceeded {tolerance_seconds}s tolerance")]
    ClockSkewExceeded {
        /// Maximum allowed clock skew in seconds
        tolerance_seconds: u64,
    },

    /// `DPoP` token binding mismatch (SEC-004)
    #[error("Token binding mismatch: expected thumbprint {expected}, got {actual}")]
    TokenBindingMismatch {
        /// Expected JWK thumbprint from our `DPoP` key
        expected: String,
        /// Actual JWK thumbprint from token's cnf.jkt claim
        actual: String,
    },
}

impl LastIDError {
    /// Create a configuration error.
    #[must_use]
    pub fn config(message: impl Into<String>) -> Self {
        Self::Config(message.into())
    }

    /// Create a crypto error.
    #[must_use]
    pub fn crypto(message: impl Into<String>) -> Self {
        Self::Crypto(message.into())
    }

    /// Create an invalid credential error.
    #[must_use]
    pub fn invalid_credential(message: impl Into<String>) -> Self {
        Self::InvalidCredential(message.into())
    }

    /// Create a credential parsing error.
    ///
    /// This is an alias for `invalid_credential` used specifically when
    /// `TryFrom` conversions fail due to type mismatch or missing required
    /// fields.
    ///
    /// # Example
    ///
    /// ```rust
    /// use lastid_sdk::LastIDError;
    ///
    /// let err = LastIDError::credential_parse("Expected LastID.Persona, got LastID.Employment");
    /// assert!(err.to_string().contains("LastID.Persona"));
    /// ```
    #[must_use]
    pub fn credential_parse(message: impl Into<String>) -> Self {
        Self::InvalidCredential(message.into())
    }

    /// Create a policy validation error.
    #[must_use]
    pub fn policy_validation(message: impl Into<String>) -> Self {
        Self::PolicyValidation(message.into())
    }

    /// Check if this error is retryable.
    #[must_use]
    #[allow(clippy::missing_const_for_fn)] // Cannot be const: calls methods on nested types
    pub fn is_retryable(&self) -> bool {
        match self {
            Self::Http(e) => e.is_retryable(),
            Self::TrustRegistry(e) => e.is_retryable(),
            Self::Timeout(_) => true,
            _ => false,
        }
    }

    /// Get suggested retry delay in milliseconds.
    ///
    /// Returns `Some(delay)` for retryable errors with a recommended wait time.
    /// Returns `None` for non-retryable errors.
    ///
    /// # Retry Strategy Hints
    ///
    /// - Rate limited: Use the `retry_after` value from the response
    /// - Network errors: Start with short delays (100-500ms), use exponential
    ///   backoff
    /// - Server errors (5xx): Use moderate delays (1-5s), with exponential
    ///   backoff
    /// - Timeout: Increase timeout or use exponential backoff
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use lastid_sdk::LastIDError;
    ///
    /// fn should_retry(err: &LastIDError, attempt: u32) -> Option<u64> {
    ///     if !err.is_retryable() || attempt >= 3 {
    ///         return None;
    ///     }
    ///     err.suggested_retry_delay_ms()
    /// }
    /// ```
    #[must_use]
    #[allow(clippy::missing_const_for_fn)] // Cannot be const: calls methods on nested types
    pub fn suggested_retry_delay_ms(&self) -> Option<u64> {
        match self {
            Self::Http(e) => e.suggested_retry_delay_ms(),
            Self::TrustRegistry(e) => e.suggested_retry_delay_ms(),
            Self::Timeout(_) => Some(2000), // 2 second base delay for timeouts
            _ => None,
        }
    }

    /// Get the error category for logging and metrics.
    ///
    /// Returns a static string describing the error category,
    /// useful for structured logging and metrics.
    #[must_use]
    pub const fn category(&self) -> &'static str {
        match self {
            Self::Config(_) => "config",
            Self::Http(_) => "http",
            Self::TrustRegistry(_) => "trust_registry",
            Self::Crypto(_) => "crypto",
            Self::InvalidCredential(_) => "invalid_credential",
            Self::Timeout(_) => "timeout",
            Self::PolicyValidation(_) => "policy_validation",
            Self::Serialization(_) => "serialization",
            Self::Io(_) => "io",
            Self::TomlParse(_) => "toml_parse",
            #[cfg(feature = "websocket")]
            Self::WebSocket(_) => "websocket",
            Self::InvalidDid(_) => "invalid_did",
            Self::EmptyClientId => "empty_client_id",
            Self::InsecureUrl(_) => "insecure_url",
            Self::ClockSkewExceeded { .. } => "clock_skew",
            Self::TokenBindingMismatch { .. } => "token_binding",
        }
    }

    /// Create an invalid DID error.
    #[must_use]
    pub fn invalid_did(did: impl Into<String>) -> Self {
        Self::InvalidDid(did.into())
    }

    /// Create an insecure URL error.
    #[must_use]
    pub fn insecure_url(url: impl Into<String>) -> Self {
        Self::InsecureUrl(url.into())
    }

    /// Create a clock skew exceeded error.
    #[must_use]
    pub const fn clock_skew_exceeded(tolerance_seconds: u64) -> Self {
        Self::ClockSkewExceeded { tolerance_seconds }
    }

    /// Create a token binding mismatch error.
    #[must_use]
    pub fn token_binding_mismatch(expected: impl Into<String>, actual: impl Into<String>) -> Self {
        Self::TokenBindingMismatch {
            expected: expected.into(),
            actual: actual.into(),
        }
    }
}

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

    #[test]
    fn test_http_error_is_retryable() {
        assert!(HttpError::network("connection refused").is_retryable());
        assert!(HttpError::Timeout.is_retryable());
        assert!(HttpError::status(500, "internal error").is_retryable());
        assert!(HttpError::status(503, "unavailable").is_retryable());
        assert!(HttpError::rate_limited(60).is_retryable());

        assert!(!HttpError::status(400, "bad request").is_retryable());
        assert!(!HttpError::status(401, "unauthorized").is_retryable());
        assert!(!HttpError::status(404, "not found").is_retryable());
    }

    #[test]
    fn test_http_error_is_rate_limited() {
        assert!(HttpError::rate_limited(60).is_rate_limited());
        assert!(HttpError::status(429, "too many requests").is_rate_limited());
        assert!(!HttpError::status(500, "error").is_rate_limited());
    }

    #[test]
    fn test_lastid_error_is_retryable() {
        assert!(LastIDError::Http(HttpError::Timeout).is_retryable());
        assert!(LastIDError::Timeout(300).is_retryable());
        assert!(!LastIDError::config("missing").is_retryable());
        assert!(!LastIDError::crypto("invalid key").is_retryable());
    }

    #[test]
    fn test_trust_registry_error_is_retryable() {
        assert!(TrustRegistryError::Http(HttpError::Timeout).is_retryable());
        assert!(!TrustRegistryError::issuer_not_found("did:test").is_retryable());
        assert!(!TrustRegistryError::invalid_status(IssuerStatus::Suspended).is_retryable());
    }

    #[test]
    fn test_http_error_suggested_retry_delay() {
        // Network errors: 500ms
        assert_eq!(
            HttpError::network("err").suggested_retry_delay_ms(),
            Some(500)
        );

        // Timeout: 1000ms
        assert_eq!(HttpError::Timeout.suggested_retry_delay_ms(), Some(1000));

        // 5xx errors: 2000ms
        assert_eq!(
            HttpError::status(500, "error").suggested_retry_delay_ms(),
            Some(2000)
        );
        assert_eq!(
            HttpError::status(503, "error").suggested_retry_delay_ms(),
            Some(2000)
        );

        // Rate limited: retry_after * 1000
        assert_eq!(
            HttpError::rate_limited(60).suggested_retry_delay_ms(),
            Some(60_000)
        );

        // 4xx errors: None (not retryable)
        assert_eq!(
            HttpError::status(400, "error").suggested_retry_delay_ms(),
            None
        );
        assert_eq!(
            HttpError::status(401, "error").suggested_retry_delay_ms(),
            None
        );
        assert_eq!(
            HttpError::status(404, "error").suggested_retry_delay_ms(),
            None
        );
    }

    #[test]
    fn test_lastid_error_suggested_retry_delay() {
        // HTTP errors delegate to HttpError
        assert_eq!(
            LastIDError::Http(HttpError::Timeout).suggested_retry_delay_ms(),
            Some(1000)
        );

        // Timeout errors: 2000ms
        assert_eq!(
            LastIDError::Timeout(300).suggested_retry_delay_ms(),
            Some(2000)
        );

        // Non-retryable errors: None
        assert_eq!(
            LastIDError::config("error").suggested_retry_delay_ms(),
            None
        );
        assert_eq!(
            LastIDError::crypto("error").suggested_retry_delay_ms(),
            None
        );
    }

    #[test]
    fn test_trust_registry_error_suggested_retry_delay() {
        // HTTP errors delegate
        assert_eq!(
            TrustRegistryError::Http(HttpError::Timeout).suggested_retry_delay_ms(),
            Some(1000)
        );

        // Non-retryable errors: None
        assert_eq!(
            TrustRegistryError::issuer_not_found("did:test").suggested_retry_delay_ms(),
            None
        );
    }

    #[test]
    fn test_http_error_status_code() {
        assert_eq!(HttpError::status(404, "not found").status_code(), Some(404));
        assert_eq!(HttpError::rate_limited(60).status_code(), Some(429));
        assert_eq!(HttpError::network("error").status_code(), None);
        assert_eq!(HttpError::Timeout.status_code(), None);
    }

    #[test]
    fn test_error_categories() {
        // LastIDError categories
        assert_eq!(LastIDError::config("error").category(), "config");
        assert_eq!(LastIDError::Http(HttpError::Timeout).category(), "http");
        assert_eq!(LastIDError::Timeout(60).category(), "timeout");

        // HttpError categories
        assert_eq!(HttpError::network("error").category(), "network");
        assert_eq!(HttpError::Timeout.category(), "timeout");
        assert_eq!(HttpError::rate_limited(60).category(), "rate_limited");
        assert_eq!(HttpError::status(500, "error").category(), "server_error");
        assert_eq!(HttpError::status(400, "error").category(), "client_error");

        // TrustRegistryError categories
        assert_eq!(
            TrustRegistryError::issuer_not_found("did").category(),
            "issuer_not_found"
        );
        assert_eq!(
            TrustRegistryError::invalid_status(IssuerStatus::Suspended).category(),
            "invalid_issuer_status"
        );
    }
}
