use derive_more::{AsRef, From};
use diesel::prelude::*;
use serde::Deserialize;
use serde_json::Value;

use crate::{
    CONFIG,
    api::{EmptyResult, core::two_factor},
    db::{
        DbConn,
        schema::{org_policies, users_organizations},
    },
    error::MapResult,
};

use super::{Membership, MembershipId, MembershipStatus, MembershipType, OrganizationId, TwoFactor, UserId};

#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = org_policies)]
#[diesel(primary_key(uuid))]
pub struct OrgPolicy {
    pub uuid: OrgPolicyId,
    pub org_uuid: OrganizationId,
    pub atype: i32,
    pub enabled: bool,
    pub data: String,
}

// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Enums/PolicyType.cs
#[derive(Copy, Clone, Eq, PartialEq, num_derive::FromPrimitive)]
pub enum OrgPolicyType {
    TwoFactorAuthentication = 0,
    MasterPassword = 1,
    PasswordGenerator = 2,
    SingleOrg = 3,
    // RequireSso = 4, // Not supported
    PersonalOwnership = 5,
    DisableSend = 6,
    SendOptions = 7,
    ResetPassword = 8,
    // MaximumVaultTimeout = 9, // Not supported (Not AGPLv3 Licensed)
    // DisablePersonalVaultExport = 10, // Not supported (Not AGPLv3 Licensed)
    // ActivateAutofill = 11,
    // AutomaticAppLogIn = 12,
    // FreeFamiliesSponsorshipPolicy = 13,
    RemoveUnlockWithPin = 14,
    RestrictedItemTypes = 15,
    UriMatchDefaults = 16,
    // AutotypeDefaultSetting = 17, // Not supported yet
    // AutoConfirm = 18, // Not supported (not implemented yet)
    // BlockClaimedDomainAccountCreation = 19, // Not supported (Not AGPLv3 Licensed)
}

// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Models/Data/Organizations/Policies/SendOptionsPolicyData.cs#L5
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SendOptionsPolicyData {
    #[serde(rename = "disableHideEmail", alias = "DisableHideEmail")]
    pub disable_hide_email: bool,
}

// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Models/Data/Organizations/Policies/ResetPasswordDataModel.cs
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResetPasswordDataModel {
    #[serde(rename = "autoEnrollEnabled", alias = "AutoEnrollEnabled")]
    pub auto_enroll_enabled: bool,
}

/// Local methods
impl OrgPolicy {
    pub fn new(org_uuid: OrganizationId, atype: OrgPolicyType, enabled: bool, data: String) -> Self {
        Self {
            uuid: OrgPolicyId(crate::util::get_uuid()),
            org_uuid,
            atype: atype as i32,
            enabled,
            data,
        }
    }

    pub fn has_type(&self, policy_type: OrgPolicyType) -> bool {
        self.atype == policy_type as i32
    }

    pub fn to_json(&self) -> Value {
        let data_json: Value = serde_json::from_str(&self.data).unwrap_or(Value::Null);
        let mut policy = json!({
            "id": self.uuid,
            "organizationId": self.org_uuid,
            "type": self.atype,
            "data": data_json,
            "enabled": self.enabled,
            "object": "policy",
        });

        // Upstream adds this key/value
        // Allow enabling Single Org policy when the organization has claimed domains.
        // See: (https://github.com/bitwarden/server/pull/5565)
        // We return the same to prevent possible issues
        if self.atype == 8i32 {
            policy["canToggleState"] = json!(true);
        }

        policy
    }
}

