//! Structured error types for JavaScript interop.
//!
//! Provides error codes and structured error information for TypeScript
//! developers.

#![cfg(target_arch = "wasm32")]

use wasm_bindgen::prelude::*;

/// Error codes for programmatic error handling in JavaScript.
///
/// These codes allow TypeScript developers to handle errors programmatically
/// without parsing error message strings.
#[wasm_bindgen]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorCode {
    /// Configuration error (invalid endpoint, missing client_id, etc.)
    ConfigError = 1,
    /// Network error (connection failed, timeout, DNS resolution)
    NetworkError = 2,
    /// Authentication error (invalid credentials, token expired)
    AuthError = 3,
    /// Policy validation error (missing required fields, invalid constraints)
    PolicyError = 4,
    /// Credential verification error (signature invalid, expired, revoked)
    VerificationError = 5,
    /// Trust registry error (issuer not found, suspended, unauthorized)
    TrustError = 6,
    /// Request not found (404 from IDP)
    NotFoundError = 7,
    /// Request expired (credential request timed out)
    ExpiredError = 8,
    /// Request denied by user
    DeniedError = 9,
    /// Rate limited by IDP
    RateLimitError = 10,
    /// Internal SDK error (unexpected state)
    InternalError = 99,
}

impl ErrorCode {
    /// Get the string representation of the error code.
    #[must_use]
    pub fn as_str(&self) -> &'static str {
        match self {
            Self::ConfigError => "CONFIG_ERROR",
            Self::NetworkError => "NETWORK_ERROR",
            Self::AuthError => "AUTH_ERROR",
            Self::PolicyError => "POLICY_ERROR",
            Self::VerificationError => "VERIFICATION_ERROR",
            Self::TrustError => "TRUST_ERROR",
            Self::NotFoundError => "NOT_FOUND_ERROR",
            Self::ExpiredError => "EXPIRED_ERROR",
            Self::DeniedError => "DENIED_ERROR",
            Self::RateLimitError => "RATE_LIMIT_ERROR",
            Self::InternalError => "INTERNAL_ERROR",
        }
    }
}

/// Structured error for JavaScript with code, message, and optional details.
///
/// This provides a better developer experience than plain string errors,
/// allowing programmatic error handling in TypeScript.
///
/// # JavaScript Usage
///
/// ```javascript
/// try {
///     const response = await client.requestCredential(policy);
/// } catch (error) {
///     if (error.code === 'NETWORK_ERROR') {
///         // Retry with backoff
///     } else if (error.code === 'POLICY_ERROR') {
///         // Show validation errors to user
///         console.error(error.details);
///     }
/// }
/// ```
#[wasm_bindgen(js_name = LastIDError)]
#[derive(Debug)]
pub struct WasmError {
    code: ErrorCode,
    message: String,
    details: Option<String>,
}

#[wasm_bindgen(js_class = LastIDError)]
impl WasmError {
    /// Create a new error.
    #[wasm_bindgen(constructor)]
    pub fn new(code: ErrorCode, message: String) -> Self {
        Self {
            code,
            message,
            details: None,
        }
    }

    /// Create an error with additional details.
    pub fn with_details(code: ErrorCode, message: String, details: String) -> Self {
        Self {
            code,
            message,
            details: Some(details),
        }
    }

    /// Get the error code string (e.g., "NETWORK_ERROR").
    #[wasm_bindgen(getter)]
    pub fn code(&self) -> String {
        self.code.as_str().to_string()
    }

    /// Get the numeric error code for switch statements.
    #[wasm_bindgen(getter, js_name = numericCode)]
    pub fn numeric_code(&self) -> u32 {
        self.code as u32
    }

    /// Get the human-readable error message.
    #[wasm_bindgen(getter)]
    pub fn message(&self) -> String {
        self.message.clone()
    }

    /// Get additional error details (if available).
    #[wasm_bindgen(getter)]
    pub fn details(&self) -> Option<String> {
        self.details.clone()
    }

    /// Check if this error is retryable.
    #[wasm_bindgen(getter, js_name = isRetryable)]
    pub fn is_retryable(&self) -> bool {
        matches!(
            self.code,
            ErrorCode::NetworkError | ErrorCode::RateLimitError
        )
    }

    /// Get a suggested retry delay in milliseconds (if retryable).
    #[wasm_bindgen(getter, js_name = suggestedRetryMs)]
    pub fn suggested_retry_ms(&self) -> Option<u32> {
        match self.code {
            ErrorCode::NetworkError => Some(1000),    // 1 second
            ErrorCode::RateLimitError => Some(30000), // 30 seconds
            _ => None,
        }
    }
}

impl std::fmt::Display for WasmError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "[{}] {}", self.code.as_str(), self.message)
    }
}

