//! Error types for the ZeroID SDK.

use thiserror::Error;

/// All errors returned by [`ZeroIDClient`](crate::zeroid::ZeroIDClient).
///
/// Every variant that originates from an API response exposes a [`code()`](ZeroIDError::code)
/// method with a machine-readable error string (e.g. `"invalid_grant"`, `"policy_violation"`).
#[derive(Debug, Error)]
pub enum ZeroIDError {
    /// The API returned an HTTP error response.
    ///
    /// For OAuth2 token endpoint errors the `title` field contains the OAuth2
    /// error code (e.g. `"invalid_grant"`) and `code` is `Some(title.clone())`.
    /// For RFC 9457 admin-endpoint errors `code` is `None`.
    #[error("[{status}] {title}: {detail}")]
    ApiError {
        /// HTTP status code.
        status: u16,
        /// Short title — for OAuth2 errors this is the error code string.
        title: String,
        /// Human-readable explanation.
        detail: String,
        /// Machine-readable error code. Set for OAuth2 token errors; `None` for
        /// RFC 9457 Problem Details responses from admin endpoints.
        code: Option<String>,
    },

    /// HTTP transport error (connection refused, timeout, TLS, ...).
    ///
    /// [`code()`](ZeroIDError::code) returns `"network_error"` for this variant.
    #[error("http error: {0}")]
    HttpError(#[from] reqwest::Error),

    /// 404 Not Found.
    #[error("not found: {0}")]
    NotFound(String),

    /// 409 Conflict.
    #[error("conflict: {0}")]
    Conflict(String),

    /// Response body could not be deserialised.
    #[error("serialisation error: {0}")]
    SerdeError(#[from] serde_json::Error),
}

impl ZeroIDError {
    /// Returns the machine-readable error code for this error.
    ///
    /// | Variant | Returned code |
    /// |---------|---------------|
    /// | `ApiError` with OAuth2 body | The `error` field value (e.g. `"invalid_grant"`) |
    /// | `ApiError` without OAuth2 body | `"api_error"` |
    /// | `HttpError` | `"network_error"` |
    /// | `NotFound` | `"not_found"` |
    /// | `Conflict` | `"already_exists"` |
    /// | `SerdeError` | `"parse_error"` |
    pub fn code(&self) -> &str {
        match self {
            ZeroIDError::ApiError { code: Some(c), .. } => c.as_str(),
            ZeroIDError::ApiError { code: None, .. } => "api_error",
            ZeroIDError::HttpError(_) => "network_error",
            ZeroIDError::NotFound(_) => "not_found",
            ZeroIDError::Conflict(_) => "already_exists",
            ZeroIDError::SerdeError(_) => "parse_error",
        }
    }

    /// Returns the HTTP status code if this is an API-level error.
    pub fn status(&self) -> Option<u16> {
        match self {
            ZeroIDError::ApiError { status, .. } => Some(*status),
            ZeroIDError::NotFound(_) => Some(404),
            ZeroIDError::Conflict(_) => Some(409),
            _ => None,
        }
    }

    /// Returns `true` if this is a 404 Not Found error.
    pub fn is_not_found(&self) -> bool {
        matches!(self, ZeroIDError::NotFound(_))
    }

    /// Returns `true` if this is a 409 Conflict error.
    pub fn is_conflict(&self) -> bool {
        matches!(self, ZeroIDError::Conflict(_))
    }

    /// Returns `true` if this is a transport-level error (timeout, connection refused).
    pub fn is_network_error(&self) -> bool {
        matches!(self, ZeroIDError::HttpError(_))
    }
}
