//! Ditto Error Types
//!
//! Modeled after [`::std::io::Error`].
//!
//! [`DittoError`] is the main error type used in the SDK.

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

use crate::{
    ffi_sdk::{self, ffi_utils::repr_c},
    identity::AuthenticationClientFeedback,
};

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

// FIXME(Daniel & Ham): properly clean up the errors of the Rust SDK in the next
// breaking version, _e.g._, V5.
#[doc(inline)]
pub use ffi_sdk::FfiErrorCode as CoreApiErrorKind;

/// Custom error for Ditto.
///
/// ### Recoverable errors
///
/// Use its [`.kind()`][Self::kind()] method to be able to `match` on a bunch of
/// specific known [`ErrorKind`]s.
///
///   - ⚠️ be aware that the list of possible kinds is **not exhaustive**.
///
/// ### Unexpected/unrecoverable errors
///
/// Can be `{}`-[`Display`][::core::fmt::Display]ed for a human-friendly message
/// and representation.
pub struct DittoError {
    repr: Repr,
}

pub(crate) struct FfiError {
    pub(crate) code: ::ffi_sdk::FfiErrorCode,
    raw: repr_c::Box<::ffi_sdk::FfiError>,
}

impl ::core::fmt::Debug for FfiError {
    fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
        f.debug_struct("FfiError")
            .field("code", &self.code)
            .field(
                "description",
                &::ffi_sdk::dittoffi_error_description(&self.raw),
            )
            .finish_non_exhaustive()
    }
}

impl ::core::fmt::Display for FfiError {
    fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
        let Self { code, raw } = self;
        write!(
            f,
            "{code:?}: {}",
            ::ffi_sdk::dittoffi_error_description(raw)
        )?;
        Ok(())
    }
}

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

#[derive(Debug)]
enum Repr {
    Simple(ErrorKind),
    Authentication(AuthenticationClientFeedback),
    Ffi(FfiError),
    FfiLegacy(legacy::FfiError),
    Rust(RustError),
    License(LicenseTokenError),
}

#[derive(Debug)]
/// Error related to License
pub(crate) enum LicenseTokenError {
    VerificationFailed { message: String },
    Expired { message: String },
    UnsupportedFutureVersion { message: String },
}

impl LicenseTokenError {
    /// Returns the internal error message for more details
    pub fn message(&self) -> &String {
        match self {
            LicenseTokenError::VerificationFailed { message } => message,
            LicenseTokenError::Expired { message } => message,
            LicenseTokenError::UnsupportedFutureVersion { message } => message,
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
/// 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
    FfiLegacy, // 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,

    /// An error occurred with one of the core APIs of Ditto.
    CoreApi(CoreApiErrorKind),

    /// Deprecation error
    Deprecation,
}

/// A datastructure representing an error from the FFI using the legacy mechanisms
/// (thread-local storage and integer error code).
mod legacy {
    #[derive(Debug)]
    /// Errors originating in FFI code.
    ///
    /// Shadows the FFI's CError type which doesn't have a C Repr.
    pub(crate) struct FfiError {
        pub(crate) code: i32,
        pub(crate) msg: String,
    }
}

/// Errors originating in Rust Code
pub(crate) struct RustError {
    pub(crate) kind: ErrorKind,
    pub(crate) 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);
        fmt.debug_struct("RustError")
            .field("kind", &msg)
            .field("error", &self.error) // Then also display underlying cause info
            .finish()
    }
}

impl ErrorKind {
    #[deprecated(note = "use the `Display` implementation instead")]
    #[doc(hidden)]
    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::FfiLegacy => "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",
            ErrorKind::CoreApi(_) => "\
                an error occurred from core Ditto functionality. \
                Please use the `Display` implementation for more info.\
            ",
            ErrorKind::Deprecation => "Deprecated method called",
        }
    }
}

impl ::core::fmt::Display for ErrorKind {
    fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
        match self {
            ErrorKind::CoreApi(ffi_error_code) => write!(f, "{ffi_error_code:?}"),
            _ => {
                #[allow(deprecated)]
                {
                    self.as_str()
                }
            }
            .fmt(f),
        }
    }
}

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 an error chain
    pub(crate) fn new<E>(kind: ErrorKind, rust_err: E) -> Self
    where
        E: Into<Box<dyn ErrorTrait + Send + Sync>>,
    {
        DittoError {
            repr: Repr::Rust(RustError {
                kind,
                error: rust_err.into(),
            }),
        }
    }
}

impl DittoError {
    /// 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 small peer info error code
    pub(crate) fn from_small_peer_info_error_code(error_code: i32) -> DittoError {
        match error_code {
            -1 => Self::from_str(
                ErrorKind::InvalidInput,
                "The observability subsystem is unavailable",
            ),
            1 => Self::from_str(
                ErrorKind::InvalidInput,
                "The amount of data is too large according to our self-imposed limits.",
            ),
            2 => Self::from_str(
                ErrorKind::InvalidInput,
                "The amount of JSON data is too nested acccording to our self-imposed limits, or \
                 if the data cannot be parsed to determine the depth.",
            ),
            3 => Self::from_str(
                ErrorKind::InvalidInput,
                "The data cannot be parsed as a Map<String, Value>.",
            ),
            _ => Self::from_str(ErrorKind::FfiLegacy, "Unmapped error"),
        }
    }

    /// Construct an error from a LicenseTokenError
    pub(crate) fn license(err: LicenseTokenError) -> Self {
        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) -> Self {
        let msg = match ffi_sdk::ditto_error_message() {
            Some(c_msg) => c_msg.into_string(),
            None => "no message".into(),
        };
        DittoError::new(kind, msg)
    }

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

    /// If this is an Authentication error, get feedback from the auth provider.
    ///
    /// The returned value may contain arbitrary JSON from the auth service
    /// with details describing the success or rejection of the authentication request.
    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::Display for DittoError {
    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self.repr {
            Repr::Simple(kind) => write!(fmt, "{}", kind),
            Repr::Rust(ref e) => e.error.fmt(fmt), // forward to cause error
            Repr::FfiLegacy(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")
                }
            },
            Repr::Ffi(ref ffi_error) => ffi_error.fmt(fmt),
        }
    }
}

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

        impl From<$i32> for DittoError {
            fn from(code: $i32) -> Self {
                DittoError { repr: Repr::FfiLegacy(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)
    }
}

impl From<repr_c::Box_<ffi_sdk::FfiError>> for DittoError {
    fn from(raw: repr_c::Box<ffi_sdk::FfiError>) -> Self {
        DittoError {
            repr: Repr::Ffi(FfiError {
                code: ::ffi_sdk::dittoffi_error_code(&*raw),
                raw,
            }),
        }
    }
}

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