//! Ditto Error Types
//!
//! Modeled after std::io::error::Error;

use std::{convert::Infallible, error::Error as ErrorTrait, fmt};

use crate::{auth::AuthenticationClientFeedback, ffi_sdk};

/// Custom result type for Ditto
pub type Result<Ok, Err = DittoError> = ::core::result::Result<Ok, Err>;

/// Custom error for Ditto
pub struct DittoError {
    repr: Repr,
}

enum Repr {
    Simple(ErrorKind),
    Authentication(AuthenticationClientFeedback),
    Ffi(FfiError),
    Rust(RustError),
    License(LicenseError),
}

/// Error related to License
pub enum LicenseError {
    LicenseTokenVerificationFailed { message: String },
    LicenseTokenExpired { message: String },
    LicenseTokenUnsupportedFutureVersion { message: String },
}

impl LicenseError {
    /// Returns the internal error message for more details
    pub fn message(&self) -> &String {
        match self {
            LicenseError::LicenseTokenVerificationFailed { message } => message,
            LicenseError::LicenseTokenExpired { message } => message,
            LicenseError::LicenseTokenUnsupportedFutureVersion { message } => message,
        }
    }
}

#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
/// General kind of an error
pub enum ErrorKind {
    /// Configured Authentication provider was not found or failed to authenticate
    Authentication,

    /// Required configuration was missing or invalid
    Config, // use to distinguish misconfigured from invalid query/document/etc.

    /// Error originated in the FFI layer
    Ffi, // use this when you can't re-map into a Rust Error

    /// An error occurred in Ditto's internals
    Internal,

    /// Provided parameters or data are not valid
    InvalidInput, // use when user input fails validation

    /// Error with the local file system or networking
    IO, // re-maps std::io::error types (Filesystem and Network), including missing local files

    /// The Ditto license is missing, expired, or otherwise invalid
    License, // specifically when the License is not valid

    /// Ditto sync was started without a valid license
    NotActivated, // When Sync started before license validation

    /// The requested resource *no longer* exists (i.e. lost or destroyed)
    /// Used typically for empty query results
    NonExtant, // when a requested resource doesn't exist

    /// The requested Ditto instance has already been dropped
    ReleasedDittoInstance,
}

// TODO(pub_check): If this is public then it should probably be named in a more customer-friendly
// way
/// Errors originating in FFI code.
///
/// Shadows the FFI's CError type which doesn't have a C Repr.
pub struct FfiError {
    pub code: i32,
    pub msg: String,
}

/// Errors originating in Rust Code
pub struct RustError {
    pub kind: ErrorKind,
    pub error: Box<dyn ErrorTrait + Send + Sync>,
}

impl fmt::Debug for RustError {
    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
        // Display both the ErrorKind Varriend but also the message text
        let msg = format!("{:?} - {}", &self.kind, self.kind.as_str());
        fmt.debug_struct("RustError")
            .field("kind", &msg)
            .field("error", &self.error) // Then also display underlying cause info
            .finish()
    }
}

impl ErrorKind {
    pub fn as_str(&self) -> &'static str {
        match *self {
            ErrorKind::Authentication => "Unable to authenticate Ditto",
            ErrorKind::Config => "Required configuration values are missing or invalid",
            ErrorKind::Ffi => "Unmapped Ditto Error",
            ErrorKind::IO => "There is a problem with the underlying file, directory, or network socket",
            ErrorKind::Internal => "Ditto encountered an internal error",
            ErrorKind::InvalidInput => "Invalid client input provided",
            ErrorKind::License => "License token error",
            ErrorKind::NotActivated => "Sync could not be started because Ditto has not yet been activated. This can be achieved with a successful call to `set_license_token`. If you need to obtain a license token then please visit https://portal.ditto.live.",
            ErrorKind::NonExtant => "The target entity can no longer be found",
            ErrorKind::ReleasedDittoInstance => "The related Ditto instance has been closed",
        }
    }
}

impl From<ErrorKind> for DittoError {
    fn from(kind: ErrorKind) -> DittoError {
        DittoError {
            repr: Repr::Simple(kind),
        }
    }
}

impl fmt::Debug for DittoError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        fmt::Debug::fmt(&self.repr, f)
    }
}

impl ErrorTrait for DittoError {}

impl DittoError {
    /// Construct an error from an existing Error creating and error chain
    pub(crate) fn new<E>(kind: ErrorKind, rust_err: E) -> DittoError
    where
        E: Into<Box<dyn ErrorTrait + Send + Sync>>,
    {
        DittoError {
            repr: Repr::Rust(RustError {
                kind,
                error: rust_err.into(),
            }),
        }
    }

    /// Construct an error with a specific message from a string
    pub(crate) fn from_str(kind: ErrorKind, msg: impl Into<String>) -> DittoError {
        let msg: String = msg.into();
        DittoError {
            repr: Repr::Rust(RustError {
                kind,
                error: msg.into(),
            }),
        }
    }

