use std::borrow::Cow;

/// Represents the [Result](std::result::Result) type for the library.
pub type Result<T> = std::result::Result<T, Error>;

/// Represents the error variants for the library.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Error {
    Io { kind: ErrorKind, msg: String },
}

impl Error {
    /// Creates a new [Error].
    pub const fn new() -> Self {
        Self::Io {
            kind: ErrorKind::new(),
            msg: String::new(),
        }
    }

    /// Gets the [Error]'s error kind.
    #[inline]
    pub const fn kind(&self) -> ErrorKind {
        match self {
            Self::Io { kind, .. } => *kind,
        }
    }

    /// Creates a new I/O [Error].
    pub fn io<'c, C: Into<Cow<'c, str>>>(kind: ErrorKind, msg: C) -> Self {
        Self::Io {
            kind,
            msg: C::into(msg).into(),
        }
    }

    /// Creates a new I/O other [Error].
    pub fn io_other<'c, C: Into<Cow<'c, str>>>(msg: C) -> Self {
        Self::Io {
            kind: ErrorKind::Other,
            msg: C::into(msg).into(),
        }
    }

    /// Creates a new I/O JSON [Error].
    pub fn io_json<'c, C: Into<Cow<'c, str>>>(msg: C) -> Self {
        Self::Io {
            kind: ErrorKind::Json,
            msg: C::into(msg).into(),
        }
    }

    /// Creates a new I/O database [Error].
    pub fn io_db<'c, C: Into<Cow<'c, str>>>(msg: C) -> Self {
        Self::Io {
            kind: ErrorKind::Db,
            msg: C::into(msg).into(),
        }
    }

    /// Creates a new I/O cryptography [Error].
    pub fn io_crypto<'c, C: Into<Cow<'c, str>>>(msg: C) -> Self {
        Self::Io {
            kind: ErrorKind::Crypto,
            msg: C::into(msg).into(),
        }
    }
}

impl Default for Error {
    fn default() -> Self {
        Self::new()
    }
}

impl From<std::io::Error> for Error {
    fn from(err: std::io::Error) -> Self {
        Self::Io {
            kind: err.kind().into(),
            msg: format!("{err}"),
        }
    }
}

impl From<sled::Error> for Error {
    fn from(err: sled::Error) -> Self {
        Self::io_db(format!("{err}"))
    }
}

impl From<std::string::FromUtf8Error> for Error {
    fn from(err: std::string::FromUtf8Error) -> Self {
        Self::io_other(format!("{err}"))
    }
}

impl From<std::str::Utf8Error> for Error {
    fn from(err: std::str::Utf8Error) -> Self {
        Self::io_other(format!("{err}"))
    }
}

impl From<chacha20::Error> for Error {
    fn from(err: chacha20::Error) -> Self {
        Self::io_crypto(format!("{err}"))
    }
}

impl From<crypto_common::InvalidLength> for Error {
    fn from(err: crypto_common::InvalidLength) -> Self {
        Self::io_crypto(format!("{err}"))
    }
}

impl From<std::array::TryFromSliceError> for Error {
    fn from(err: std::array::TryFromSliceError) -> Self {
        Self::io_other(format!("{err}"))
    }
}

impl From<totp::Rfc6238Error> for Error {
    fn from(err: totp::Rfc6238Error) -> Self {
        Self::io_crypto(format!("TOTP error: {err}"))
    }
}

impl From<totp::TotpUrlError> for Error {
    fn from(err: totp::TotpUrlError) -> Self {
        Self::io_crypto(format!("TOTP error: {err}"))
    }
}

impl From<std::time::SystemTimeError> for Error {
    fn from(err: std::time::SystemTimeError) -> Self {
        Self::io_db(format!("time error: {err}"))
    }
}

impl From<base64::Error> for Error {
    fn from(err: base64::Error) -> Self {
        Self::io_crypto(format!("base64 error: {err}"))
    }
}

impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Io { kind, msg } => write!(f, "{kind} error: {msg}"),
        }
    }
}

impl std::error::Error for Error {}

/// Represents the error kind variants for the library.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ErrorKind {
    Json,
    Crypto,
    Db,
    Other,
}

impl ErrorKind {
    pub const JSON: &str = "JSON";
    pub const CRYPTO: &str = "cryptography";
    pub const DB: &str = "database";
    pub const OTHER: &str = "other";

    /// Creates a new [ErrorKind].
    #[inline]
    pub const fn new() -> Self {
        Self::Other
    }

    /// Gets whether the [ErrorKind] is a JSON variant.
    #[inline]
    pub const fn is_json(&self) -> bool {
        matches!(self, Self::Json)
    }

    /// Gets whether the [ErrorKind] is a crypto variant.
    #[inline]
    pub const fn is_crypto(&self) -> bool {
        matches!(self, Self::Crypto)
    }

    /// Gets whether the [ErrorKind] is a database variant.
    #[inline]
    pub const fn is_db(&self) -> bool {
        matches!(self, Self::Db)
    }

    /// Gets whether the [ErrorKind] is a other variant.
    #[inline]
    pub const fn is_other(&self) -> bool {
        matches!(self, Self::Other)
    }

    /// Gets the string representation of the [ErrorKind].
    #[inline]
    pub const fn as_str(&self) -> &'static str {
        match self {
            Self::Json => Self::JSON,
            Self::Crypto => Self::CRYPTO,
            Self::Db => Self::DB,
            Self::Other => Self::OTHER,
        }
    }
}

impl From<std::io::ErrorKind> for ErrorKind {
    fn from(_err: std::io::ErrorKind) -> Self {
        Self::Other
    }
}

impl From<ErrorKind> for std::io::ErrorKind {
    fn from(_err: ErrorKind) -> Self {
        Self::Other
    }
}

impl Default for ErrorKind {
    fn default() -> Self {
        Self::new()
    }
}

impl std::fmt::Display for ErrorKind {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.as_str())
    }
}
