use std::cmp::Ordering;

use base64::Engine;
use chrono::{NaiveDateTime, TimeZone};
use lldap_auth::types::CaseInsensitiveString;
use sea_orm::{
    entity::IntoActiveValue,
    sea_query::{value::ValueType, ArrayType, BlobSize, ColumnType, Nullable, ValueTypeErr},
    DbErr, DeriveValueType, QueryResult, TryFromU64, TryGetError, TryGetable, Value,
};
use serde::{Deserialize, Serialize};
use strum::{EnumString, IntoStaticStr};

pub use super::model::UserColumn;
pub use lldap_auth::types::UserId;

#[derive(PartialEq, Hash, Eq, Clone, Debug, Default, Serialize, Deserialize, DeriveValueType)]
#[serde(try_from = "&str")]
#[sea_orm(column_type = "String(Some(36))")]
pub struct Uuid(String);

impl Uuid {
    pub fn from_name_and_date(name: &str, creation_date: &NaiveDateTime) -> Self {
        Uuid(
            uuid::Uuid::new_v3(
                &uuid::Uuid::NAMESPACE_X500,
                &[
                    name.as_bytes(),
                    chrono::Utc
                        .from_utc_datetime(creation_date)
                        .to_rfc3339()
                        .as_bytes(),
                ]
                .concat(),
            )
            .to_string(),
        )
    }

    pub fn as_str(&self) -> &str {
        &self.0
    }

    pub fn into_string(self) -> String {
        self.0
    }
}

impl<'a> std::convert::TryFrom<&'a str> for Uuid {
    type Error = anyhow::Error;
    fn try_from(s: &'a str) -> anyhow::Result<Self> {
        Ok(Uuid(uuid::Uuid::parse_str(s)?.to_string()))
    }
}

impl std::string::ToString for Uuid {
    fn to_string(&self) -> String {
        self.0.clone()
    }
}

#[cfg(test)]
#[macro_export]
macro_rules! uuid {
    ($s:literal) => {
        <$crate::domain::types::Uuid as std::convert::TryFrom<_>>::try_from($s).unwrap()
    };
}

#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize, DeriveValueType)]
#[sea_orm(column_type = "Binary(BlobSize::Long)", array_type = "Bytes")]
pub struct Serialized(Vec<u8>);

const SERIALIZED_I64_LEN: usize = 8;

impl std::fmt::Debug for Serialized {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_tuple("Serialized")
            .field(
                &self
                    .convert_to()
                    .and_then(|s| {
                        String::from_utf8(s)
                            .map_err(|_| Box::new(bincode::ErrorKind::InvalidCharEncoding))
                    })
                    .or_else(|e| {
                        if self.0.len() == SERIALIZED_I64_LEN {
                            self.convert_to::<i64>()
                                .map(|i| i.to_string())
                                .map_err(|_| Box::new(bincode::ErrorKind::InvalidCharEncoding))
                        } else {
                            Err(e)
                        }
                    })
                    .unwrap_or_else(|_| {
                        format!("hash: {:#016X}", {
                            let mut hasher = std::collections::hash_map::DefaultHasher::new();
                            std::hash::Hash::hash(&self.0, &mut hasher);
                            std::hash::Hasher::finish(&hasher)
                        })
                    }),
            )
            .finish()
    }
}

impl<'a, T: Serialize + ?Sized> From<&'a T> for Serialized {
    fn from(t: &'a T) -> Self {
        Self(bincode::serialize(&t).unwrap())
    }
}

impl Serialized {
    fn convert_to<'a, T: Deserialize<'a>>(&'a self) -> bincode::Result<T> {
        bincode::deserialize(&self.0)
    }

    pub fn unwrap<'a, T: Deserialize<'a>>(&'a self) -> T {
        self.convert_to().unwrap()
    }

    pub fn expect<'a, T: Deserialize<'a>>(&'a self, message: &str) -> T {
        self.convert_to().expect(message)
    }
}

