use crate::prelude::*;
use hashbrown::HashMap;
use std::collections::BTreeSet;

use super::profiles::{
    AccessControlModify, AccessControlModifyResolved, AccessControlReceiverCondition,
    AccessControlTargetCondition,
};
use super::{AccessResult, AccessResultClass};
use std::sync::Arc;

pub(super) enum ModifyResult<'a> {
    Denied,
    Grant,
    Allow {
        pres: BTreeSet<Attribute>,
        rem: BTreeSet<Attribute>,
        cls: BTreeSet<&'a str>,
    },
}

pub(super) fn apply_modify_access<'a>(
    ident: &Identity,
    related_acp: &'a [AccessControlModifyResolved],
    sync_agreements: &HashMap<Uuid, BTreeSet<Attribute>>,
    entry: &Arc<EntrySealedCommitted>,
) -> ModifyResult<'a> {
    let mut denied = false;
    let mut grant = false;
    let mut constrain_pres = BTreeSet::default();
    let mut allow_pres = BTreeSet::default();
    let mut constrain_rem = BTreeSet::default();
    let mut allow_rem = BTreeSet::default();
    let mut constrain_cls = BTreeSet::default();
    let mut allow_cls = BTreeSet::default();

    // Some useful references.
    //  - needed for checking entry manager conditions.
    let ident_memberof = ident.get_memberof();
    let ident_uuid = ident.get_uuid();

    // run each module. These have to be broken down further due to modify
    // kind of being three operations all in one.

    match modify_ident_test(ident) {
        AccessResult::Denied => denied = true,
        AccessResult::Grant => grant = true,
        AccessResult::Ignore => {}
        AccessResult::Constrain(mut set) => constrain_pres.append(&mut set),
        AccessResult::Allow(mut set) => allow_pres.append(&mut set),
    }

    if !grant && !denied {
        // Check with protected if we should proceed.

        // If it's a sync entry, constrain it.
        match modify_sync_constrain(ident, entry, sync_agreements) {
            AccessResult::Denied => denied = true,
            AccessResult::Constrain(mut set) => {
                constrain_rem.extend(set.iter().cloned());
                constrain_pres.append(&mut set)
            }
            // Can't grant.
            AccessResult::Grant |
            // Can't allow
            AccessResult::Allow(_) |
            AccessResult::Ignore => {}
        }

        // Setup the acp's here
        let scoped_acp: Vec<&AccessControlModify> = related_acp
            .iter()
            .filter_map(|acm| {
                match &acm.receiver_condition {
                    AccessControlReceiverCondition::GroupChecked => {
                        // The groups were already checked during filter resolution. Trust
                        // that result, and continue.
                    }
                    AccessControlReceiverCondition::EntryManager => {
                        // This condition relies on the entry we are looking at to have a back-ref
                        // to our uuid or a group we are in as an entry manager.

                        // Note, while schema has this as single value, we currently
                        // fetch it as a multivalue btreeset for future incase we allow
                        // multiple entry manager by in future.
                        if let Some(entry_manager_uuids) =
                            entry.get_ava_refer(Attribute::EntryManagedBy)
                        {
                            let group_check = ident_memberof
                                // Have at least one group allowed.
                                .map(|imo| imo.intersection(entry_manager_uuids).next().is_some())
                                .unwrap_or_default();

                            let user_check = ident_uuid
                                .map(|u| entry_manager_uuids.contains(&u))
                                .unwrap_or_default();

                            if !(group_check || user_check) {
                                // Not the entry manager
                                return None;
                            }
                        } else {
                            // Can not satisfy.
                            return None;
                        }
                    }
                };

                match &acm.target_condition {
                    AccessControlTargetCondition::Scope(f_res) => {
                        if !entry.entry_match_no_index(f_res) {
                            debug!(entry = ?entry.get_display_id(), acm = %acm.acp.acp.name, "entry DOES NOT match acs");
                            return None;
                        }
                    }
                };

                debug!(entry = ?entry.get_display_id(), acs = %acm.acp.acp.name, "acs applied to entry");

                Some(acm.acp)
            })
            .collect();

        match modify_pres_test(scoped_acp.as_slice()) {
            AccessResult::Denied => denied = true,
            // Can never return a unilateral grant.
            AccessResult::Grant => {}
            AccessResult::Ignore => {}
            AccessResult::Constrain(mut set) => constrain_pres.append(&mut set),
            AccessResult::Allow(mut set) => allow_pres.append(&mut set),
        }

        match modify_rem_test(scoped_acp.as_slice()) {
            AccessResult::Denied => denied = true,
            // Can never return a unilateral grant.
            AccessResult::Grant => {}
            AccessResult::Ignore => {}
            AccessResult::Constrain(mut set) => constrain_rem.append(&mut set),
            AccessResult::Allow(mut set) => allow_rem.append(&mut set),
        }

        match modify_cls_test(scoped_acp.as_slice()) {
            AccessResultClass::Denied => denied = true,
            // Can never return a unilateral grant.
            AccessResultClass::Grant => {}
            AccessResultClass::Ignore => {}
            AccessResultClass::Constrain(mut set) => constrain_cls.append(&mut set),
            AccessResultClass::Allow(mut set) => allow_cls.append(&mut set),
        }
    }

    if denied {
        ModifyResult::Denied
    } else if grant {
        ModifyResult::Grant
    } else {
        let allowed_pres = if !constrain_pres.is_empty() {
            // bit_and
            &constrain_pres & &allow_pres
        } else {
            allow_pres
        };

        let allowed_rem = if !constrain_rem.is_empty() {
            // bit_and
            &constrain_rem & &allow_rem
        } else {
            allow_rem
        };

        let allowed_cls = if !constrain_cls.is_empty() {
            // bit_and
            &constrain_cls & &allow_cls
        } else {
            allow_cls
        };

        ModifyResult::Allow {
            pres: allowed_pres,
            rem: allowed_rem,
            cls: allowed_cls,
        }
    }
}

