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::{collections, collections_groups, groups, groups_users, users_organizations},
    },
    error::MapResult,
};
use macros::UuidFromParam;

use super::{CollectionId, Membership, MembershipId, OrganizationId, User, UserId};

#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = groups)]
#[diesel(treat_none_as_null = true)]
#[diesel(primary_key(uuid))]
pub struct Group {
    pub uuid: GroupId,
    pub organizations_uuid: OrganizationId,
    pub name: String,
    pub access_all: bool,
    pub external_id: Option<String>,
    pub creation_date: NaiveDateTime,
    pub revision_date: NaiveDateTime,
}

#[derive(Identifiable, Queryable, Insertable)]
#[diesel(table_name = collections_groups)]
#[diesel(primary_key(collections_uuid, groups_uuid))]
pub struct CollectionGroup {
    pub collections_uuid: CollectionId,
    pub groups_uuid: GroupId,
    pub read_only: bool,
    pub hide_passwords: bool,
    pub manage: bool,
}

#[derive(Identifiable, Queryable, Insertable)]
#[diesel(table_name = groups_users)]
#[diesel(primary_key(groups_uuid, users_organizations_uuid))]
pub struct GroupUser {
    pub groups_uuid: GroupId,
    pub users_organizations_uuid: MembershipId,
}

/// Local methods
impl Group {
    pub fn new(
        organizations_uuid: OrganizationId,
        name: String,
        access_all: bool,
        external_id: Option<String>,
    ) -> Self {
        let now = Utc::now().naive_utc();

        let mut new_model = Self {
            uuid: GroupId(crate::util::get_uuid()),
            organizations_uuid,
            name,
            access_all,
            external_id: None,
            creation_date: now,
            revision_date: now,
        };

        new_model.set_external_id(external_id);

        new_model
    }

    pub fn to_json(&self) -> Value {
        json!({
            "id": self.uuid,
            "organizationId": self.organizations_uuid,
            "name": self.name,
            "externalId": self.external_id,
            "object": "group"
        })
    }

    pub async fn to_json_details(&self, conn: &DbConn) -> Value {
        // If both read_only and hide_passwords are false, then manage should be true
        // You can't have an entry with read_only and manage, or hide_passwords and manage
        // Or an entry with everything to false
        let collections_groups: Vec<Value> = CollectionGroup::find_by_group(&self.uuid, &self.organizations_uuid, conn)
            .await
            .iter()
            .map(|entry| {
                json!({
                    "id": entry.collections_uuid,
                    "readOnly": entry.read_only,
                    "hidePasswords": entry.hide_passwords,
                    "manage": entry.manage,
                })
            })
            .collect();

        json!({
            "id": self.uuid,
            "organizationId": self.organizations_uuid,
            "name": self.name,
            "accessAll": self.access_all,
            "externalId": self.external_id,
            "collections": collections_groups,
            "object": "groupDetails"
        })
    }

    pub fn set_external_id(&mut self, external_id: Option<String>) {
        // Check if external_id is empty. We do not want to have empty strings in the database
        self.external_id = match external_id {
            Some(external_id) if !external_id.trim().is_empty() => Some(external_id),
            _ => None,
        };
    }
}

impl CollectionGroup {
    pub fn new(
        collections_uuid: CollectionId,
        groups_uuid: GroupId,
        read_only: bool,
        hide_passwords: bool,
        manage: bool,
    ) -> Self {
        Self {
            collections_uuid,
            groups_uuid,
            read_only,
            hide_passwords,
            manage,
        }
    }

    pub fn to_json_details_for_group(&self) -> Value {
        // If both read_only and hide_passwords are false, then manage should be true
        // You can't have an entry with read_only and manage, or hide_passwords and manage
        // Or an entry with everything to false
        // For backwards compatibility and migration proposes we keep checking read_only and hide_password
        json!({
            "id": self.groups_uuid,
            "readOnly": self.read_only,
            "hidePasswords": self.hide_passwords,
            "manage": self.manage || (!self.read_only && !self.hide_passwords),
        })
    }
}

impl GroupUser {
    pub fn new(groups_uuid: GroupId, users_organizations_uuid: MembershipId) -> Self {
        Self {
            groups_uuid,
            users_organizations_uuid,
        }
    }
}