fn compare_str_case_insensitive(s1: &str, s2: &str) -> Ordering {
    let mut it_1 = s1.chars().flat_map(|c| c.to_lowercase());
    let mut it_2 = s2.chars().flat_map(|c| c.to_lowercase());
    loop {
        match (it_1.next(), it_2.next()) {
            (Some(c1), Some(c2)) => {
                let o = c1.cmp(&c2);
                if o != Ordering::Equal {
                    return o;
                }
            }
            (None, Some(_)) => return Ordering::Less,
            (Some(_), None) => return Ordering::Greater,
            (None, None) => return Ordering::Equal,
        }
    }
}

macro_rules! make_case_insensitive_comparable_string {
    ($c:ident) => {
        #[derive(Clone, Debug, Default, Serialize, Deserialize, DeriveValueType)]
        pub struct $c(String);

        impl PartialEq for $c {
            fn eq(&self, other: &Self) -> bool {
                compare_str_case_insensitive(&self.0, &other.0) == Ordering::Equal
            }
        }

        impl Eq for $c {}

        impl PartialOrd for $c {
            fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
                Some(self.cmp(other))
            }
        }

        impl Ord for $c {
            fn cmp(&self, other: &Self) -> Ordering {
                compare_str_case_insensitive(&self.0, &other.0)
            }
        }

        impl std::hash::Hash for $c {
            fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
                self.0.to_lowercase().hash(state)
            }
        }

        impl $c {
            pub fn new(raw: &str) -> Self {
                Self(raw.to_owned())
            }

            pub fn as_str(&self) -> &str {
                self.0.as_str()
            }

            pub fn into_string(self) -> String {
                self.0
            }
        }

        impl From<String> for $c {
            fn from(s: String) -> Self {
                Self(s)
            }
        }

        impl From<&str> for $c {
            fn from(s: &str) -> Self {
                Self::new(s)
            }
        }

        impl std::fmt::Display for $c {
            fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
                write!(f, "{}", self.0.as_str())
            }
        }

        impl From<&$c> for Value {
            fn from(user_id: &$c) -> Self {
                user_id.as_str().into()
            }
        }

        impl TryFromU64 for $c {
            fn try_from_u64(_n: u64) -> Result<Self, DbErr> {
                Err(DbErr::ConvertFromU64("$c cannot be constructed from u64"))
            }
        }
    };
}

#[derive(
    PartialEq,
    Eq,
    PartialOrd,
    Ord,
    Clone,
    Debug,
    Default,
    Hash,
    Serialize,
    Deserialize,
    DeriveValueType,
)]
#[serde(from = "CaseInsensitiveString")]
pub struct AttributeName(CaseInsensitiveString);

impl AttributeName {
    pub fn new(s: &str) -> Self {
        s.into()
    }
    pub fn as_str(&self) -> &str {
        self.0.as_str()
    }
    pub fn into_string(self) -> String {
        self.0.into_string()
    }
}
impl<T> From<T> for AttributeName
where
    T: Into<CaseInsensitiveString>,
{
    fn from(s: T) -> Self {
        Self(s.into())
    }
}
impl std::fmt::Display for AttributeName {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "{}", self.0.as_str())
    }
}
impl From<&AttributeName> for Value {
    fn from(attribute_name: &AttributeName) -> Self {
        attribute_name.as_str().into()
    }
}
impl TryFromU64 for AttributeName {
    fn try_from_u64(_n: u64) -> Result<Self, DbErr> {
        Err(DbErr::ConvertFromU64(
            "AttributeName cannot be constructed from u64",
        ))
    }
}

make_case_insensitive_comparable_string!(LdapObjectClass);
make_case_insensitive_comparable_string!(Email);
make_case_insensitive_comparable_string!(GroupName);

impl AsRef<GroupName> for GroupName {
    fn as_ref(&self) -> &GroupName {
        self
    }
}

