use chrono::{DateTime, NaiveDate, Utc};
use rust_decimal::Decimal;

use super::{Plaintext, PlaintextNullVariant};
use crate::encryption::errors::TypeParseError;

// Custom TryFrom trait for plaintext so consumers can convert from plaintext to their own types
// and get Option and other impls for free. This isn't possible with TryFrom because of
// specialization.
pub trait TryFromPlaintext: Sized {
    fn try_from_plaintext(value: Plaintext) -> Result<Self, TypeParseError>;
    fn try_from_optional_plaintext(value: Option<Plaintext>) -> Result<Self, TypeParseError>;
}

macro_rules! impl_try_from_plaintext {
    ($ty:ty, $variant:ident) => {
        impl TryFromPlaintext for $ty {
            fn try_from_plaintext(value: Plaintext) -> Result<Self, TypeParseError> {
                match value {
                    Plaintext::$variant(Some(ref v)) => Ok(v.to_owned()),
                    _ => Err(TypeParseError(format!(
                        "Cannot convert type to {}",
                        std::any::type_name::<Self>()
                    ))),
                }
            }

            fn try_from_optional_plaintext(
                value: Option<Plaintext>,
            ) -> Result<Self, TypeParseError> {
                match value {
                    Some(plaintext) => Self::try_from_plaintext(plaintext),
                    _ => Err(TypeParseError(format!(
                        "Cannot convert type to {}",
                        std::any::type_name::<Self>()
                    ))),
                }
            }
        }

        // Also implement deprecated TryFrom for our types to maintain backwards compatibility.
        impl TryFrom<Plaintext> for $ty {
            type Error = TypeParseError;

            fn try_from(value: Plaintext) -> Result<Self, Self::Error> {
                Self::try_from_plaintext(value)
            }
        }

        impl TryFrom<Plaintext> for Option<$ty> {
            type Error = TypeParseError;

            fn try_from(value: Plaintext) -> Result<Self, Self::Error> {
                Self::try_from_plaintext(value)
            }
        }
    };
}

impl<T> TryFromPlaintext for Option<T>
where
    T: TryFromPlaintext + PlaintextNullVariant,
{
    fn try_from_optional_plaintext(value: Option<Plaintext>) -> Result<Self, TypeParseError> {
        match value {
            Some(value) => Self::try_from_plaintext(value),
            None => Ok(None),
        }
    }

    fn try_from_plaintext(value: Plaintext) -> Result<Self, TypeParseError> {
        match (&value, T::null()) {
            // Return OK(None) if the inner value is None
            (Plaintext::BigInt(None), Plaintext::BigInt(_))
            | (Plaintext::BigUInt(None), Plaintext::BigUInt(_))
            | (Plaintext::Boolean(None), Plaintext::Boolean(_))
            | (Plaintext::Decimal(None), Plaintext::Decimal(_))
            | (Plaintext::Float(None), Plaintext::Float(_))
            | (Plaintext::Int(None), Plaintext::Int(_))
            | (Plaintext::NaiveDate(None), Plaintext::NaiveDate(_))
            | (Plaintext::SmallInt(None), Plaintext::SmallInt(_))
            | (Plaintext::Timestamp(None), Plaintext::Timestamp(_))
            | (Plaintext::Text(None), Plaintext::Text(_)) => Ok(None),
            (Plaintext::Json(None), Plaintext::Json(_)) => Ok(None),

            // Return Result<Some(T))> if the inner value is Some
            (Plaintext::BigInt(Some(_)), Plaintext::BigInt(_))
            | (Plaintext::BigUInt(Some(_)), Plaintext::BigUInt(_))
            | (Plaintext::Boolean(Some(_)), Plaintext::Boolean(_))
            | (Plaintext::Decimal(Some(_)), Plaintext::Decimal(_))
            | (Plaintext::Float(Some(_)), Plaintext::Float(_))
            | (Plaintext::Int(Some(_)), Plaintext::Int(_))
            | (Plaintext::NaiveDate(Some(_)), Plaintext::NaiveDate(_))
            | (Plaintext::SmallInt(Some(_)), Plaintext::SmallInt(_))
            | (Plaintext::Timestamp(Some(_)), Plaintext::Timestamp(_))
            | (Plaintext::Text(Some(_)), Plaintext::Text(_))
            | (Plaintext::Json(Some(_)), Plaintext::Json(_)) => {
                T::try_from_plaintext(value).map(Some)
            }
            // Return type error if the expected variant for T and value doesn't match
            _ => Err(TypeParseError(format!(
                "Cannot convert type to {}",
                std::any::type_name::<Self>()
            ))),
        }
    }
}