    /// Construct an error from a LicenseError
    pub(crate) fn license(err: LicenseError) -> DittoError {
        DittoError {
            repr: Repr::License(err),
        }
    }

    /// Manually specify the Error Kind, but fetch the message from the FFI.
    ///
    /// The result is returned as a Rust style error with the cause of `<String as ErrorTrait>`
    pub(crate) fn from_ffi(kind: ErrorKind) -> DittoError {
        match unsafe { ffi_sdk::ditto_error_message() } {
            Some(c_msg) => {
                let msg = c_msg.into_string();
                DittoError::new(kind, msg)
            }
            None => DittoError {
                repr: Repr::Ffi(FfiError {
                    msg: "No Message".to_owned(),
                    code: -1, // undefined
                }),
            },
        }
    }

    /// Return the kind of the Error
    pub fn kind(&self) -> ErrorKind {
        match &self.repr {
            Repr::Simple(kind) => *kind,
            Repr::Rust(e) => e.kind,
            Repr::Ffi(_c) => ErrorKind::Ffi, // could implement uniform code to kind mapping in
            // future
            Repr::License(_) => ErrorKind::License,
            Repr::Authentication(_) => ErrorKind::Authentication,
        }
    }

    // TODO(pub_check)
    pub fn get_authentication_client_feedback(&self) -> Option<AuthenticationClientFeedback> {
        if let Repr::Authentication(ref feedback) = self.repr {
            Some(feedback.clone())
        } else {
            None
        }
    }

    // We may want to evolve how this is constructed, so let's not expose it to customers yet
    pub(crate) fn from_authentication_feedback(feedback: AuthenticationClientFeedback) -> Self {
        DittoError {
            repr: Repr::Authentication(feedback),
        }
    }
}

impl fmt::Debug for Repr {
    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
        match *self {
            Repr::Simple(kind) => fmt.debug_tuple("Kind").field(&kind).finish(),
            Repr::Rust(ref e) => fmt::Debug::fmt(&e, fmt),
            Repr::Ffi(ref c) => fmt
                .debug_struct("FFIError")
                .field("code", &c.code)
                .field("message", &c.msg)
                .field("kind", &ErrorKind::Ffi)
                .finish(),
            Repr::License(ref e) => fmt
                .debug_struct("LicenseTokenError")
                .field("message", e.message())
                .field("kind", &ErrorKind::License)
                .finish(),
            Repr::Authentication(ref feedback) => fmt
                .debug_struct("AuthenticationError")
                .field("Feedback", &feedback.feedback)
                .finish(),
        }
    }
}

impl fmt::Display for DittoError {
    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self.repr {
            Repr::Simple(kind) => write!(fmt, "{}", kind.as_str()),
            Repr::Rust(ref e) => e.error.fmt(fmt), // forward to cause error
            Repr::Ffi(ref c) => write!(fmt, "{} (code {})", &c.msg, &c.code),
            Repr::License(ref e) => write!(fmt, "{}", e.message()),
            Repr::Authentication(ref feedback) => match feedback.feedback {
                Some(ref feedback) => {
                    write!(fmt, "Authentication Error with feedback: {}", feedback)
                }
                None => {
                    write!(fmt, "Authentication Error")
                }
            },
        }
    }
}

error_from_i32! {
    i32, ::core::num::NonZeroI32
}
#[rustfmt::skip]
macro_rules! error_from_i32 {(
    $( $i32:ty ),* $(,)?
) => (
    $(
        impl From<$i32> for FfiError {
            fn from(code: $i32) -> FfiError {
                let code: i32 = code.into();
                debug_assert_ne!(code, 0);
                match unsafe { ffi_sdk::ditto_error_message() } {
                    Some(c_msg) => {
                        let msg = c_msg.into_string();
                        FfiError { code, msg }
                    }
                    None => FfiError {
                        msg: "No Message".to_owned(),
                        code,
                    },
                }
            }
        }

        impl From<$i32> for DittoError {
            fn from(code: $i32) -> DittoError {
                DittoError { repr: Repr::Ffi(code.into()) }
            }
        }
    )*
)}
use error_from_i32;

impl From<::serde_cbor::Error> for DittoError {
    fn from(err: ::serde_cbor::Error) -> Self {
        DittoError::new(ErrorKind::InvalidInput, err)
    }
}

impl From<::serde_json::Error> for DittoError {
    fn from(err: ::serde_json::Error) -> Self {
        DittoError::new(ErrorKind::InvalidInput, err)
    }
}

impl From<::std::io::Error> for DittoError {
    fn from(err: ::std::io::Error) -> Self {
        DittoError::new(ErrorKind::IO, err)
    }
}

impl From<Infallible> for DittoError {
    fn from(err: Infallible) -> Self {
        DittoError::new(ErrorKind::Internal, err)
    }
}