#[derive(PartialEq, Eq, Clone, Serialize, Deserialize, DeriveValueType)]
#[sea_orm(column_type = "Binary(BlobSize::Long)", array_type = "Bytes")]
pub struct JpegPhoto(#[serde(with = "serde_bytes")] Vec<u8>);

impl From<&JpegPhoto> for Value {
    fn from(photo: &JpegPhoto) -> Self {
        photo.0.as_slice().into()
    }
}

impl TryFrom<&[u8]> for JpegPhoto {
    type Error = anyhow::Error;
    fn try_from(bytes: &[u8]) -> anyhow::Result<Self> {
        if bytes.is_empty() {
            return Ok(JpegPhoto::null());
        }
        // Confirm that it's a valid Jpeg, then store only the bytes.
        image::io::Reader::with_format(std::io::Cursor::new(bytes), image::ImageFormat::Jpeg)
            .decode()?;
        Ok(JpegPhoto(bytes.to_vec()))
    }
}

impl TryFrom<Vec<u8>> for JpegPhoto {
    type Error = anyhow::Error;
    fn try_from(bytes: Vec<u8>) -> anyhow::Result<Self> {
        if bytes.is_empty() {
            return Ok(JpegPhoto::null());
        }
        // Confirm that it's a valid Jpeg, then store only the bytes.
        image::io::Reader::with_format(
            std::io::Cursor::new(bytes.as_slice()),
            image::ImageFormat::Jpeg,
        )
        .decode()?;
        Ok(JpegPhoto(bytes))
    }
}

impl TryFrom<&str> for JpegPhoto {
    type Error = anyhow::Error;
    fn try_from(string: &str) -> anyhow::Result<Self> {
        // The String format is in base64.
        <Self as TryFrom<_>>::try_from(base64::engine::general_purpose::STANDARD.decode(string)?)
    }
}

impl From<&JpegPhoto> for String {
    fn from(val: &JpegPhoto) -> Self {
        base64::engine::general_purpose::STANDARD.encode(&val.0)
    }
}

impl std::fmt::Debug for JpegPhoto {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let mut encoded = base64::engine::general_purpose::STANDARD.encode(&self.0);
        if encoded.len() > 100 {
            encoded.truncate(100);
            encoded.push_str(" ...");
        };
        f.debug_tuple("JpegPhoto")
            .field(&format!("b64[{}]", encoded))
            .finish()
    }
}

impl JpegPhoto {
    pub fn is_empty(&self) -> bool {
        self.0.is_empty()
    }

    pub fn null() -> Self {
        Self(vec![])
    }

    pub fn into_bytes(self) -> Vec<u8> {
        self.0
    }

    #[cfg(test)]
    pub fn for_tests() -> Self {
        use image::{ImageOutputFormat, Rgb, RgbImage};
        let img = RgbImage::from_fn(32, 32, |x, y| {
            if (x + y) % 2 == 0 {
                Rgb([0, 0, 0])
            } else {
                Rgb([255, 255, 255])
            }
        });
        let mut bytes: Vec<u8> = Vec::new();
        img.write_to(
            &mut std::io::Cursor::new(&mut bytes),
            ImageOutputFormat::Jpeg(0),
        )
        .unwrap();
        Self(bytes)
    }
}

impl Nullable for JpegPhoto {
    fn null() -> Value {
        JpegPhoto::null().into()
    }
}

impl IntoActiveValue<Serialized> for JpegPhoto {
    fn into_active_value(self) -> sea_orm::ActiveValue<Serialized> {
        if self.is_empty() {
            sea_orm::ActiveValue::NotSet
        } else {
            sea_orm::ActiveValue::Set(Serialized::from(&self))
        }
    }
}

#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Hash)]
pub struct AttributeValue {
    pub name: AttributeName,
    pub value: Serialized,
}

#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
pub struct User {
    pub user_id: UserId,
    pub email: Email,
    pub display_name: Option<String>,
    pub creation_date: NaiveDateTime,
    pub uuid: Uuid,
    pub attributes: Vec<AttributeValue>,
}

