//! The single error type surfaced by every fallible client method.
//!
//! [`ImaigeError`] collapses transport, protocol, and API faults into a
//! small, stable enum that maps cleanly across every binding language. The
//! variants intentionally mirror the categories a consumer dispatches on:
//! authentication, rate-limiting (with a retry hint), request validation,
//! generic HTTP failures, timeouts, response-decoding faults, and SSE
//! stream faults.

/// Error returned by all [`crate::ImaigeClient`] operations.
#[derive(Debug, thiserror::Error)]
pub enum ImaigeError {
    /// The API key was missing, malformed, or rejected (HTTP 401 / 403).
    #[error("authentication failed: {message}")]
    Auth {
        /// Human-readable detail from the server, if any.
        message: String,
    },

    /// The caller exceeded a rate or spend cap (HTTP 429 / 402). When the
    /// server supplied a `Retry-After` header it is surfaced in milliseconds.
    #[error("rate limited{}", match .retry_after_ms { Some(ms) => format!(" (retry after {ms}ms)"), None => String::new() })]
    RateLimit {
        /// Suggested wait before retrying, in milliseconds, if known.
        retry_after_ms: Option<u64>,
        /// Human-readable detail from the server, if any.
        message: String,
    },

    /// The request body failed server-side validation (HTTP 4xx, typically
    /// 400 / 422) for a non-auth, non-rate-limit reason.
    #[error("validation error: {message}")]
    Validation {
        /// Human-readable detail from the server, if any.
        message: String,
    },

    /// Any other non-success HTTP status the more specific variants did not
    /// claim. `status` carries the raw code so callers can branch on it.
    #[error("http error {status}: {message}")]
    Http {
        /// The HTTP status code returned by the server.
        status: u16,
        /// The response body (or a transport-level description).
        message: String,
    },

    /// The request (or a long poll) exceeded its deadline.
    #[error("request timed out: {message}")]
    Timeout {
        /// Detail about which operation timed out.
        message: String,
    },

    /// A successful HTTP response could not be deserialized into the
    /// expected shape.
    #[error("failed to decode response: {message}")]
    Decode {
        /// The decode failure detail.
        message: String,
    },

    /// The Server-Sent-Events stream faulted (connection dropped, malformed
    /// frame, or the server emitted a terminal error event).
    #[error("event stream error: {message}")]
    Stream {
        /// The stream failure detail.
        message: String,
    },
}

impl ImaigeError {
    /// Build the right variant from an HTTP status code, an optional
    /// `Retry-After` value (already converted to milliseconds), and a body.
    pub(crate) fn from_status(status: u16, retry_after_ms: Option<u64>, body: String) -> Self {
        match status {
            401 | 403 => ImaigeError::Auth { message: body },
            402 | 429 => ImaigeError::RateLimit {
                retry_after_ms,
                message: body,
            },
            400 | 422 => ImaigeError::Validation { message: body },
            408 | 504 => ImaigeError::Timeout { message: body },
            other => ImaigeError::Http {
                status: other,
                message: body,
            },
        }
    }
}

impl From<reqwest::Error> for ImaigeError {
    fn from(err: reqwest::Error) -> Self {
        if err.is_timeout() {
            return ImaigeError::Timeout {
                message: err.to_string(),
            };
        }
        if err.is_decode() {
            return ImaigeError::Decode {
                message: err.to_string(),
            };
        }
        if let Some(status) = err.status() {
            return ImaigeError::from_status(status.as_u16(), None, err.to_string());
        }
        ImaigeError::Http {
            status: 0,
            message: err.to_string(),
        }
    }
}

impl From<serde_json::Error> for ImaigeError {
    fn from(err: serde_json::Error) -> Self {
        ImaigeError::Decode {
            message: err.to_string(),
        }
    }
}

/// Convenience result alias used throughout the crate.
pub type Result<T> = std::result::Result<T, ImaigeError>;
