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

use super::profiles::{
    AccessControlReceiverCondition, AccessControlSearchResolved, AccessControlTargetCondition,
};
use super::AccessResult;
use std::sync::Arc;

pub(super) enum SearchResult {
    Denied,
    Grant,
    Allow(BTreeSet<Attribute>),
}

pub(super) fn apply_search_access(
    ident: &Identity,
    related_acp: &[AccessControlSearchResolved],
    entry: &Arc<EntrySealedCommitted>,
) -> SearchResult {
    // This could be considered "slow" due to allocs each iter with the entry. We
    // could move these out of the loop and reuse, but there are likely risks to
    // that.
    let mut denied = false;
    let mut grant = false;
    let mut constrain = BTreeSet::default();
    let mut allow = BTreeSet::default();

    // The access control profile
    match search_filter_entry(ident, related_acp, entry) {
        AccessResult::Denied => denied = true,
        AccessResult::Grant => grant = true,
        AccessResult::Ignore => {}
        AccessResult::Constrain(mut set) => constrain.append(&mut set),
        AccessResult::Allow(mut set) => allow.append(&mut set),
    };

    match search_oauth2_filter_entry(ident, entry) {
        AccessResult::Denied => denied = true,
        AccessResult::Grant => grant = true,
        AccessResult::Ignore => {}
        AccessResult::Constrain(mut set) => constrain.append(&mut set),
        AccessResult::Allow(mut set) => allow.append(&mut set),
    };

    match search_sync_account_filter_entry(ident, entry) {
        AccessResult::Denied => denied = true,
        AccessResult::Grant => grant = true,
        AccessResult::Ignore => {}
        AccessResult::Constrain(mut set) => constrain.append(&mut set),
        AccessResult::Allow(mut set) => allow.append(&mut set),
    };

    // We'll add more modules later.

    // Now finalise the decision.

    if denied {
        SearchResult::Denied
    } else if grant {
        SearchResult::Grant
    } else {
        let allowed_attrs = if !constrain.is_empty() {
            // bit_and
            &constrain & &allow
        } else {
            allow
        };
        SearchResult::Allow(allowed_attrs)
    }
}

fn search_filter_entry(
    ident: &Identity,
    related_acp: &[AccessControlSearchResolved],
    entry: &Arc<EntrySealedCommitted>,
) -> AccessResult {
    // If this is an internal search, return our working set.
    match &ident.origin {
        IdentType::Internal => {
            trace!(uuid = ?entry.get_display_id(), "Internal operation, bypassing access check");
            // No need to check ACS
            return AccessResult::Grant;
        }
        IdentType::Synch(_) => {
            security_debug!(uuid = ?entry.get_display_id(), "Blocking sync check");
            return AccessResult::Denied;
        }
        IdentType::User(_) => {}
    };
    debug!(event = %ident, "Access check for search (filter) event");

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

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

    let allowed_attrs: BTreeSet<Attribute> = related_acp
        .iter()
        .filter_map(|acs| {
            // Assert that the receiver condition applies.
            match &acs.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 &acs.target_condition {
                AccessControlTargetCondition::Scope(f_res) => {
                    if !entry.entry_match_no_index(f_res) {
                        security_debug!(entry = ?entry.get_display_id(), acs = %acs.acp.acp.name, "entry DOES NOT match acs");
                        return None
                    }
                }
            };

            // -- Conditions pass -- release the attributes.

            security_debug!(entry = ?entry.get_display_id(), acs = %acs.acp.acp.name, "acs applied to entry");
            // add search_attrs to allowed.
            Some(acs.acp.attrs.iter().cloned())
        })
        .flatten()
        .collect();

    AccessResult::Allow(allowed_attrs)
}

fn search_oauth2_filter_entry(ident: &Identity, entry: &Arc<EntrySealedCommitted>) -> AccessResult {
    match &ident.origin {
        IdentType::Internal | IdentType::Synch(_) => AccessResult::Ignore,
        IdentType::User(iuser) => {
            let contains_o2_rs = entry
                .get_ava_as_iutf8(Attribute::Class)
                .map(|set| {
                    trace!(?set);
                    set.contains(&EntryClass::OAuth2ResourceServer.to_string())
                })
                .unwrap_or(false);

            let contains_o2_scope_member = entry
                .get_ava_as_oauthscopemaps(Attribute::OAuth2RsScopeMap)
                .and_then(|maps| ident.get_memberof().map(|mo| (maps, mo)))
                .map(|(maps, mo)| maps.keys().any(|k| mo.contains(k)))
                .unwrap_or(false);

            if contains_o2_rs && contains_o2_scope_member {
                security_debug!(entry = ?entry.get_uuid(), ident = ?iuser.entry.get_uuid2rdn(), "ident is a memberof a group granted an oauth2 scope by this entry");

                return AccessResult::Allow(btreeset!(
                    Attribute::Class,
                    Attribute::DisplayName,
                    Attribute::Uuid,
                    Attribute::Name,
                    Attribute::OAuth2RsOriginLanding,
                    Attribute::Image
                ));
            }
            AccessResult::Ignore
        }
    }
}

fn search_sync_account_filter_entry(
    ident: &Identity,
    entry: &Arc<EntrySealedCommitted>,
) -> AccessResult {
    match &ident.origin {
        IdentType::Internal | IdentType::Synch(_) => AccessResult::Ignore,
        IdentType::User(iuser) => {
            // Is the user a synced object?
            let is_user_sync_account = iuser
                .entry
                .get_ava_as_iutf8(Attribute::Class)
                .map(|set| {
                    trace!(?set);
                    set.contains(&EntryClass::SyncObject.to_string())
                        && set.contains(EntryClass::Account.into())
                })
                .unwrap_or(false);

            if is_user_sync_account {
                let is_target_sync_account = entry
                    .get_ava_as_iutf8(Attribute::Class)
                    .map(|set| {
                        trace!(?set);
                        set.contains(&EntryClass::SyncAccount.to_string())
                    })
                    .unwrap_or(false);

                if is_target_sync_account {
                    // Okay, now we need to check if the uuids line up.
                    let sync_uuid = entry.get_uuid();
                    let sync_source_match = iuser
                        .entry
                        .get_ava_single_refer(Attribute::SyncParentUuid)
                        .map(|sync_parent_uuid| sync_parent_uuid == sync_uuid)
                        .unwrap_or(false);

                    if sync_source_match {
                        // We finally got here!
                        security_debug!(entry = ?entry.get_uuid(), ident = ?iuser.entry.get_uuid2rdn(), "ident is a synchronised account from this sync account");

                        return AccessResult::Allow(btreeset!(
                            Attribute::Class,
                            Attribute::Uuid,
                            Attribute::SyncCredentialPortal
                        ));
                    }
                }
            }
            // Fall through
            AccessResult::Ignore
        }
    }
}