#[cfg(test)]
impl Default for User {
    fn default() -> Self {
        let epoch = chrono::Utc.timestamp_opt(0, 0).unwrap().naive_utc();
        User {
            user_id: UserId::default(),
            email: Email::default(),
            display_name: None,
            creation_date: epoch,
            uuid: Uuid::from_name_and_date("", &epoch),
            attributes: Vec::new(),
        }
    }
}

#[derive(
    Debug,
    Copy,
    Clone,
    PartialEq,
    Eq,
    PartialOrd,
    Ord,
    Hash,
    Serialize,
    Deserialize,
    DeriveValueType,
)]
pub struct GroupId(pub i32);

impl TryFromU64 for GroupId {
    fn try_from_u64(n: u64) -> Result<Self, DbErr> {
        Ok(GroupId(i32::try_from_u64(n)?))
    }
}

impl From<&GroupId> for Value {
    fn from(id: &GroupId) -> Self {
        (*id).into()
    }
}

#[derive(
    Debug,
    Copy,
    Clone,
    PartialEq,
    Eq,
    Hash,
    Serialize,
    Deserialize,
    EnumString,
    IntoStaticStr,
    juniper::GraphQLEnum,
)]
pub enum AttributeType {
    String,
    Integer,
    JpegPhoto,
    DateTime,
}

impl From<AttributeType> for Value {
    fn from(attribute_type: AttributeType) -> Self {
        Into::<&'static str>::into(attribute_type).into()
    }
}

impl TryGetable for AttributeType {
    fn try_get_by<I: sea_orm::ColIdx>(res: &QueryResult, index: I) -> Result<Self, TryGetError> {
        use std::str::FromStr;
        Ok(AttributeType::from_str(&String::try_get_by(res, index)?).expect("Invalid enum value"))
    }
}

impl ValueType for AttributeType {
    fn try_from(v: Value) -> Result<Self, ValueTypeErr> {
        use std::str::FromStr;
        Ok(
            AttributeType::from_str(&<String as ValueType>::try_from(v)?)
                .expect("Invalid enum value"),
        )
    }

    fn type_name() -> String {
        "AttributeType".to_owned()
    }

    fn array_type() -> ArrayType {
        ArrayType::String
    }

    fn column_type() -> ColumnType {
        ColumnType::String(Some(64))
    }
}

#[derive(PartialEq, Eq, Debug, Serialize, Deserialize)]
pub struct Group {
    pub id: GroupId,
    pub display_name: GroupName,
    pub creation_date: NaiveDateTime,
    pub uuid: Uuid,
    pub users: Vec<UserId>,
    pub attributes: Vec<AttributeValue>,
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct GroupDetails {
    pub group_id: GroupId,
    pub display_name: GroupName,
    pub creation_date: NaiveDateTime,
    pub uuid: Uuid,
    pub attributes: Vec<AttributeValue>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UserAndGroups {
    pub user: User,
    pub groups: Option<Vec<GroupDetails>>,
}

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

    #[test]
    fn test_serialized_debug_string() {
        assert_eq!(
            &format!("{:?}", Serialized::from("abcd")),
            "Serialized(\"abcd\")"
        );
        assert_eq!(
            &format!("{:?}", Serialized::from(&1234i64)),
            "Serialized(\"1234\")"
        );
        assert_eq!(
            &format!("{:?}", Serialized::from(&JpegPhoto::for_tests())),
            "Serialized(\"hash: 0xB947C77A16F3C3BD\")"
        );
    }

    #[test]
    fn test_serialized_i64_len() {
        assert_eq!(SERIALIZED_I64_LEN, Serialized::from(&0i64).0.len());
        assert_eq!(
            SERIALIZED_I64_LEN,
            Serialized::from(&i64::max_value()).0.len()
        );
        assert_eq!(
            SERIALIZED_I64_LEN,
            Serialized::from(&i64::min_value()).0.len()
        );
        assert_eq!(SERIALIZED_I64_LEN, Serialized::from(&-1000i64).0.len());
    }
}