/// Database methods
impl OrgPolicy {
    pub async fn save(&self, conn: &DbConn) -> EmptyResult {
        db_run! { conn:
            sqlite, mysql {
                match diesel::replace_into(org_policies::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(org_policies::table)
                            .filter(org_policies::uuid.eq(&self.uuid))
                            .set(self)
                            .execute(conn)
                            .map_res("Error saving org_policy")
                    }
                    Err(e) => Err(e.into()),
                }.map_res("Error saving org_policy")
            }
            postgresql {
                // We need to make sure we're not going to violate the unique constraint on org_uuid and atype.
                // This happens automatically on other DBMS backends due to replace_into(). PostgreSQL does
                // not support multiple constraints on ON CONFLICT clauses.
                let _: () = diesel::delete(
                    org_policies::table
                        .filter(org_policies::org_uuid.eq(&self.org_uuid))
                        .filter(org_policies::atype.eq(&self.atype)),
                )
                .execute(conn)
                .map_res("Error deleting org_policy for insert")?;

                diesel::insert_into(org_policies::table)
                    .values(self)
                    .on_conflict(org_policies::uuid)
                    .do_update()
                    .set(self)
                    .execute(conn)
                    .map_res("Error saving org_policy")
            }
        }
    }

    pub async fn delete(self, conn: &DbConn) -> EmptyResult {
        conn.run(move |conn| {
            diesel::delete(org_policies::table.filter(org_policies::uuid.eq(self.uuid)))
                .execute(conn)
                .map_res("Error deleting org_policy")
        })
        .await
    }

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

    pub async fn find_confirmed_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<Self> {
        conn.run(move |conn| {
            org_policies::table
                .inner_join(
                    users_organizations::table.on(users_organizations::org_uuid
                        .eq(org_policies::org_uuid)
                        .and(users_organizations::user_uuid.eq(user_uuid))),
                )
                .filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
                .select(org_policies::all_columns)
                .load::<Self>(conn)
                .expect("Error loading org_policy")
        })
        .await
    }

    pub async fn find_by_org_and_type(
        org_uuid: &OrganizationId,
        policy_type: OrgPolicyType,
        conn: &DbConn,
    ) -> Option<Self> {
        conn.run(move |conn| {
            org_policies::table
                .filter(org_policies::org_uuid.eq(org_uuid))
                .filter(org_policies::atype.eq(policy_type as i32))
                .first::<Self>(conn)
                .ok()
        })
        .await
    }

    pub async fn delete_all_by_organization(org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult {
        conn.run(move |conn| {
            diesel::delete(org_policies::table.filter(org_policies::org_uuid.eq(org_uuid)))
                .execute(conn)
                .map_res("Error deleting org_policy")
        })
        .await
    }

    pub async fn find_accepted_and_confirmed_by_user_and_active_policy(
        user_uuid: &UserId,
        policy_type: OrgPolicyType,
        conn: &DbConn,
    ) -> Vec<Self> {
        conn.run(move |conn| {
            org_policies::table
                .inner_join(
                    users_organizations::table.on(users_organizations::org_uuid
                        .eq(org_policies::org_uuid)
                        .and(users_organizations::user_uuid.eq(user_uuid))),
                )
                .filter(users_organizations::status.eq(MembershipStatus::Accepted as i32))
                .or_filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
                .filter(org_policies::atype.eq(policy_type as i32))
                .filter(org_policies::enabled.eq(true))
                .select(org_policies::all_columns)
                .load::<Self>(conn)
                .expect("Error loading org_policy")
        })
        .await
    }

    pub async fn find_confirmed_by_user_and_active_policy(
        user_uuid: &UserId,
        policy_type: OrgPolicyType,
        conn: &DbConn,
    ) -> Vec<Self> {
        conn.run(move |conn| {
            org_policies::table
                .inner_join(
                    users_organizations::table.on(users_organizations::org_uuid
                        .eq(org_policies::org_uuid)
                        .and(users_organizations::user_uuid.eq(user_uuid))),
                )
                .filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
                .filter(org_policies::atype.eq(policy_type as i32))
                .filter(org_policies::enabled.eq(true))
                .select(org_policies::all_columns)
                .load::<Self>(conn)
                .expect("Error loading org_policy")
        })
        .await
    }

    /// Returns true if the user belongs to an org that has enabled the specified policy type,
    /// and the user is not an owner or admin of that org. This is only useful for checking
    /// applicability of policy types that have these particular semantics.
    pub async fn is_applicable_to_user(
        user_uuid: &UserId,
        policy_type: OrgPolicyType,
        exclude_org_uuid: Option<&OrganizationId>,
        conn: &DbConn,
    ) -> bool {
        for policy in
            OrgPolicy::find_accepted_and_confirmed_by_user_and_active_policy(user_uuid, policy_type, conn).await
        {
            // Check if we need to skip this organization.
            if exclude_org_uuid.is_some() && *exclude_org_uuid.unwrap() == policy.org_uuid {
                continue;
            }

            if let Some(user) = Membership::find_confirmed_by_user_and_org(user_uuid, &policy.org_uuid, conn).await
                && user.atype < MembershipType::Admin
            {
                return true;
            }
        }
        false
    }

    pub async fn check_user_allowed(m: &Membership, action: &str, conn: &DbConn) -> EmptyResult {
        if m.atype < MembershipType::Admin && m.status > (MembershipStatus::Invited as i32) {
            // Enforce TwoFactor/TwoStep login
            if let Some(p) = Self::find_by_org_and_type(&m.org_uuid, OrgPolicyType::TwoFactorAuthentication, conn).await
                && p.enabled
                && TwoFactor::find_by_user(&m.user_uuid, conn).await.is_empty()
            {
                if CONFIG.email_2fa_auto_fallback() {
                    two_factor::email::find_and_activate_email_2fa(&m.user_uuid, conn).await?;
                } else {
                    err!(format!("Cannot {} because 2FA is required (membership {})", action, m.uuid));
                }
            }

            // Check if the user is part of another Organization with SingleOrg activated
            if Self::is_applicable_to_user(&m.user_uuid, OrgPolicyType::SingleOrg, Some(&m.org_uuid), conn).await {
                err!(format!(
                    "Cannot {} because another organization policy forbids it (membership {})",
                    action, m.uuid
                ));
            }

            if let Some(p) = Self::find_by_org_and_type(&m.org_uuid, OrgPolicyType::SingleOrg, conn).await
                && p.enabled
                && Membership::count_accepted_and_confirmed_by_user(&m.user_uuid, &m.org_uuid, conn).await > 0
            {
                err!(format!(
                    "Cannot {} because the organization policy forbids being part of other organization (membership {})",
                    action, m.uuid
                ));
            }
        }

        Ok(())
    }

    pub async fn org_is_reset_password_auto_enroll(org_uuid: &OrganizationId, conn: &DbConn) -> bool {
        match OrgPolicy::find_by_org_and_type(org_uuid, OrgPolicyType::ResetPassword, conn).await {
            Some(policy) => match serde_json::from_str::<ResetPasswordDataModel>(&policy.data) {
                Ok(opts) => {
                    return policy.enabled && opts.auto_enroll_enabled;
                }
                _ => error!("Failed to deserialize ResetPasswordDataModel: {}", policy.data),
            },
            None => return false,
        }

        false
    }

    /// Returns true if the user belongs to an org that has enabled the `DisableHideEmail`
    /// option of the `Send Options` policy, and the user is not an owner or admin of that org.
    pub async fn is_hide_email_disabled(user_uuid: &UserId, conn: &DbConn) -> bool {
        for policy in
            OrgPolicy::find_confirmed_by_user_and_active_policy(user_uuid, OrgPolicyType::SendOptions, conn).await
        {
            if let Some(user) = Membership::find_confirmed_by_user_and_org(user_uuid, &policy.org_uuid, conn).await
                && user.atype < MembershipType::Admin
            {
                match serde_json::from_str::<SendOptionsPolicyData>(&policy.data) {
                    Ok(opts) => {
                        if opts.disable_hide_email {
                            return true;
                        }
                    }
                    _ => error!("Failed to deserialize SendOptionsPolicyData: {}", policy.data),
                }
            }
        }
        false
    }

    pub async fn is_enabled_for_member(member_uuid: &MembershipId, policy_type: OrgPolicyType, conn: &DbConn) -> bool {
        if let Some(member) = Membership::find_by_uuid(member_uuid, conn).await
            && let Some(policy) = OrgPolicy::find_by_org_and_type(&member.org_uuid, policy_type, conn).await
        {
            return policy.enabled;
        }
        false
    }
}

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