impl std::error::Error for WasmError {}

/// Convert from SDK error types to WASM errors.
impl From<crate::error::LastIDError> for WasmError {
    #[allow(unreachable_patterns)] // Catch-all for future #[non_exhaustive] variants
    fn from(err: crate::error::LastIDError) -> Self {
        use crate::error::LastIDError;

        let (code, message, details) = match &err {
            LastIDError::Config(msg) => (ErrorCode::ConfigError, msg.clone(), None),
            LastIDError::Http(http_err) => {
                let code = if http_err.is_rate_limited() {
                    ErrorCode::RateLimitError
                } else if http_err.status_code() == Some(404) {
                    ErrorCode::NotFoundError
                } else {
                    ErrorCode::NetworkError
                };
                (code, http_err.to_string(), None)
            }
            LastIDError::TrustRegistry(trust_err) => {
                (ErrorCode::TrustError, trust_err.to_string(), None)
            }
            LastIDError::Crypto(msg) => (ErrorCode::InternalError, msg.clone(), None),
            LastIDError::InvalidCredential(msg) => {
                (ErrorCode::VerificationError, msg.clone(), None)
            }
            LastIDError::Timeout(secs) => (
                ErrorCode::ExpiredError,
                format!("Request timed out after {secs} seconds"),
                None,
            ),
            LastIDError::PolicyValidation(msg) => (ErrorCode::PolicyError, msg.clone(), None),
            LastIDError::Serialization(e) => (ErrorCode::InternalError, e.to_string(), None),
            LastIDError::Io(e) => (ErrorCode::NetworkError, e.to_string(), None),
            LastIDError::TomlParse(e) => (ErrorCode::ConfigError, e.to_string(), None),
            LastIDError::InvalidDid(msg) => (ErrorCode::VerificationError, msg.clone(), None),
            LastIDError::EmptyClientId => (
                ErrorCode::ConfigError,
                "Client ID cannot be empty".to_string(),
                None,
            ),
            LastIDError::InsecureUrl(url) => (
                ErrorCode::ConfigError,
                format!("Insecure URL not allowed: {url}"),
                None,
            ),
            LastIDError::ClockSkewExceeded { tolerance_seconds } => (
                ErrorCode::AuthError,
                format!("Clock skew exceeded {tolerance_seconds}s tolerance"),
                None,
            ),
            LastIDError::TokenBindingMismatch { expected, actual } => (
                ErrorCode::AuthError,
                "Token binding mismatch".to_string(),
                Some(format!("expected: {expected}, actual: {actual}")),
            ),
            #[cfg(feature = "websocket")]
            LastIDError::WebSocket(e) => (ErrorCode::NetworkError, e.to_string(), None),
            // Handle any future variants gracefully
            _ => (ErrorCode::InternalError, err.to_string(), None),
        };

        if let Some(details) = details {
            Self::with_details(code, message, details)
        } else {
            Self::new(code, message)
        }
    }
}

/// Helper to convert SDK errors to JsError while preserving structure.
///
/// This creates a JavaScript Error object with additional properties
/// for the error code and details.
pub fn to_js_error(err: impl Into<WasmError>) -> JsError {
    let wasm_err = err.into();
    JsError::new(&format!(
        "[{}] {}",
        wasm_err.code.as_str(),
        wasm_err.message
    ))
}

/// Create a serialization error (internal error code).
///
/// Used for JSON serialization failures which are typically SDK bugs.
pub fn serialization_error(err: impl std::fmt::Display) -> JsError {
    JsError::new(&format!("[INTERNAL_ERROR] Serialization failed: {err}"))
}

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

    #[test]
    fn test_error_code_as_str() {
        assert_eq!(ErrorCode::ConfigError.as_str(), "CONFIG_ERROR");
        assert_eq!(ErrorCode::NetworkError.as_str(), "NETWORK_ERROR");
        assert_eq!(ErrorCode::PolicyError.as_str(), "POLICY_ERROR");
    }

    #[test]
    fn test_error_is_retryable() {
        let network_err = WasmError::new(ErrorCode::NetworkError, "Connection failed".into());
        assert!(network_err.is_retryable());

        let policy_err = WasmError::new(ErrorCode::PolicyError, "Invalid callback".into());
        assert!(!policy_err.is_retryable());
    }

    #[test]
    fn test_error_suggested_retry() {
        let rate_limit = WasmError::new(ErrorCode::RateLimitError, "Too many requests".into());
        assert_eq!(rate_limit.suggested_retry_ms(), Some(30000));

        let auth_err = WasmError::new(ErrorCode::AuthError, "Invalid token".into());
        assert_eq!(auth_err.suggested_retry_ms(), None);
    }
}
