use thiserror::Error;

use crate::types::{ClientId, GroupName, NamespaceId, TopicName, TopicOffset};

/// Unified error type for the queue server.
#[derive(Debug, Error)]
pub enum Error {
    #[error("namespace not found: {0:?}")]
    NamespaceNotFound(NamespaceId),

    #[error("topic not found: {0:?}")]
    TopicNotFound(TopicName),

    #[error("authentication failed")]
    AuthFailed,

    #[error("permission denied")]
    PermissionDenied,

    #[error("queue full: namespace {namespace:?}, topic {topic:?}")]
    QueueFull {
        namespace: NamespaceId,
        topic: TopicName,
    },

    #[error("client {client:?} is not subscribed to topic {topic:?}")]
    NotSubscribed { client: ClientId, topic: TopicName },

    #[error("client {client:?} is already subscribed to topic {topic:?}")]
    DuplicateSubscription { client: ClientId, topic: TopicName },

    #[error("consumer group {group:?} not found on topic {topic:?}")]
    GroupNotFound { group: GroupName, topic: TopicName },

    #[error("codec error: {0}")]
    Codec(String),

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

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

    #[error("TLS error: {0}")]
    Tls(String),

    #[error("serialization error: {0}")]
    Serialization(String),

    #[error("resource exhausted: {0}")]
    ResourceExhausted(String),

    #[error(
        "message at offset {offset} is {message_bytes} bytes but fetch response budget is {budget_bytes} bytes"
    )]
    MessageTooLarge {
        offset: TopicOffset,
        message_bytes: usize,
        budget_bytes: usize,
    },

    #[error("invalid topic: {0}")]
    InvalidTopic(String),

    #[error("invalid operation: {0}")]
    InvalidOperation(String),
}

impl Error {
    /// Returns `true` if this error indicates a message exceeded the fetch response budget.
    pub fn is_message_too_large(&self) -> bool {
        matches!(self, Error::MessageTooLarge { .. })
    }
}

impl From<serde_json::Error> for Error {
    fn from(e: serde_json::Error) -> Self {
        Self::Serialization(e.to_string())
    }
}

impl From<toml::de::Error> for Error {
    fn from(e: toml::de::Error) -> Self {
        Self::Config(e.to_string())
    }
}

impl From<bincode::Error> for Error {
    fn from(e: bincode::Error) -> Self {
        Self::Serialization(e.to_string())
    }
}

pub type Result<T> = std::result::Result<T, Error>;

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

    #[test]
    fn error_display_messages() {
        let e = Error::AuthFailed;
        assert_eq!(e.to_string(), "authentication failed");

        let e = Error::PermissionDenied;
        assert_eq!(e.to_string(), "permission denied");

        let e = Error::QueueFull {
            namespace: NamespaceId("ns1".into()),
            topic: TopicName("events".into()),
        };
        assert_eq!(
            e.to_string(),
            r#"queue full: namespace NamespaceId("ns1"), topic TopicName("events")"#
        );

        let e = Error::Codec("bad frame".into());
        assert_eq!(e.to_string(), "codec error: bad frame");

        let e = Error::Config("missing field".into());
        assert_eq!(e.to_string(), "config error: missing field");

        let e = Error::Serialization("unexpected token".into());
        assert_eq!(e.to_string(), "serialization error: unexpected token");

        let e = Error::ResourceExhausted("global memory budget exceeded".into());
        assert_eq!(
            e.to_string(),
            "resource exhausted: global memory budget exceeded"
        );
    }

    #[test]
    fn io_error_converts() {
        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "gone");
        let err: Error = io_err.into();
        assert!(matches!(err, Error::Storage(_)));
        assert!(err.to_string().contains("gone"));
    }

    #[test]
    fn serde_json_error_converts() {
        let json_err = serde_json::from_str::<serde_json::Value>("{{bad").unwrap_err();
        let err: Error = json_err.into();
        assert!(matches!(err, Error::Serialization(_)));
    }

    #[test]
    fn bincode_error_converts() {
        // Force a bincode deserialization error by providing invalid data
        let result: std::result::Result<String, _> = bincode::deserialize(&[255, 255, 255, 255]);
        let bincode_err = result.unwrap_err();
        let err: Error = bincode_err.into();
        assert!(matches!(err, Error::Serialization(_)));
    }

    #[test]
    fn toml_error_converts() {
        let toml_err = toml::from_str::<toml::Value>("= bad").unwrap_err();
        let err: Error = toml_err.into();
        assert!(matches!(err, Error::Config(_)));
    }
}
