use chrono::{NaiveDateTime, Utc};
use derive_more::{AsRef, Deref, Display, From};
use diesel::prelude::*;
use serde_json::Value;

use crate::{
    api::EmptyResult,
    db::{DbConn, schema::emergency_access},
    error::MapResult,
};
use macros::UuidFromParam;

use super::{User, UserId};

#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = emergency_access)]
#[diesel(treat_none_as_null = true)]
#[diesel(primary_key(uuid))]
pub struct EmergencyAccess {
    pub uuid: EmergencyAccessId,
    pub grantor_uuid: UserId,
    pub grantee_uuid: Option<UserId>,
    pub email: Option<String>,
    pub key_encrypted: Option<String>,
    pub atype: i32,  //EmergencyAccessType
    pub status: i32, //EmergencyAccessStatus
    pub wait_time_days: i32,
    pub recovery_initiated_at: Option<NaiveDateTime>,
    pub last_notification_at: Option<NaiveDateTime>,
    pub updated_at: NaiveDateTime,
    pub created_at: NaiveDateTime,
}

// Local methods
impl EmergencyAccess {
    pub fn new(grantor_uuid: UserId, email: String, status: i32, atype: i32, wait_time_days: i32) -> Self {
        let now = Utc::now().naive_utc();

        Self {
            uuid: EmergencyAccessId(crate::util::get_uuid()),
            grantor_uuid,
            grantee_uuid: None,
            email: Some(email),
            status,
            atype,
            wait_time_days,
            recovery_initiated_at: None,
            created_at: now,
            updated_at: now,
            key_encrypted: None,
            last_notification_at: None,
        }
    }

    pub fn get_type_as_str(&self) -> &'static str {
        if self.atype == EmergencyAccessType::View as i32 {
            "View"
        } else {
            "Takeover"
        }
    }

    pub fn to_json(&self) -> Value {
        json!({
            "id": self.uuid,
            "status": self.status,
            "type": self.atype,
            "waitTimeDays": self.wait_time_days,
            "object": "emergencyAccess",
        })
    }

    pub async fn to_json_grantor_details(&self, conn: &DbConn) -> Value {
        let grantor_user = User::find_by_uuid(&self.grantor_uuid, conn).await.expect("Grantor user not found.");

        json!({
            "id": self.uuid,
            "status": self.status,
            "type": self.atype,
            "waitTimeDays": self.wait_time_days,
            "grantorId": grantor_user.uuid,
            "email": grantor_user.email,
            "name": grantor_user.name,
            "avatarColor": grantor_user.avatar_color,
            "object": "emergencyAccessGrantorDetails",
        })
    }

    pub async fn to_json_grantee_details(&self, conn: &DbConn) -> Option<Value> {
        let grantee_user = if let Some(grantee_uuid) = &self.grantee_uuid {
            User::find_by_uuid(grantee_uuid, conn).await.expect("Grantee user not found.")
        } else {
            let email = self.email.as_deref()?;
            if let Some(user) = User::find_by_mail(email, conn).await {
                user
            } else {
                // remove outstanding invitations which should not exist
                Self::delete_all_by_grantee_email(email, conn).await.ok();
                return None;
            }
        };

        Some(json!({
            "id": self.uuid,
            "status": self.status,
            "type": self.atype,
            "waitTimeDays": self.wait_time_days,
            "granteeId": grantee_user.uuid,
            "email": grantee_user.email,
            "name": grantee_user.name,
            "avatarColor": grantee_user.avatar_color,
            "object": "emergencyAccessGranteeDetails",
        }))
    }
}

#[derive(Copy, Clone)]
pub enum EmergencyAccessType {
    View = 0,
    Takeover = 1,
}

impl EmergencyAccessType {
    pub fn from_str(s: &str) -> Option<Self> {
        match s {
            "0" | "View" => Some(EmergencyAccessType::View),
            "1" | "Takeover" => Some(EmergencyAccessType::Takeover),
            _ => None,
        }
    }
}

pub enum EmergencyAccessStatus {
    Invited = 0,
    Accepted = 1,
    Confirmed = 2,
    RecoveryInitiated = 3,
    RecoveryApproved = 4,
}

// region Database methods