macro_rules! from_conversions_and_tests {
    ($(($($value:expr),*) => $ty:ty => $variant:ident),*) => {
        // Implement the traits for all variants
        $(
            impl_try_from_plaintext!($ty, $variant);
        )*

        // This method is used to throw a compile error when the from impls aren't implemented
        // for all variants of the plaintext type. The message is going to be a little weird but at
        // least it'll fail.
        #[allow(dead_code)]
        fn exhaustive_check(value: Plaintext) {
            match value {
                $(
                    Plaintext::$variant(_) => {},
                )*
            }
        }

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

            $(
                paste::paste! {
                    // The following tests are for the old TryFrom impls
                    #[test]
                    fn [<test_try_from_ $variant:snake>]() {
                        $(
                            assert_eq!(
                                $ty::try_from(Plaintext::$variant(Some($ty::from($value)))).unwrap(),
                                $ty::from($value)
                            );
                        )*
                    }

                    #[test]
                    fn [<test_try_from_ $variant:snake _option>]() {
                        $(
                            assert_eq!(
                                Option::<$ty>::try_from(Plaintext::$variant(Some($ty::from($value)))).unwrap(),
                                Option::<$ty>::Some($value.into())
                            );
                        )*
                    }

                    #[test]
                    fn [<test_try_from_ $variant:snake _option_none>]() {
                        assert_eq!(
                            Option::<$ty>::try_from(Plaintext::$variant(None)).unwrap(),
                            Option::<$ty>::None
                        );
                    }

                    // The following tests are to make sure the new TryFromPlaintext impls work
                    #[test]
                    fn [<test_try_from_plaintext_ $variant:snake>]() {
                        $(
                            assert_eq!(
                                $ty::try_from_plaintext(Plaintext::$variant(Some($ty::from($value)))).unwrap(),
                                $ty::from($value)
                            );
                        )*
                    }

                    #[test]
                    fn [<test_try_from_optional_plaintext_ $variant:snake _some>]() {
                        $(
                            assert_eq!(
                                $ty::try_from_optional_plaintext(Some(Plaintext::$variant(Some($ty::from($value))))).unwrap(),
                                $ty::from($value)
                            );
                        )*
                    }

                    #[test]
                    fn [<test_try_from_plaintext_ $variant:snake _option>]() {
                        $(
                            assert_eq!(
                                Option::<$ty>::try_from_plaintext(Plaintext::$variant(Some($ty::from($value)))).unwrap(),
                                Option::<$ty>::Some($value.into())
                            );
                        )*
                    }

                    #[test]
                    fn [<test_try_from_optional_plaintext_ $variant:snake _option_some>]() {
                        $(
                            assert_eq!(
                                Option::<$ty>::try_from_optional_plaintext(Some(Plaintext::$variant(Some($ty::from($value))))).unwrap(),
                                Option::<$ty>::Some($value.into())
                            );
                        )*
                    }

                    #[test]
                    fn [<test_try_from_plaintext_ $variant:snake _option_none>]() {
                        assert_eq!(
                            Option::<$ty>::try_from_plaintext(Plaintext::$variant(None)).unwrap(),
                            Option::<$ty>::None
                        );
                    }

                    #[test]
                    fn [<test_try_from_optional_plaintext_ $variant:snake _option_none>]() {
                        assert_eq!(
                            Option::<$ty>::try_from_optional_plaintext(None).unwrap(),
                            Option::<$ty>::None
                        );
                    }
                }
            )*
        }
    };
}

from_conversions_and_tests! {
    (10_i64, -10_i64) => i64 => BigInt,
    (10_u64, 100_u64) => u64 => BigUInt,
    (10_i32, -10_i32) => i32 => Int,
    (10_i16, -10_i16) => i16 => SmallInt,
    (0., -1., 100.) => f64 => Float,
    (Decimal::new(10, 0), Decimal::new(-10, 0)) => Decimal => Decimal,
    (NaiveDate::from_ymd_opt(2020, 1, 1).expect("Expected date to create")) => NaiveDate => NaiveDate,
    (DateTime::<Utc>::from_timestamp(1000, 0).expect("Expected timestamp to create")) => DateTime::<Utc> => Timestamp,
    (true, false) => bool => Boolean,
    ("hello", "") => String => Text,
    (serde_json::json!({"hello": "world"}), serde_json::json!({})) => serde_json::Value => Json
}
