use std::{
    error::Error as StdError,
    path::{Path, PathBuf},
};

use libsignal_service::{
    prelude::ServiceError,
    sender::{AttachmentUploadError, MessageSenderError},
};

use crate::{CaptchaToken, ContactId, GroupId, MessageId, ReceivedMessage, ThreadId};

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

#[derive(Debug, PartialEq, Eq, Clone, Copy)]
#[non_exhaustive]
pub enum ErrorKind {
    NoInternet,
    Unlinked,
    Database,
    Parse,
    Protocol,
    RateLimit,
}

impl ErrorKind {
    pub fn caused_by<E: std::error::Error + Send + Sync + 'static>(self, error: E) -> Error {
        self.error().with_source(error)
    }

    pub fn error(self) -> Error {
        Error::new(self)
    }

    pub fn guess_transiency(&self) -> ErrorTransiency {
        match self {
            ErrorKind::NoInternet => ErrorTransiency::Transient,
            ErrorKind::Unlinked => ErrorTransiency::Constant,
            ErrorKind::Database => ErrorTransiency::Constant,
            ErrorKind::Parse => ErrorTransiency::OneTime,
            ErrorKind::Protocol => ErrorTransiency::OneTime,
            ErrorKind::RateLimit => ErrorTransiency::Transient,
        }
    }

    pub fn guess_action_required(&self) -> ErrorActionRequired {
        match self {
            ErrorKind::NoInternet => ErrorActionRequired::None,
            ErrorKind::Unlinked => ErrorActionRequired::Relink,
            ErrorKind::Database => ErrorActionRequired::Report,
            ErrorKind::Parse => ErrorActionRequired::Report,
            ErrorKind::Protocol => ErrorActionRequired::Report,
            ErrorKind::RateLimit => ErrorActionRequired::FillCaptcha,
        }
    }
}

#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
pub enum ErrorTransiency {
    Transient,
    OneTime,
    Constant,
    #[default]
    Unknown,
}

#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
pub enum ErrorActionRequired {
    None,
    Relink,
    FillCaptcha,
    #[default]
    Report,
}

#[derive(Debug, Clone)]
pub enum ErrorContext {
    Message(Box<ReceivedMessage>),
    MessageId(MessageId),
    Thread(ThreadId),
    Contact(ContactId),
    Group(GroupId),
    Field(String),
    Value(String),
    Path(PathBuf),
    CaptchaToken(CaptchaToken),
    // Key, Value
    Custom(String, String),
}

#[derive(Debug)]
pub struct Error {
    message: Option<String>,
    kind: ErrorKind,
    transiency: ErrorTransiency,
    action_required: ErrorActionRequired,
    context: Vec<ErrorContext>,
    source: Option<Box<dyn std::error::Error + Send + Sync>>,
}

impl Error {
    pub fn new(kind: ErrorKind) -> Self {
        Self {
            message: None,
            kind,
            transiency: kind.guess_transiency(),
            action_required: kind.guess_action_required(),
            context: Default::default(),
            source: Default::default(),
        }
    }

    pub fn new_with_source(
        kind: ErrorKind,
        source: Box<dyn std::error::Error + Send + Sync>,
    ) -> Self {
        Self::new(kind).set_source(Some(source))
    }

    pub fn with_message<S: AsRef<str>>(mut self, msg: S) -> Self {
        self.message = Some(msg.as_ref().to_owned());
        self
    }

    pub fn set_transiency(mut self, tranciency: ErrorTransiency) -> Self {
        self.transiency = tranciency;
        self
    }

    pub fn set_action_required(mut self, action: ErrorActionRequired) -> Self {
        self.action_required = action;
        self
    }

    pub fn add_context(mut self, context: ErrorContext) -> Self {
        self.context.push(context);
        self
    }

    pub fn add_field_context<S: AsRef<str>>(self, field: S) -> Self {
        self.add_context(ErrorContext::Field(field.as_ref().to_owned()))
    }

    pub fn add_value_context<S: AsRef<str>>(self, value: S) -> Self {
        self.add_context(ErrorContext::Value(value.as_ref().to_owned()))
    }

    pub fn add_value_dbg_context<S: std::fmt::Debug>(self, value: S) -> Self {
        self.add_context(ErrorContext::Value(format!("{:?}", value)))
    }

