use std::time::Duration;

use thiserror::Error;

/// All errors returned by the Anakin SDK. Match on a specific variant to
/// react to a particular failure mode (e.g. [`Error::InsufficientCredits`]),
/// or treat the enum as opaque for a generic fallback.
#[derive(Debug, Error)]
pub enum Error {
    /// 401 — invalid or missing API key.
    #[error("authentication failed: {message}")]
    Authentication {
        message: String,
        status: u16,
        code: Option<String>,
    },

    /// 402 — out of credits. `balance` is the caller's current balance,
    /// `required` is the credit cost the failed request would have incurred.
    #[error("insufficient credits: balance={balance}, required={required} ({message})")]
    InsufficientCredits {
        message: String,
        status: u16,
        code: Option<String>,
        balance: i64,
        required: i64,
    },

    /// 400 — validation failure.
    #[error("invalid request: {message}")]
    InvalidRequest {
        message: String,
        status: u16,
        code: Option<String>,
    },

    /// 429 — rate limited after the SDK exhausted its retry budget.
    /// `retry_after` is the duration the API asked the client to wait.
    #[error("rate limited (retry_after={retry_after:?}): {message}")]
    RateLimit {
        message: String,
        status: u16,
        code: Option<String>,
        retry_after: Duration,
    },

    /// Polled job came back with status="failed".
    #[error("job failed: {reason} (job_id={job_id:?})")]
    JobFailed {
        job_id: Option<String>,
        reason: String,
    },

    /// Polling budget exhausted before the job reached a terminal status.
    #[error("polling timed out after {elapsed:?} (job_id={job_id:?})")]
    JobTimeout {
        job_id: Option<String>,
        elapsed: Duration,
    },

    /// 5xx after retries were exhausted.
    #[error("server error {status}: {message}")]
    Server {
        message: String,
        status: u16,
        code: Option<String>,
    },

    /// DNS / connect / read-timeout failure from the underlying HTTP client.
    #[error("network error: {message}")]
    Network {
        message: String,
        #[source]
        source: Option<Box<dyn std::error::Error + Send + Sync>>,
    },

    /// Any other error: serialization, unexpected response shape, etc.
    #[error("anakin: {0}")]
    Other(String),
}

impl Error {
    /// HTTP status code if this came from the API, else 0.
    pub fn status(&self) -> u16 {
        match self {
            Error::Authentication { status, .. }
            | Error::InsufficientCredits { status, .. }
            | Error::InvalidRequest { status, .. }
            | Error::RateLimit { status, .. }
            | Error::Server { status, .. } => *status,
            _ => 0,
        }
    }

    /// API machine-readable error code if present.
    pub fn code(&self) -> Option<&str> {
        match self {
            Error::Authentication { code, .. }
            | Error::InsufficientCredits { code, .. }
            | Error::InvalidRequest { code, .. }
            | Error::RateLimit { code, .. }
            | Error::Server { code, .. } => code.as_deref(),
            _ => None,
        }
    }
}

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