fn modify_ident_test(ident: &Identity) -> AccessResult {
    match &ident.origin {
        IdentType::Internal => {
            trace!("Internal operation, bypassing access check");
            // No need to check ACS
            return AccessResult::Grant;
        }
        IdentType::Synch(_) => {
            security_critical!("Blocking sync check");
            return AccessResult::Denied;
        }
        IdentType::User(_) => {}
    };
    debug!(event = %ident, "Access check for modify event");

    match ident.access_scope() {
        AccessScope::ReadOnly | AccessScope::Synchronise => {
            security_access!("denied ❌ - identity access scope is not permitted to modify");
            return AccessResult::Denied;
        }
        AccessScope::ReadWrite => {
            // As you were
        }
    };

    AccessResult::Ignore
}

fn modify_pres_test(scoped_acp: &[&AccessControlModify]) -> AccessResult {
    let allowed_pres: BTreeSet<Attribute> = scoped_acp
        .iter()
        .flat_map(|acp| acp.presattrs.iter().cloned())
        .collect();
    AccessResult::Allow(allowed_pres)
}

fn modify_rem_test(scoped_acp: &[&AccessControlModify]) -> AccessResult {
    let allowed_rem: BTreeSet<Attribute> = scoped_acp
        .iter()
        .flat_map(|acp| acp.remattrs.iter().cloned())
        .collect();
    AccessResult::Allow(allowed_rem)
}

// TODO: Should this be reverted to the Str borrow method? Or do we try to change
// to EntryClass?
fn modify_cls_test<'a>(scoped_acp: &[&'a AccessControlModify]) -> AccessResultClass<'a> {
    let allowed_classes: BTreeSet<&'a str> = scoped_acp
        .iter()
        .flat_map(|acp| acp.classes.iter().map(|s| s.as_str()))
        .collect();
    AccessResultClass::Allow(allowed_classes)
}

fn modify_sync_constrain(
    ident: &Identity,
    entry: &Arc<EntrySealedCommitted>,
    sync_agreements: &HashMap<Uuid, BTreeSet<Attribute>>,
) -> AccessResult {
    match &ident.origin {
        IdentType::Internal => AccessResult::Ignore,
        IdentType::Synch(_) => {
            // Allowed to mod sync objects. Later we'll probably need to check the limits of what
            // it can do if we go that way.
            AccessResult::Ignore
        }
        IdentType::User(_) => {
            // We need to meet these conditions.
            // * We are a sync object
            // * We have a sync_parent_uuid
            let is_sync = entry
                .get_ava_set(Attribute::Class)
                .map(|classes| classes.contains(&EntryClass::SyncObject.into()))
                .unwrap_or(false);

            if !is_sync {
                return AccessResult::Ignore;
            }

            if let Some(sync_uuid) = entry.get_ava_single_refer(Attribute::SyncParentUuid) {
                let mut set = btreeset![
                    Attribute::UserAuthTokenSession,
                    Attribute::OAuth2Session,
                    Attribute::OAuth2ConsentScopeMap,
                    Attribute::CredentialUpdateIntentToken
                ];

                if let Some(sync_yield_authority) = sync_agreements.get(&sync_uuid) {
                    set.extend(sync_yield_authority.iter().cloned())
                }

                AccessResult::Constrain(set)
            } else {
                warn!(entry = ?entry.get_uuid(), "sync_parent_uuid not found on sync object, preventing all access");
                AccessResult::Denied
            }
        }
    }
}