    pub fn add_message_id_context(self, msg: MessageId) -> Self {
        self.add_context(ErrorContext::MessageId(msg))
    }

    pub fn add_message_context(self, msg: ReceivedMessage) -> Self {
        self.add_message_id_context(msg.id)
            .add_thread_context(msg.thread)
            .add_contact_context(msg.sender())
            .add_context(ErrorContext::Message(Box::new(msg)))
    }

    pub fn add_thread_context(self, thread: ThreadId) -> Self {
        self.add_context(ErrorContext::Thread(thread))
    }

    pub fn add_contact_context(self, contact: ContactId) -> Self {
        self.add_context(ErrorContext::Contact(contact))
    }

    pub fn add_group_context(self, group: GroupId) -> Self {
        self.add_context(ErrorContext::Group(group))
    }

    pub fn add_path_context<P: AsRef<Path>>(self, path: P) -> Self {
        self.add_context(ErrorContext::Path(path.as_ref().to_owned()))
    }

    pub fn add_captcha_token_context(self, token: CaptchaToken) -> Self {
        self.add_context(ErrorContext::CaptchaToken(token))
    }

    pub fn add_custom_context<S1: AsRef<str>, S2: AsRef<str>>(self, key: S1, value: S2) -> Self {
        self.add_context(ErrorContext::Custom(
            key.as_ref().to_owned(),
            value.as_ref().to_owned(),
        ))
    }

    pub fn set_source(mut self, source: Option<Box<dyn std::error::Error + Send + Sync>>) -> Self {
        self.source = source;
        self
    }

    pub fn with_source<E>(self, source: E) -> Self
    where
        E: Into<Box<dyn std::error::Error + Send + Sync>>,
    {
        self.set_source(Some(source.into()))
    }

    pub fn kind(&self) -> ErrorKind {
        self.kind
    }

    pub fn transiency(&self) -> ErrorTransiency {
        self.transiency
    }

    pub fn action_required(&self) -> ErrorActionRequired {
        self.action_required
    }

    pub fn context(&self) -> &[ErrorContext] {
        &self.context
    }

    pub fn source(&self) -> Option<&(dyn std::error::Error + Send + Sync)> {
        self.source.as_deref()
    }
}

const FAILED_TO_LOOK_UP_ADDRESS: &str = "failed to lookup address information";
const NETWORK_UNREACHABLE: &str = "Network is unreachable";
const TIMED_OUT: &str = "timed out";
const REQWEST_ERROR: &str = "reqwest error";

impl<E: std::error::Error + Send + Sync + 'static> From<presage::Error<E>> for Error {
    fn from(value: presage::Error<E>) -> Self {
        match value {
            presage::Error::MessageSenderError(source) => match &*source {
                MessageSenderError::ProofRequired { token, .. } => ErrorKind::RateLimit
                    .error()
                    .add_captcha_token_context(CaptchaToken(token.clone()))
                    .with_source(presage::Error::<E>::MessageSenderError(source)),
                _ => ErrorKind::Protocol
                    .error()
                    .with_source(presage::Error::<E>::MessageSenderError(source)),
            },
            presage::Error::ServiceError(ServiceError::Unauthorized) => ErrorKind::Unlinked
                .error()
                .with_source(presage::Error::<E>::ServiceError(
                    ServiceError::Unauthorized,
                )),
            presage::Error::Store(e) => ErrorKind::Database
                .error()
                .with_source(presage::Error::Store(e)),
            presage::Error::ServiceError(source) => match &source {
                ServiceError::WsError(e)
                    if e.to_string().contains(FAILED_TO_LOOK_UP_ADDRESS)
                        || e.to_string().contains(NETWORK_UNREACHABLE)
                        || e.to_string().contains(TIMED_OUT)
                        || e.to_string().contains(REQWEST_ERROR)
                        || e.source().is_some_and(|s| {
                            s.to_string().contains(FAILED_TO_LOOK_UP_ADDRESS)
                                || s.to_string().contains(NETWORK_UNREACHABLE)
                                || s.to_string().contains(TIMED_OUT)
                                || s.to_string().contains(REQWEST_ERROR)
                        }) =>
                {
                    ErrorKind::NoInternet
                        .error()
                        .with_source(presage::Error::<E>::ServiceError(source))
                }
                ServiceError::SendError { reason: e } if e.contains(FAILED_TO_LOOK_UP_ADDRESS) => {
                    ErrorKind::NoInternet
                        .error()
                        .with_source(presage::Error::<E>::ServiceError(source))
                }
                ServiceError::SendError { reason: e } if e.contains(FAILED_TO_LOOK_UP_ADDRESS) => {
                    ErrorKind::NoInternet
                        .error()
                        .with_source(presage::Error::<E>::ServiceError(source))
                }
                ServiceError::ProofRequiredError(proof) => ErrorKind::RateLimit
                    .error()
                    .add_captcha_token_context(CaptchaToken(proof.token.clone()))
                    .with_source(presage::Error::<E>::ServiceError(source)),
                _ => ErrorKind::Protocol
                    .error()
                    .with_source(presage::Error::<E>::ServiceError(source)),
            },
            _ => ErrorKind::Protocol.error().with_source(value),
        }
    }
}