impl EmergencyAccess {
    pub async fn save(&mut self, conn: &DbConn) -> EmptyResult {
        User::update_uuid_revision(&self.grantor_uuid, conn).await;
        self.updated_at = Utc::now().naive_utc();

        db_run! { conn:
            sqlite, mysql {
                match diesel::replace_into(emergency_access::table)
                    .values(&*self)
                    .execute(conn)
                {
                    Ok(_) => Ok(()),
                    // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first.
                    Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => {
                        diesel::update(emergency_access::table)
                            .filter(emergency_access::uuid.eq(&self.uuid))
                            .set(&*self)
                            .execute(conn)
                            .map_res("Error updating emergency access")
                    }
                    Err(e) => Err(e.into()),
                }.map_res("Error saving emergency access")
            }
            postgresql {
                diesel::insert_into(emergency_access::table)
                    .values(&*self)
                    .on_conflict(emergency_access::uuid)
                    .do_update()
                    .set(&*self)
                    .execute(conn)
                    .map_res("Error saving emergency access")
            }
        }
    }

    pub async fn update_access_status_and_save(
        &mut self,
        status: i32,
        date: &NaiveDateTime,
        conn: &DbConn,
    ) -> EmptyResult {
        // Update the grantee so that it will refresh it's status.
        User::update_uuid_revision(self.grantee_uuid.as_ref().expect("Error getting grantee"), conn).await;
        self.status = status;
        date.clone_into(&mut self.updated_at);

        conn.run(move |conn| {
            crate::util::retry(
                || {
                    diesel::update(emergency_access::table.filter(emergency_access::uuid.eq(&self.uuid)))
                        .set((emergency_access::status.eq(status), emergency_access::updated_at.eq(date)))
                        .execute(conn)
                },
                10,
            )
            .map_res("Error updating emergency access status")
        })
        .await
    }

    pub async fn update_last_notification_date_and_save(&mut self, date: &NaiveDateTime, conn: &DbConn) -> EmptyResult {
        self.last_notification_at = Some(date.to_owned());
        date.clone_into(&mut self.updated_at);

        conn.run(move |conn| {
            crate::util::retry(
                || {
                    diesel::update(emergency_access::table.filter(emergency_access::uuid.eq(&self.uuid)))
                        .set((emergency_access::last_notification_at.eq(date), emergency_access::updated_at.eq(date)))
                        .execute(conn)
                },
                10,
            )
            .map_res("Error updating emergency access status")
        })
        .await
    }

    pub async fn delete_all_by_user(user_uuid: &UserId, conn: &DbConn) -> EmptyResult {
        for ea in Self::find_all_by_grantor_uuid(user_uuid, conn).await {
            ea.delete(conn).await?;
        }
        for ea in Self::find_all_by_grantee_uuid(user_uuid, conn).await {
            ea.delete(conn).await?;
        }
        Ok(())
    }

    pub async fn delete_all_by_grantee_email(grantee_email: &str, conn: &DbConn) -> EmptyResult {
        for ea in Self::find_all_invited_by_grantee_email(grantee_email, conn).await {
            ea.delete(conn).await?;
        }
        Ok(())
    }

    pub async fn delete(self, conn: &DbConn) -> EmptyResult {
        User::update_uuid_revision(&self.grantor_uuid, conn).await;

        conn.run(move |conn| {
            diesel::delete(emergency_access::table.filter(emergency_access::uuid.eq(self.uuid)))
                .execute(conn)
                .map_res("Error removing user from emergency access")
        })
        .await
    }

    pub async fn find_by_grantor_uuid_and_grantee_uuid_or_email(
        grantor_uuid: &UserId,
        grantee_uuid: &UserId,
        email: &str,
        conn: &DbConn,
    ) -> Option<Self> {
        conn.run(move |conn| {
            emergency_access::table
                .filter(emergency_access::grantor_uuid.eq(grantor_uuid))
                .filter(emergency_access::grantee_uuid.eq(grantee_uuid).or(emergency_access::email.eq(email)))
                .first::<Self>(conn)
                .ok()
        })
        .await
    }

    pub async fn find_all_recoveries_initiated(conn: &DbConn) -> Vec<Self> {
        conn.run(move |conn| {
            emergency_access::table
                .filter(emergency_access::status.eq(EmergencyAccessStatus::RecoveryInitiated as i32))
                .filter(emergency_access::recovery_initiated_at.is_not_null())
                .load::<Self>(conn)
                .expect("Error loading emergency_access")
        })
        .await
    }

