//! `ProviderError` taxonomy shared across every LLM provider impl.
//!
//! Each provider maps native errors onto these variants so the matcher
//! and runner can branch on the variant rather than parsing free-text
//! messages. See ADR-0020 for the mapping rules per provider.

use std::time::Duration;

use thiserror::Error;

/// Errors returned by any [`super::LlmProvider`] call.
///
/// New variants append at the end; the enum is `#[non_exhaustive]` so a
/// downstream `match` on `_` keeps compiling.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ProviderError {
    /// Authentication failed (HTTP 401 from a remote provider, missing
    /// key on a local provider that requires one).
    #[error("authentication failed: {0}")]
    AuthFailure(String),

    /// Provider rate-limited the request. `retry_after` carries the
    /// duration the server asked us to wait, when known.
    #[error("rate limited; retry after {retry_after:?}")]
    RateLimit {
        /// Server-advertised retry delay, parsed from the `retry-after`
        /// header when present.
        retry_after: Option<Duration>,
    },

    /// Request exceeded the configured timeout.
    #[error("request timed out after {0:?}")]
    Timeout(Duration),

    /// Provider returned a payload we could not parse.
    #[error("malformed response: {0}")]
    MalformedResponse(String),

    /// Request was rejected before the model ran (HTTP 400, bad model
    /// name, schema mismatch).
    #[error("invalid request: {0}")]
    InvalidRequest(String),

    /// Provider returned a 5xx or otherwise internal failure.
    #[error("provider returned internal error: {0}")]
    ProviderInternal(String),

    /// HTTP transport itself failed (DNS, TLS, connection refused).
    #[error("transport error: {0}")]
    Transport(String),

    /// The provider does not offer the requested capability (for example,
    /// Anthropic exposes no embeddings endpoint, so `embed` is unsupported).
    /// A permanent failure; the runner surfaces it rather than retrying.
    #[error("feature unsupported by {provider}: {feature}")]
    FeatureUnsupported {
        /// Provider that lacks the capability (kebab-case wire name).
        provider: &'static str,
        /// The capability the caller asked for.
        feature: &'static str,
    },
}

impl ProviderError {
    /// Whether the runner should retry against this error variant.
    ///
    /// Retry on transient failures (rate limit, timeout, internal,
    /// transport); never retry on permanent failures (auth, invalid
    /// request, malformed response).
    pub fn is_retryable(&self) -> bool {
        matches!(
            self,
            ProviderError::RateLimit { .. }
                | ProviderError::Timeout(_)
                | ProviderError::ProviderInternal(_)
                | ProviderError::Transport(_)
        )
    }
}

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

    #[test]
    fn rate_limit_and_timeout_are_retryable() {
        assert!(ProviderError::RateLimit { retry_after: None }.is_retryable());
        assert!(ProviderError::Timeout(Duration::from_secs(1)).is_retryable());
        assert!(ProviderError::ProviderInternal("500".into()).is_retryable());
        assert!(ProviderError::Transport("dns".into()).is_retryable());
    }

    #[test]
    fn auth_and_invalid_request_are_not_retryable() {
        assert!(!ProviderError::AuthFailure("nope".into()).is_retryable());
        assert!(!ProviderError::InvalidRequest("bad model".into()).is_retryable());
        assert!(!ProviderError::MalformedResponse("bad json".into()).is_retryable());
    }

    #[test]
    fn feature_unsupported_is_not_retryable() {
        let err = ProviderError::FeatureUnsupported {
            provider: "anthropic",
            feature: "embeddings",
        };
        assert!(!err.is_retryable());
        assert!(format!("{err}").contains("anthropic"));
    }

    #[test]
    fn display_format_includes_payload() {
        let s = format!("{}", ProviderError::AuthFailure("bad key".into()));
        assert!(s.contains("authentication failed"));
        assert!(s.contains("bad key"));
    }
}
