use thiserror::Error;

#[derive(Error, Debug)]
pub enum NlshError {
    #[error("failed to connect to {provider}: {message}")]
    ConnectionFailed { provider: String, message: String },

    #[error("auth failed: invalid API key")]
    InvalidApiKey,

    #[error("auth failed: {message}")]
    AuthenticationFailed { message: String },

    #[error("model not found: {0}")]
    ModelNotFound(String),

    #[error("rate limit exceeded{}", if retry_after.is_some() { format!("; retry after {} seconds", retry_after.unwrap()) } else { "; please try again later".to_string() })]
    RateLimitExceeded { retry_after: Option<u64> },

    #[error("server error from {provider}: {message}")]
    ServerError { provider: String, message: String },

    #[error("request timeout after {seconds} seconds")]
    Timeout { seconds: u64 },

    #[error("invalid response from API: {0}")]
    InvalidResponse(String),

    #[error("network error: {0}")]
    NetworkError(String),

    #[error("config error: {0}")]
    ConfigError(String),

    #[error("io error: {0}")]
    IoError(#[from] std::io::Error),

    #[error("request cancelled")]
    Cancelled,

    #[error("empty response from {0}")]
    EmptyResponse(String),
}

impl NlshError {
    pub fn connection_failed(provider: impl Into<String>, message: impl Into<String>) -> Self {
        NlshError::ConnectionFailed {
            provider: provider.into(),
            message: message.into(),
        }
    }

    pub fn server_error(provider: impl Into<String>, message: impl Into<String>) -> Self {
        NlshError::ServerError {
            provider: provider.into(),
            message: message.into(),
        }
    }

    pub fn timeout(seconds: u64) -> Self {
        NlshError::Timeout { seconds }
    }

    pub fn auth_failed(message: impl Into<String>) -> Self {
        NlshError::AuthenticationFailed {
            message: message.into(),
        }
    }

    pub fn from_http_status(status: reqwest::StatusCode, provider: &str, body: &str) -> NlshError {
        match status.as_u16() {
            401 | 403 => {
                if body.contains("key") || body.contains("api") || body.contains("token") {
                    NlshError::InvalidApiKey
                } else {
                    NlshError::auth_failed(body)
                }
            }
            404 => {
                if body.contains("model") {
                    NlshError::ModelNotFound(body.to_string())
                } else {
                    NlshError::InvalidResponse(format!("endpoint not found: {}", body))
                }
            }
            429 => {
                let retry_after = if body.contains("retry") {
                    body.split("retry in ")
                        .nth(1)
                        .and_then(|s| s.split('s').next())
                        .and_then(|s| s.parse::<f64>().ok())
                        .map(|f| f.ceil() as u64)
                } else {
                    None
                };
                NlshError::RateLimitExceeded { retry_after }
            }
            500..=599 => NlshError::server_error(provider, body),
            _ => NlshError::InvalidResponse(format!("{}: {}", status, body)),
        }
    }

    pub fn from_reqwest(error: reqwest::Error, provider: &str) -> NlshError {
        if error.is_timeout() {
            NlshError::timeout(30)
        } else if error.is_connect() {
            NlshError::connection_failed(
                provider,
                "check if the service is running and the URL is correct",
            )
        } else if error.is_request() {
            NlshError::NetworkError("invalid request".to_string())
        } else if let Some(status) = error.status() {
            NlshError::from_http_status(status, provider, &error.to_string())
        } else {
            NlshError::NetworkError(error.to_string())
        }
    }

    /// Turn the error into a simple user-facing string using the standard
    /// `Display` implementation generated by [`thiserror`].  callers still use
    /// this helper so they don’t have to call `to_string()` directly.
    pub fn user_message(&self) -> String {
        self.to_string()
    }

    /// Print the error using CLI styling (red "error:" prefix) so callers
    /// don’t have to repeat the `print_error` boilerplate.
    pub fn print(&self) {
        crate::cli::print_error(&self.user_message());
    }
}

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

    #[test]
    fn user_message_examples() {
        let err = NlshError::ConnectionFailed {
            provider: "ollama".into(),
            message: "cannot connect".into(),
        };
        assert_eq!(
            err.user_message(),
            "failed to connect to ollama: cannot connect"
        );

        let err = NlshError::RateLimitExceeded {
            retry_after: Some(10),
        };
        assert_eq!(
            err.user_message(),
            "rate limit exceeded; retry after 10 seconds"
        );

        let err = NlshError::RateLimitExceeded { retry_after: None };
        assert_eq!(
            err.user_message(),
            "rate limit exceeded; please try again later"
        );
    }
}