    pub async fn find_by_uuid_and_grantor_uuid(
        uuid: &EmergencyAccessId,
        grantor_uuid: &UserId,
        conn: &DbConn,
    ) -> Option<Self> {
        conn.run(move |conn| {
            emergency_access::table
                .filter(emergency_access::uuid.eq(uuid))
                .filter(emergency_access::grantor_uuid.eq(grantor_uuid))
                .first::<Self>(conn)
                .ok()
        })
        .await
    }

    pub async fn find_by_uuid_and_grantee_uuid(
        uuid: &EmergencyAccessId,
        grantee_uuid: &UserId,
        conn: &DbConn,
    ) -> Option<Self> {
        conn.run(move |conn| {
            emergency_access::table
                .filter(emergency_access::uuid.eq(uuid))
                .filter(emergency_access::grantee_uuid.eq(grantee_uuid))
                .first::<Self>(conn)
                .ok()
        })
        .await
    }

    pub async fn find_by_uuid_and_grantee_email(
        uuid: &EmergencyAccessId,
        grantee_email: &str,
        conn: &DbConn,
    ) -> Option<Self> {
        conn.run(move |conn| {
            emergency_access::table
                .filter(emergency_access::uuid.eq(uuid))
                .filter(emergency_access::email.eq(grantee_email))
                .first::<Self>(conn)
                .ok()
        })
        .await
    }

    pub async fn find_all_by_grantee_uuid(grantee_uuid: &UserId, conn: &DbConn) -> Vec<Self> {
        conn.run(move |conn| {
            emergency_access::table
                .filter(emergency_access::grantee_uuid.eq(grantee_uuid))
                .load::<Self>(conn)
                .expect("Error loading emergency_access")
        })
        .await
    }

    pub async fn find_invited_by_grantee_email(grantee_email: &str, conn: &DbConn) -> Option<Self> {
        conn.run(move |conn| {
            emergency_access::table
                .filter(emergency_access::email.eq(grantee_email))
                .filter(emergency_access::status.eq(EmergencyAccessStatus::Invited as i32))
                .first::<Self>(conn)
                .ok()
        })
        .await
    }

    pub async fn find_all_invited_by_grantee_email(grantee_email: &str, conn: &DbConn) -> Vec<Self> {
        conn.run(move |conn| {
            emergency_access::table
                .filter(emergency_access::email.eq(grantee_email))
                .filter(emergency_access::status.eq(EmergencyAccessStatus::Invited as i32))
                .load::<Self>(conn)
                .expect("Error loading emergency_access")
        })
        .await
    }

    pub async fn find_all_by_grantor_uuid(grantor_uuid: &UserId, conn: &DbConn) -> Vec<Self> {
        conn.run(move |conn| {
            emergency_access::table
                .filter(emergency_access::grantor_uuid.eq(grantor_uuid))
                .load::<Self>(conn)
                .expect("Error loading emergency_access")
        })
        .await
    }

    pub async fn find_all_confirmed_by_grantor_uuid(grantor_uuid: &UserId, conn: &DbConn) -> Vec<Self> {
        conn.run(move |conn| {
            emergency_access::table
                .filter(emergency_access::grantor_uuid.eq(grantor_uuid))
                .filter(emergency_access::status.ge(EmergencyAccessStatus::Confirmed as i32))
                .load::<Self>(conn)
                .expect("Error loading emergency_access")
        })
        .await
    }

    pub async fn accept_invite(&mut self, grantee_uuid: &UserId, grantee_email: &str, conn: &DbConn) -> EmptyResult {
        if self.email.is_none() || self.email.as_ref().unwrap() != grantee_email {
            err!("User email does not match invite.");
        }

        if self.status == EmergencyAccessStatus::Accepted as i32 {
            err!("Emergency contact already accepted.");
        }

        self.status = EmergencyAccessStatus::Accepted as i32;
        self.grantee_uuid = Some(grantee_uuid.clone());
        self.email = None;
        self.save(conn).await
    }
}

// endregion

#[derive(
    Clone,
    Debug,
    AsRef,
    Deref,
    DieselNewType,
    Display,
    From,
    FromForm,
    Hash,
    PartialEq,
    Eq,
    Serialize,
    Deserialize,
    UuidFromParam,
)]
pub struct EmergencyAccessId(String);