impl From<AttachmentUploadError> for Error {
    fn from(value: AttachmentUploadError) -> Self {
        // TODO
        ErrorKind::Protocol.error().with_source(value)
    }
}

impl StdError for Error {
    fn source(&self) -> Option<&(dyn StdError + 'static)> {
        // Note: It's a bit of a weird implementation, as I have to discard `Send + Sync` for some reason.
        // See https://users.rust-lang.org/t/question-about-error-source-s-static-return-type/34515.
        self.source.as_ref().map(|e| e.as_ref() as &_)
    }
}

impl std::fmt::Display for ErrorKind {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ErrorKind::NoInternet => write!(f, "no internet"),
            ErrorKind::Unlinked => write!(f, "device not linked or registered"),
            ErrorKind::Database => write!(f, "database error"),
            ErrorKind::Parse => write!(f, "parsing error"),
            ErrorKind::Protocol => write!(f, "protocol error"),
            ErrorKind::RateLimit => write!(f, "rate limited"),
        }
    }
}

impl std::fmt::Display for ErrorTransiency {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ErrorTransiency::Transient => write!(f, "likely a transient error"),
            ErrorTransiency::OneTime => write!(f, "likely a one-time error"),
            ErrorTransiency::Constant => write!(f, "likely will keep occuring"),
            ErrorTransiency::Unknown => write!(f, "unknown how often this error can occur"),
        }
    }
}

impl std::fmt::Display for ErrorActionRequired {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ErrorActionRequired::None => write!(f, "no action required"),
            ErrorActionRequired::Relink => write!(f, "the device should be unlinked"),
            ErrorActionRequired::Report => write!(f, "please report this error"),
            ErrorActionRequired::FillCaptcha => write!(f, "please fill out a captcha"),
        }
    }
}

impl std::fmt::Display for ErrorContext {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ErrorContext::Message(message) => write!(
                f,
                "message from {} ({}) of account {}",
                message.id.timestamp, message.id.sub_id, message.id.sender
            ),
            ErrorContext::MessageId(message_id) => write!(
                f,
                "message (id) from {} ({}) of account {}",
                message_id.timestamp, message_id.sub_id, message_id.sender
            ),
            ErrorContext::Thread(thread) => write!(f, "in thread {:?}", thread),
            ErrorContext::Contact(contact) => write!(f, "with contact {:?}", contact),
            ErrorContext::Group(group) => write!(f, "with group {:?}", group),
            ErrorContext::Field(field) => write!(f, "field: {}", field),
            ErrorContext::Value(value) => write!(f, "value: {}", value),
            ErrorContext::Path(path) => write!(f, "path: {}", path.display()),
            ErrorContext::CaptchaToken(token) => write!(f, "captcha token: {}", token.0),
            ErrorContext::Custom(key, value) => write!(f, "{}: {}", key, value),
        }
    }
}

impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        writeln!(
            f,
            "{}{} ({}, {})",
            self.kind,
            if let Some(msg) = &self.message {
                format!(":{}", msg)
            } else {
                "".to_string()
            },
            self.transiency,
            self.action_required
        )?;
        for context in &self.context {
            writeln!(f, "{}", context)?;
        }
        if let Some(source) = &self.source {
            writeln!(f, "source: {}", source)?;
        }
        Ok(())
    }
}
