use std::fmt;

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

#[derive(Debug, Eq, PartialEq, thiserror::Error)]
pub(crate) enum Error {
    #[error("{0}")]
    InvalidInput(String),

    #[error("{0}")]
    ConfigFormat(String),

    #[error("{0}")]
    Detection(String),

    #[error("{0}")]
    Lsp(String),

    #[error("{0}")]
    Network(String),

    #[error("{0}")]
    MissingExecutable(String),

    #[error("{0}")]
    Unexpected(String),
}

impl Error {
    fn message(&self) -> &str {
        match self {
            Self::InvalidInput(message)
            | Self::ConfigFormat(message)
            | Self::Detection(message)
            | Self::Lsp(message)
            | Self::Network(message)
            | Self::MissingExecutable(message)
            | Self::Unexpected(message) => message,
        }
    }

    pub(crate) fn invalid_input(message: impl Into<String>) -> Self {
        Self::InvalidInput(message.into())
    }

    pub(crate) fn config_format(message: impl Into<String>) -> Self {
        Self::ConfigFormat(message.into())
    }

    pub(crate) fn detection(message: impl Into<String>) -> Self {
        Self::Detection(message.into())
    }

    pub(crate) fn lsp(message: impl Into<String>) -> Self {
        Self::Lsp(message.into())
    }

    pub(crate) fn network(message: impl Into<String>) -> Self {
        Self::Network(message.into())
    }

    pub(crate) fn missing_executable(message: impl Into<String>) -> Self {
        Self::MissingExecutable(message.into())
    }

    pub(crate) fn unexpected(message: impl Into<String>) -> Self {
        Self::Unexpected(message.into())
    }

    pub(crate) fn with_prefix(self, prefix: impl fmt::Display) -> Self {
        match self {
            Self::InvalidInput(message) => Self::InvalidInput(format!("{prefix}: {message}")),
            Self::ConfigFormat(message) => Self::ConfigFormat(format!("{prefix}: {message}")),
            Self::Detection(message) => Self::Detection(format!("{prefix}: {message}")),
            Self::Lsp(message) => Self::Lsp(format!("{prefix}: {message}")),
            Self::Network(message) => Self::Network(format!("{prefix}: {message}")),
            Self::MissingExecutable(message) => {
                Self::MissingExecutable(format!("{prefix}: {message}"))
            }
            Self::Unexpected(message) => Self::Unexpected(format!("{prefix}: {message}")),
        }
    }

    #[must_use]
    pub(crate) fn exit_code(&self) -> i32 {
        match self {
            Self::InvalidInput(_) => 2,
            Self::ConfigFormat(_)
            | Self::Detection(_)
            | Self::Lsp(_)
            | Self::Network(_)
            | Self::MissingExecutable(_)
            | Self::Unexpected(_) => 1,
        }
    }

    #[must_use]
    pub(crate) fn should_log_as_unexpected(&self) -> bool {
        matches!(self, Self::Unexpected(_))
    }

    #[cfg(test)]
    #[must_use]
    pub(crate) fn contains(&self, needle: &str) -> bool {
        self.message().contains(needle)
    }
}

impl PartialEq<String> for Error {
    fn eq(&self, other: &String) -> bool {
        self.message() == other
    }
}

impl PartialEq<&str> for Error {
    fn eq(&self, other: &&str) -> bool {
        self.message() == *other
    }
}

macro_rules! error_fn {
    ($error:expr, $fmt:literal) => (
        |error| $error(format!(concat!($fmt, ": {}"), error))
    );
    ($error:expr, $fmt:literal, $($rest:expr),+) => (
        |error| $error(format!(concat!($fmt, ": {}"), $($rest),+, error))
    );
}
pub(crate) use error_fn;

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

    #[test]
    fn invalid_input_uses_exit_code_two() {
        assert_eq!(Error::invalid_input("bad").exit_code(), 2);
    }

    #[test]
    fn only_unexpected_is_logged_as_unexpected() {
        assert!(!Error::detection("detected").should_log_as_unexpected());
        assert!(Error::unexpected("boom").should_log_as_unexpected());
    }
}
