//! HTTP client errors with retry classification.

use thiserror::Error;

/// HTTP client errors with retry classification.
///
/// Categorizes HTTP errors for proper retry handling:
/// - Network errors and timeouts are retryable
/// - 5xx server errors are retryable
/// - 429 rate limit responses include `retry_after_seconds`
/// - 4xx client errors are non-retryable
/// - CORS errors (WASM only) with remediation hints
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum HttpError {
    /// Network connectivity error (retryable)
    #[error("Network error: {0}")]
    Network(String),

    /// HTTP status error
    #[error("HTTP {status}: {message}")]
    Status {
        /// HTTP status code
        status: u16,
        /// Error message from response
        message: String,
    },

    /// Rate limited (retryable with backoff)
    #[error("Rate limited: retry after {retry_after_seconds}s")]
    RateLimited {
        /// Seconds to wait before retrying
        retry_after_seconds: u64,
    },

    /// Request timeout (retryable)
    #[error("Request timeout")]
    Timeout,

    /// CORS error (WASM only, not retryable)
    ///
    /// This error occurs when the browser blocks a cross-origin request.
    /// The IDP server must include proper CORS headers for the SDK to work in
    /// browsers.
    #[error("CORS error: {message}. {remediation}")]
    Cors {
        /// Error description
        message: String,
        /// Remediation hint
        remediation: String,
    },
}

impl HttpError {
    /// Check if this error is retryable.
    ///
    /// Returns `true` for:
    /// - Network errors
    /// - Timeouts
    /// - 5xx server errors
    /// - Rate limiting (after waiting)
    ///
    /// CORS errors are NOT retryable - they require server configuration
    /// changes.
    #[must_use]
    pub const fn is_retryable(&self) -> bool {
        match self {
            Self::Status { status, .. } => *status >= 500,
            Self::Network(_) | Self::Timeout | Self::RateLimited { .. } => true,
            Self::Cors { .. } => false, // CORS requires server config, not retryable
        }
    }

    /// Check if this error indicates rate limiting.
    #[must_use]
    pub const fn is_rate_limited(&self) -> bool {
        matches!(
            self,
            Self::RateLimited { .. } | Self::Status { status: 429, .. }
        )
    }

    /// Get retry-after seconds if rate limited.
    #[must_use]
    pub const fn retry_after(&self) -> Option<u64> {
        match self {
            Self::RateLimited {
                retry_after_seconds,
            } => Some(*retry_after_seconds),
            _ => None,
        }
    }

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

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

    /// Create a rate limited error.
    #[must_use]
    pub const fn rate_limited(retry_after_seconds: u64) -> Self {
        Self::RateLimited {
            retry_after_seconds,
        }
    }

    /// Get suggested retry delay in milliseconds.
    ///
    /// Returns appropriate delay based on error type:
    /// - Network errors: 500ms (short delay, transient issues)
    /// - Timeout: 1000ms (moderate delay)
    /// - 5xx errors: 2000ms (server needs time to recover)
    /// - Rate limited: `retry_after_seconds * 1000` (respect server's hint)
    /// - 4xx errors: None (not retryable)
    /// - CORS errors: None (requires server configuration)
    #[must_use]
    pub const fn suggested_retry_delay_ms(&self) -> Option<u64> {
        match self {
            Self::Network(_) => Some(500), // Short delay for transient network issues
            Self::Timeout => Some(1000),   // Moderate delay for timeouts
            Self::Status { status, .. } if *status >= 500 => Some(2000), // Server errors
            Self::RateLimited {
                retry_after_seconds,
            } => Some(*retry_after_seconds * 1000),
            Self::Status { .. } | Self::Cors { .. } => None, // Not retryable
        }
    }

    /// Get the HTTP status code if this is a status error.
    #[must_use]
    pub const fn status_code(&self) -> Option<u16> {
        match self {
            Self::Status { status, .. } => Some(*status),
            Self::RateLimited { .. } => Some(429),
            _ => None,
        }
    }

    /// Get the error category for metrics and logging.
    #[must_use]
    pub const fn category(&self) -> &'static str {
        match self {
            Self::Network(_) => "network",
            Self::Timeout => "timeout",
            Self::RateLimited { .. } => "rate_limited",
            Self::Status { status, .. } if *status >= 500 => "server_error",
            Self::Status { status, .. } if *status >= 400 => "client_error",
            Self::Status { .. } => "unknown_status",
            Self::Cors { .. } => "cors",
        }
    }

    /// Create a CORS error with remediation hints.
    ///
    /// Use this when a request fails due to CORS policy in WASM environments.
    ///
    /// # Example
    ///
    /// ```rust
    /// use lastid_sdk::HttpError;
    ///
    /// let err = HttpError::cors(
    ///     "Request blocked by CORS policy",
    ///     "https://human.lastid.co",
    /// );
    /// assert!(!err.is_retryable());
    /// ```
    #[must_use]
    pub fn cors(message: impl Into<String>, origin: &str) -> Self {
        Self::Cors {
            message: message.into(),
            remediation: format!(
                "Ensure the IDP server includes CORS headers: \
                 Access-Control-Allow-Origin: {origin}, \
                 Access-Control-Allow-Headers: Authorization, Content-Type, DPoP, \
                 Access-Control-Allow-Methods: GET, POST, OPTIONS"
            ),
        }
    }

    /// Create a CORS error with custom remediation.
    #[must_use]
    pub fn cors_with_remediation(
        message: impl Into<String>,
        remediation: impl Into<String>,
    ) -> Self {
        Self::Cors {
            message: message.into(),
            remediation: remediation.into(),
        }
    }

    /// Check if this error is a CORS error.
    #[must_use]
    pub const fn is_cors(&self) -> bool {
        matches!(self, Self::Cors { .. })
    }
}