/// Database methods
impl Group {
    pub async fn save(&mut self, conn: &DbConn) -> EmptyResult {
        self.revision_date = Utc::now().naive_utc();

        db_run! { conn:
            sqlite, mysql {
                match diesel::replace_into(groups::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(groups::table)
                            .filter(groups::uuid.eq(&self.uuid))
                            .set(&*self)
                            .execute(conn)
                            .map_res("Error saving group")
                    }
                    Err(e) => Err(e.into()),
                }.map_res("Error saving group")
            }
            postgresql {
                diesel::insert_into(groups::table)
                    .values(&*self)
                    .on_conflict(groups::uuid)
                    .do_update()
                    .set(&*self)
                    .execute(conn)
                    .map_res("Error saving group")
            }
        }
    }

    pub async fn delete_all_by_organization(org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult {
        for group in Self::find_by_organization(org_uuid, conn).await {
            group.delete(org_uuid, conn).await?;
        }
        Ok(())
    }

    pub async fn find_by_organization(org_uuid: &OrganizationId, conn: &DbConn) -> Vec<Self> {
        conn.run(move |conn| {
            groups::table
                .filter(groups::organizations_uuid.eq(org_uuid))
                .load::<Self>(conn)
                .expect("Error loading groups")
        })
        .await
    }

    pub async fn count_by_org(org_uuid: &OrganizationId, conn: &DbConn) -> i64 {
        conn.run(move |conn| {
            groups::table.filter(groups::organizations_uuid.eq(org_uuid)).count().first::<i64>(conn).ok().unwrap_or(0)
        })
        .await
    }

    pub async fn find_by_uuid_and_org(uuid: &GroupId, org_uuid: &OrganizationId, conn: &DbConn) -> Option<Self> {
        conn.run(move |conn| {
            groups::table
                .filter(groups::uuid.eq(uuid))
                .filter(groups::organizations_uuid.eq(org_uuid))
                .first::<Self>(conn)
                .ok()
        })
        .await
    }

    pub async fn find_by_external_id_and_org(
        external_id: &str,
        org_uuid: &OrganizationId,
        conn: &DbConn,
    ) -> Option<Self> {
        conn.run(move |conn| {
            groups::table
                .filter(groups::external_id.eq(external_id))
                .filter(groups::organizations_uuid.eq(org_uuid))
                .first::<Self>(conn)
                .ok()
        })
        .await
    }
    //Returns all organizations the user has full access to
    pub async fn get_orgs_by_user_with_full_access(user_uuid: &UserId, conn: &DbConn) -> Vec<OrganizationId> {
        conn.run(move |conn| {
            groups_users::table
                .inner_join(
                    users_organizations::table.on(users_organizations::uuid.eq(groups_users::users_organizations_uuid)),
                )
                .inner_join(
                    groups::table.on(groups::uuid
                        .eq(groups_users::groups_uuid)
                        .and(groups::organizations_uuid.eq(users_organizations::org_uuid))),
                )
                .filter(users_organizations::user_uuid.eq(user_uuid))
                .filter(groups::access_all.eq(true))
                .select(groups::organizations_uuid)
                .distinct()
                .load::<OrganizationId>(conn)
                .expect("Error loading organization group full access information for user")
        })
        .await
    }

    pub async fn is_in_full_access_group(user_uuid: &UserId, org_uuid: &OrganizationId, conn: &DbConn) -> bool {
        conn.run(move |conn| {
            groups::table
                .inner_join(groups_users::table.on(groups_users::groups_uuid.eq(groups::uuid)))
                .inner_join(
                    users_organizations::table.on(users_organizations::uuid
                        .eq(groups_users::users_organizations_uuid)
                        .and(users_organizations::org_uuid.eq(groups::organizations_uuid))),
                )
                .filter(users_organizations::user_uuid.eq(user_uuid))
                .filter(groups::organizations_uuid.eq(org_uuid))
                .filter(groups::access_all.eq(true))
                .select(groups::access_all)
                .first::<bool>(conn)
                .unwrap_or_default()
        })
        .await
    }

    pub async fn delete(&self, org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult {
        CollectionGroup::delete_all_by_group(&self.uuid, org_uuid, conn).await?;
        GroupUser::delete_all_by_group(&self.uuid, org_uuid, conn).await?;

        conn.run(move |conn| {
            diesel::delete(groups::table.filter(groups::uuid.eq(&self.uuid)))
                .execute(conn)
                .map_res("Error deleting group")
        })
        .await
    }

    pub async fn update_revision(uuid: &GroupId, conn: &DbConn) {
        if let Err(e) = Self::update_revision_impl(uuid, &Utc::now().naive_utc(), conn).await {
            warn!("Failed to update revision for {uuid}: {e:#?}");
        }
    }

    async fn update_revision_impl(uuid: &GroupId, date: &NaiveDateTime, conn: &DbConn) -> EmptyResult {
        conn.run(move |conn| {
            crate::util::retry(
                || {
                    diesel::update(groups::table.filter(groups::uuid.eq(uuid)))
                        .set(groups::revision_date.eq(date))
                        .execute(conn)
                },
                10,
            )
            .map_res("Error updating group revision")
        })
        .await
    }
}

impl CollectionGroup {
    pub async fn save(&mut self, org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult {
        let group_users = GroupUser::find_by_group(&self.groups_uuid, org_uuid, conn).await;
        for group_user in group_users {
            group_user.update_user_revision(conn).await;
        }

        db_run! { conn:
            sqlite, mysql {
                match diesel::replace_into(collections_groups::table)
                    .values((
                        collections_groups::collections_uuid.eq(&self.collections_uuid),
                        collections_groups::groups_uuid.eq(&self.groups_uuid),
                        collections_groups::read_only.eq(&self.read_only),
                        collections_groups::hide_passwords.eq(&self.hide_passwords),
                        collections_groups::manage.eq(&self.manage),
                    ))
                    .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(collections_groups::table)
                            .filter(collections_groups::collections_uuid.eq(&self.collections_uuid))
                            .filter(collections_groups::groups_uuid.eq(&self.groups_uuid))
                            .set((
                                collections_groups::collections_uuid.eq(&self.collections_uuid),
                                collections_groups::groups_uuid.eq(&self.groups_uuid),
                                collections_groups::read_only.eq(&self.read_only),
                                collections_groups::hide_passwords.eq(&self.hide_passwords),
                                collections_groups::manage.eq(&self.manage),
                            ))
                            .execute(conn)
                            .map_res("Error adding group to collection")
                    }
                    Err(e) => Err(e.into()),
                }.map_res("Error adding group to collection")
            }
            postgresql {
                diesel::insert_into(collections_groups::table)
                    .values((
                        collections_groups::collections_uuid.eq(&self.collections_uuid),
                        collections_groups::groups_uuid.eq(&self.groups_uuid),
                        collections_groups::read_only.eq(self.read_only),
                        collections_groups::hide_passwords.eq(self.hide_passwords),
                        collections_groups::manage.eq(self.manage),
                    ))
                    .on_conflict((collections_groups::collections_uuid, collections_groups::groups_uuid))
                    .do_update()
                    .set((
                        collections_groups::read_only.eq(self.read_only),
                        collections_groups::hide_passwords.eq(self.hide_passwords),
                        collections_groups::manage.eq(self.manage),
                    ))
                    .execute(conn)
                    .map_res("Error adding group to collection")
            }
        }
    }

    pub async fn find_by_group(group_uuid: &GroupId, org_uuid: &OrganizationId, conn: &DbConn) -> Vec<Self> {
        conn.run(move |conn| {
            collections_groups::table
                .inner_join(groups::table.on(groups::uuid.eq(collections_groups::groups_uuid)))
                .inner_join(
                    collections::table.on(collections::uuid
                        .eq(collections_groups::collections_uuid)
                        .and(collections::org_uuid.eq(groups::organizations_uuid))),
                )
                .filter(collections_groups::groups_uuid.eq(group_uuid))
                .filter(collections::org_uuid.eq(org_uuid))
                .select(collections_groups::all_columns)
                .load::<Self>(conn)
                .expect("Error loading collection groups")
        })
        .await
    }

    pub async fn find_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<Self> {
        conn.run(move |conn| {
            collections_groups::table
                .inner_join(groups_users::table.on(groups_users::groups_uuid.eq(collections_groups::groups_uuid)))
                .inner_join(
                    users_organizations::table.on(users_organizations::uuid.eq(groups_users::users_organizations_uuid)),
                )
                .inner_join(
                    groups::table.on(groups::uuid
                        .eq(collections_groups::groups_uuid)
                        .and(groups::organizations_uuid.eq(users_organizations::org_uuid))),
                )
                .inner_join(
                    collections::table.on(collections::uuid
                        .eq(collections_groups::collections_uuid)
                        .and(collections::org_uuid.eq(groups::organizations_uuid))),
                )
                .filter(users_organizations::user_uuid.eq(user_uuid))
                .select(collections_groups::all_columns)
                .load::<Self>(conn)
                .expect("Error loading user collection groups")
        })
        .await
    }

    pub async fn find_by_collection(collection_uuid: &CollectionId, conn: &DbConn) -> Vec<Self> {
        conn.run(move |conn| {
            collections_groups::table
                .filter(collections_groups::collections_uuid.eq(collection_uuid))
                .inner_join(collections::table.on(collections::uuid.eq(collections_groups::collections_uuid)))
                .inner_join(
                    groups::table.on(groups::uuid
                        .eq(collections_groups::groups_uuid)
                        .and(groups::organizations_uuid.eq(collections::org_uuid))),
                )
                .select(collections_groups::all_columns)
                .load::<Self>(conn)
                .expect("Error loading collection groups")
        })
        .await
    }

    pub async fn delete(&self, org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult {
        let group_users = GroupUser::find_by_group(&self.groups_uuid, org_uuid, conn).await;
        for group_user in group_users {
            group_user.update_user_revision(conn).await;
        }

        conn.run(move |conn| {
            diesel::delete(collections_groups::table)
                .filter(collections_groups::collections_uuid.eq(&self.collections_uuid))
                .filter(collections_groups::groups_uuid.eq(&self.groups_uuid))
                .execute(conn)
                .map_res("Error deleting collection group")
        })
        .await
    }

    pub async fn delete_all_by_group(group_uuid: &GroupId, org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult {
        let group_users = GroupUser::find_by_group(group_uuid, org_uuid, conn).await;
        for group_user in group_users {
            group_user.update_user_revision(conn).await;
        }

        conn.run(move |conn| {
            diesel::delete(collections_groups::table)
                .filter(collections_groups::groups_uuid.eq(group_uuid))
                .execute(conn)
                .map_res("Error deleting collection group")
        })
        .await
    }

    pub async fn delete_all_by_collection(
        collection_uuid: &CollectionId,
        org_uuid: &OrganizationId,
        conn: &DbConn,
    ) -> EmptyResult {
        let collection_assigned_to_groups = CollectionGroup::find_by_collection(collection_uuid, conn).await;
        for collection_assigned_to_group in collection_assigned_to_groups {
            let group_users = GroupUser::find_by_group(&collection_assigned_to_group.groups_uuid, org_uuid, conn).await;
            for group_user in group_users {
                group_user.update_user_revision(conn).await;
            }
        }

        conn.run(move |conn| {
            diesel::delete(collections_groups::table)
                .filter(collections_groups::collections_uuid.eq(collection_uuid))
                .execute(conn)
                .map_res("Error deleting collection group")
        })
        .await
    }
}

impl GroupUser {
    pub async fn save(&mut self, conn: &DbConn) -> EmptyResult {
        self.update_user_revision(conn).await;

        db_run! { conn:
            sqlite, mysql {
                match diesel::replace_into(groups_users::table)
                    .values((
                        groups_users::users_organizations_uuid.eq(&self.users_organizations_uuid),
                        groups_users::groups_uuid.eq(&self.groups_uuid),
                    ))
                    .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(groups_users::table)
                            .filter(groups_users::users_organizations_uuid.eq(&self.users_organizations_uuid))
                            .filter(groups_users::groups_uuid.eq(&self.groups_uuid))
                            .set((
                                groups_users::users_organizations_uuid.eq(&self.users_organizations_uuid),
                                groups_users::groups_uuid.eq(&self.groups_uuid),
                            ))
                            .execute(conn)
                            .map_res("Error adding user to group")
                    }
                    Err(e) => Err(e.into()),
                }.map_res("Error adding user to group")
            }
            postgresql {
                diesel::insert_into(groups_users::table)
                    .values((
                        groups_users::users_organizations_uuid.eq(&self.users_organizations_uuid),
                        groups_users::groups_uuid.eq(&self.groups_uuid),
                    ))
                    .on_conflict((groups_users::users_organizations_uuid, groups_users::groups_uuid))
                    .do_update()
                    .set((
                        groups_users::users_organizations_uuid.eq(&self.users_organizations_uuid),
                        groups_users::groups_uuid.eq(&self.groups_uuid),
                    ))
                    .execute(conn)
                    .map_res("Error adding user to group")
            }
        }
    }

    pub async fn find_by_group(group_uuid: &GroupId, org_uuid: &OrganizationId, conn: &DbConn) -> Vec<Self> {
        conn.run(move |conn| {
            groups_users::table
                .inner_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid)))
                .inner_join(
                    users_organizations::table.on(users_organizations::uuid
                        .eq(groups_users::users_organizations_uuid)
                        .and(users_organizations::org_uuid.eq(groups::organizations_uuid))),
                )
                .filter(groups_users::groups_uuid.eq(group_uuid))
                .filter(groups::organizations_uuid.eq(org_uuid))
                .select(groups_users::all_columns)
                .load::<Self>(conn)
                .expect("Error loading group users")
        })
        .await
    }

    pub async fn find_by_member(member_uuid: &MembershipId, conn: &DbConn) -> Vec<Self> {
        conn.run(move |conn| {
            groups_users::table
                .filter(groups_users::users_organizations_uuid.eq(member_uuid))
                .load::<Self>(conn)
                .expect("Error loading groups for user")
        })
        .await
    }

    pub async fn has_access_to_collection_by_member(
        collection_uuid: &CollectionId,
        member_uuid: &MembershipId,
        conn: &DbConn,
    ) -> bool {
        conn.run(move |conn| {
            groups_users::table
                .inner_join(collections_groups::table.on(collections_groups::groups_uuid.eq(groups_users::groups_uuid)))
                .inner_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid)))
                .inner_join(
                    collections::table.on(collections::uuid
                        .eq(collections_groups::collections_uuid)
                        .and(collections::org_uuid.eq(groups::organizations_uuid))),
                )
                .filter(collections_groups::collections_uuid.eq(collection_uuid))
                .filter(groups_users::users_organizations_uuid.eq(member_uuid))
                .count()
                .first::<i64>(conn)
                .unwrap_or(0)
                != 0
        })
        .await
    }

    pub async fn has_full_access_by_member(
        org_uuid: &OrganizationId,
        member_uuid: &MembershipId,
        conn: &DbConn,
    ) -> bool {
        conn.run(move |conn| {
            groups_users::table
                .inner_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid)))
                .filter(groups::organizations_uuid.eq(org_uuid))
                .filter(groups::access_all.eq(true))
                .filter(groups_users::users_organizations_uuid.eq(member_uuid))
                .count()
                .first::<i64>(conn)
                .unwrap_or(0)
                != 0
        })
        .await
    }

    pub async fn update_user_revision(&self, conn: &DbConn) {
        match Membership::find_by_uuid(&self.users_organizations_uuid, conn).await {
            Some(member) => User::update_uuid_revision(&member.user_uuid, conn).await,
            None => warn!("Member could not be found!"),
        }
    }

    pub async fn delete_by_group_and_member(
        group_uuid: &GroupId,
        member_uuid: &MembershipId,
        conn: &DbConn,
    ) -> EmptyResult {
        match Membership::find_by_uuid(member_uuid, conn).await {
            Some(member) => User::update_uuid_revision(&member.user_uuid, conn).await,
            None => warn!("Member could not be found!"),
        }

        conn.run(move |conn| {
            diesel::delete(groups_users::table)
                .filter(groups_users::groups_uuid.eq(group_uuid))
                .filter(groups_users::users_organizations_uuid.eq(member_uuid))
                .execute(conn)
                .map_res("Error deleting group users")
        })
        .await
    }

    pub async fn delete_all_by_group(group_uuid: &GroupId, org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult {
        let group_users = GroupUser::find_by_group(group_uuid, org_uuid, conn).await;
        for group_user in group_users {
            group_user.update_user_revision(conn).await;
        }

        conn.run(move |conn| {
            diesel::delete(groups_users::table)
                .filter(groups_users::groups_uuid.eq(group_uuid))
                .execute(conn)
                .map_res("Error deleting group users")
        })
        .await
    }

    pub async fn delete_all_by_member(member_uuid: &MembershipId, conn: &DbConn) -> EmptyResult {
        match Membership::find_by_uuid(member_uuid, conn).await {
            Some(member) => User::update_uuid_revision(&member.user_uuid, conn).await,
            None => warn!("Member could not be found!"),
        }

        conn.run(move |conn| {
            diesel::delete(groups_users::table)
                .filter(groups_users::users_organizations_uuid.eq(member_uuid))
                .execute(conn)
                .map_res("Error deleting user groups")
        })
        .await
    }
}

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