use std::{
    collections::{HashMap, HashSet},
    hash::Hash,
    time::Duration,
};

use defguard_common::db::models::{Settings, User};
use ldap3::{
    LdapConnAsync, LdapConnSettings, Mod, Scope, SearchEntry,
    adapters::{Adapter, EntriesOnly, PagedResults},
    drive, ldap_escape,
};

use super::{LDAPConfig, LDAPConnection, error::LdapError};
use crate::enterprise::ldap::model::{extract_rdn_value, is_search_entry};

const STREAMING_PAGE_SIZE: i32 = 500;

impl LDAPConnection {
    pub async fn create() -> Result<Self, LdapError> {
        let settings = Settings::get_current_settings();
        let config = LDAPConfig::try_from(settings.clone())?;
        let url = settings.ldap_url.ok_or(LdapError::MissingSettings(
            "LDAP URL is required for LDAP configuration to work".to_string(),
        ))?;
        let password = settings
            .ldap_bind_password
            .ok_or(LdapError::MissingSettings("LDAP bind password".to_string()))?;
        let conn_settings = LdapConnSettings::new()
            .set_starttls(settings.ldap_use_starttls)
            .set_no_tls_verify(!settings.ldap_tls_verify_cert)
            .set_conn_timeout(Duration::from_secs(8));
        let (conn, mut ldap) = LdapConnAsync::with_settings(conn_settings, &url).await?;
        drive!(conn);
        info!("Connected to LDAP: {url}");
        ldap.simple_bind(&config.ldap_bind_username, password.expose_secret())
            .await?
            .success()?;

        Ok(Self { config, url, ldap })
    }

    /// Searches LDAP for users.
    pub(super) async fn search_users(
        &mut self,
        filter: &str,
    ) -> Result<Vec<SearchEntry>, LdapError> {
        debug!("Performing LDAP user search with filter: {filter}");
        let (entries, result) = self
            .ldap
            .search(
                &self.config.ldap_user_search_base,
                Scope::Subtree,
                filter,
                &["*", &self.config.ldap_member_attr],
            )
            .await?
            .success()?;
        debug!("LDAP user search result: {result:?}");
        debug!("Found users: {entries:?}");

        Ok(entries
            .into_iter()
            .filter(is_search_entry)
            .map(SearchEntry::construct)
            .collect())
    }

    pub(crate) async fn get(&mut self, dn: &str) -> Result<Option<SearchEntry>, LdapError> {
        debug!("Searching for LDAP object with DN {dn}");
        let search_result = self
            .ldap
            .search(dn, Scope::Base, "(objectClass=*)", &["*"])
            .await;

        match search_result {
            Ok(ldap_result) => match ldap_result.success() {
                Ok((mut entries, result)) => {
                    debug!("LDAP search result: {result:?}");
                    entries.retain(is_search_entry);
                    if let Some(entry) = entries.pop() {
                        debug!("Found LDAP object with DN {dn}: {entry:?}");
                        Ok(Some(SearchEntry::construct(entry)))
                    } else {
                        debug!("No LDAP object found with DN {dn}");
                        Ok(None)
                    }
                }
                // LDAP returns Error if no entries found, so we should handle it gracefully
                Err(err) => {
                    debug!("LDAP search failed for DN {dn}: {err:?}");
                    Ok(None)
                }
            },
            Err(err) => {
                debug!("LDAP connection/search error for DN {dn}: {err:?}");
                Err(LdapError::from(err))
            }
        }
    }

    pub(super) async fn test_bind_user(&self, dn: &str, password: &str) -> Result<(), LdapError> {
        debug!("Testing LDAP bind for user {dn}");
        let settings = Settings::get_current_settings();
        let conn_settings = LdapConnSettings::new()
            .set_starttls(settings.ldap_use_starttls)
            .set_no_tls_verify(!settings.ldap_tls_verify_cert)
            .set_conn_timeout(Duration::from_secs(8));
        let (conn, mut ldap) = LdapConnAsync::with_settings(conn_settings, &self.url).await?;
        drive!(conn);
        let res = ldap.simple_bind(dn, password).await?.success()?;
        debug!("LDAP user bind test result: {res}");
        ldap.unbind().await?;
        info!("LDAP bind test for user {dn} successful");
        Ok(())
    }

    /// Returns groups that the given user is a member of.
    pub async fn get_user_groups(&mut self, user_dn: &str) -> Result<Vec<String>, LdapError> {
        let user_dn_escaped = ldap_escape(user_dn);
        let filter = format!("({}={user_dn_escaped})", self.config.ldap_group_member_attr);
        debug!("Performing LDAP group search with filter: {filter}");
        let (entries, result) = self
            .ldap
            .search(
                &self.config.ldap_group_search_base,
                Scope::Subtree,
                &filter,
                &[&self.config.ldap_groupname_attr],
            )
            .await?
            .success()?;
        debug!("LDAP user groups search result: {result}");
        debug!("Found groups: {entries:?}");

        let mut groups = Vec::new();
        for entry in entries.into_iter().filter(is_search_entry) {
            let se = SearchEntry::construct(entry);
            for (key, mut values) in se.attrs {
                if key.eq_ignore_ascii_case(&self.config.ldap_groupname_attr) {
                    groups.append(&mut values);
                }
            }
        }

        Ok(groups)
    }

    /// Searches LDAP for groups.
    pub(super) async fn search_groups(
        &mut self,
        filter: &str,
    ) -> Result<Vec<SearchEntry>, LdapError> {
        let (rs, res) = self
            .ldap
            .search(
                &self.config.ldap_group_search_base,
                Scope::Subtree,
                filter,
                &[
                    &self.config.ldap_username_attr,
                    &self.config.ldap_group_member_attr,
                ],
            )
            .await?
            .success()?;
        debug!("LDAP group search result: {res}");
        info!("Performed LDAP group search with filter = {filter}");
        Ok(rs
            .into_iter()
            .filter(is_search_entry)
            .map(SearchEntry::construct)
            .collect())
    }

    /// Creates LDAP object with specified distinguished name and attributes.
    pub(super) async fn add(
        &mut self,
        dn: &str,
        attrs: Vec<(&str, HashSet<&str>)>,
    ) -> Result<(), LdapError> {
        debug!("Adding object {dn}");
        let result = self.ldap.add(dn, attrs).await?.success()?;
        debug!("LDAP add result: {result:?}");
        info!("Added object {dn}");

        Ok(())
    }

    /// Updates LDAP object with specified distinguished name and attributes.
    pub(super) async fn modify<S>(
        &mut self,
        old_dn: &str,
        new_dn: &str,
        mods: Vec<Mod<S>>,
    ) -> Result<(), LdapError>
    where
        S: AsRef<[u8]> + Eq + Hash,
    {
        self.ldap.modify(old_dn, mods).await?;
        if old_dn != new_dn {
            if let Some((new_rdn, _rest)) = new_dn.split_once(',') {
                self.ldap.modifydn(old_dn, new_rdn, true, None).await?;
            } else {
                warn!("Failed to rename LDAP object {old_dn} to {new_dn}, new DN is invalid");
            }
        }
        info!("Modified LDAP object {old_dn}");

        Ok(())
    }

    /// Deletes LDAP object with specified distinguished name.
    pub(super) async fn delete(&mut self, dn: &str) -> Result<(), LdapError> {
        debug!("Deleting LDAP object {dn}");
        let result = self.ldap.delete(dn).await?;
        debug!("LDAP deletion result: {result:?}");
        info!("Deleted LDAP object {dn}");

        Ok(())
    }

    /// Returns a map of group names to a set of members
    pub(super) async fn get_ldap_group_memberships<'a>(
        &mut self,
        all_ldap_users: &'a [User],
    ) -> Result<HashMap<String, HashSet<&'a User>>, LdapError> {
        debug!("Retrieving LDAP group memberships");
        let mut membership_entries = self.list_group_memberships().await?;
        let mut memberships = HashMap::new();
        // dn: user map
        let dn_map = all_ldap_users
            .iter()
            .map(|u| (self.config.user_dn_for_user(u).to_lowercase(), u))
            .collect::<HashMap<_, _>>();

        for entry in &mut membership_entries {
            let groupname = entry
                .attrs
                .remove(&self.config.ldap_groupname_attr)
                .and_then(|mut v| v.pop());

            if let Some(groupname) = groupname {
                if let Some(members) = entry.attrs.get(&self.config.ldap_group_member_attr) {
                    let members = members
                        .iter()
                        .filter_map(|v| {
                            if let Some(user) = dn_map.get(v.to_lowercase().as_str()) {
                                Some(*user)
                            } else {
                                debug!(
                                    "LDAP group {groupname} contains member {v} that does not \
                                    belong to the filtered LDAP users list; skipping"
                                );
                                None
                            }
                        })
                        .collect::<HashSet<_>>();
                    // Union rather than overwrite: under a broad group search base several LDAP
                    // groups can share one name across different subtrees (e.g. a per-site group),
                    // and they all map onto the single Defguard group of that name.
                    memberships
                        .entry(groupname)
                        .or_insert_with(HashSet::new)
                        .extend(members);
                } else {
                    warn!("LDAP group {groupname} missing group member attribute, skipping");
                }
            } else {
                warn!("Group entry {entry:?} missing groupname attribute, skipping");
            }
        }

        Ok(memberships)
    }

    pub(super) async fn is_member_of(
        &mut self,
        user_dn: &str,
        groupname: &str,
    ) -> Result<bool, LdapError> {
        debug!("Checking if user {user_dn} is member of group {groupname}");
        let user_dn_escaped = ldap_escape(user_dn);
        let groupname_escaped = ldap_escape(groupname);
        let filter = format!(
            "(&(objectClass={})({}={groupname_escaped})({}={user_dn_escaped}))",
            self.config.ldap_group_obj_class,
            self.config.ldap_groupname_attr,
            self.config.ldap_group_member_attr,
        );
        debug!(
            "Using the following filter for group search: {filter} and base: {}",
            self.config.ldap_group_search_base
        );
        let (entries, result) = self
            .ldap
            .search(
                &self.config.ldap_group_search_base,
                Scope::Subtree,
                &filter,
                &["*"],
            )
            .await?
            .success()?;
        debug!("LDAP group membership search result: {result:?}");
        Ok(entries.iter().any(is_search_entry))
    }

    pub(super) async fn get_group_members(
        &mut self,
        groupname: &str,
    ) -> Result<Vec<String>, LdapError> {
        debug!("Searching for group memberships for group {groupname}");
        let groupname_escaped = ldap_escape(groupname);
        let filter = format!(
            "(&(objectClass={})({}={}))",
            self.config.ldap_group_obj_class, self.config.ldap_groupname_attr, groupname_escaped
        );
        debug!(
            "Using the following filter for group search: {filter} and base: {}",
            self.config.ldap_group_search_base
        );
        let attrs = [&self.config.ldap_group_member_attr];
        let adapters: Vec<Box<dyn Adapter<_, _>>> = vec![
            Box::new(EntriesOnly::new()),
            Box::new(PagedResults::new(STREAMING_PAGE_SIZE)),
        ];
        let mut search_stream = self
            .ldap
            .streaming_search_with(
                adapters,
                &self.config.ldap_group_search_base,
                Scope::Subtree,
                &filter,
                &attrs,
            )
            .await?;

        let mut member_entries = Vec::new();
        while let Some(entry) = search_stream.next().await? {
            member_entries.push(SearchEntry::construct(entry));
        }

        let members = member_entries
            .first()
            .and_then(|entry| {
                let member_entries = entry.attrs.get(&self.config.ldap_group_member_attr);
                member_entries.map(|v| {
                    v.iter()
                        .filter_map(|v| extract_rdn_value(v))
                        .collect::<Vec<_>>()
                })
            })
            .unwrap_or_default();
        debug!("Performed LDAP group memberships search for group {groupname}");

        Ok(members)
    }

    /// Resolves the actual DN(s) of a group by name within the group search base.
    ///
    /// `LDAPConfig::group_dn` builds a DN by concatenation, which only matches when the
    /// group sits directly under the group search base. AD stores each user's `memberOf`
    /// as the group's full DN, so when the base is broad (e.g. the domain root) and the
    /// group is nested deeper, the constructed DN matches nothing and every user is
    /// silently filtered out. Resolving the real DN by search keeps the filter correct
    /// regardless of where the group lives beneath the base.
    async fn resolve_group_dns(&mut self, groupname: &str) -> Result<Vec<String>, LdapError> {
        let groupname_escaped = ldap_escape(groupname);
        let filter = format!(
            "(&(objectClass={})({}={}))",
            self.config.ldap_group_obj_class, self.config.ldap_groupname_attr, groupname_escaped
        );
        let entries = self.search_groups(&filter).await?;
        Ok(entries.into_iter().map(|entry| entry.dn).collect())
    }

    pub(super) async fn list_users(&mut self) -> Result<Vec<SearchEntry>, LdapError> {
        let filter = if self.config.ldap_sync_groups.is_empty() {
            debug!("No LDAP sync groups defined, searching for all users in the base DN");
            format!("(objectClass={})", self.config.ldap_user_obj_class)
        } else {
            debug!(
                "LDAP sync groups are defined, filtering users by those groups: {:?}",
                self.config.ldap_sync_groups
            );
            let sync_groups = self.config.ldap_sync_groups.clone();
            let mut group_filters = Vec::new();
            for group in &sync_groups {
                let group_dns = self.resolve_group_dns(group).await?;
                if group_dns.is_empty() {
                    warn!(
                        "Sync group {group} not found under group search base {}; its \
                        members will not be synced",
                        self.config.ldap_group_search_base
                    );
                }
                for group_dn in group_dns {
                    let group_dn_escaped = ldap_escape(&group_dn);
                    group_filters.push(format!(
                        "({}={group_dn_escaped})",
                        self.config.ldap_member_attr
                    ));
                }
            }
            if group_filters.is_empty() {
                warn!(
                    "None of the configured sync groups were found under the group search \
                    base; no users will be synced"
                );
                return Ok(Vec::new());
            }
            debug!("Using the following group filters for user search: {group_filters:?}");
            format!(
                "(&(objectClass={})(|{}))",
                self.config.ldap_user_obj_class,
                group_filters.join("")
            )
        };

        debug!(
            "Using the following filter for user search: {filter} and base: {}",
            self.config.ldap_user_search_base
        );
        let attrs = ["*", &self.config.ldap_member_attr];
        let adapters: Vec<Box<dyn Adapter<_, _>>> = vec![
            Box::new(EntriesOnly::new()),
            Box::new(PagedResults::new(STREAMING_PAGE_SIZE)),
        ];
        let mut search_stream = self
            .ldap
            .streaming_search_with(
                adapters,
                &self.config.ldap_user_search_base,
                Scope::Subtree,
                &filter,
                &attrs,
            )
            .await?;

        let mut entries = Vec::new();
        while let Some(entry) = search_stream.next().await? {
            entries.push(SearchEntry::construct(entry));
        }

        debug!("Performed LDAP user search");

        Ok(entries)
    }

    pub(super) async fn list_group_memberships(&mut self) -> Result<Vec<SearchEntry>, LdapError> {
        debug!("Searching for group memberships");
        let filter = format!(
            "(&(objectClass={})({}=*))",
            self.config.ldap_group_obj_class, self.config.ldap_group_member_attr
        );
        debug!(
            "Using the following filter for group search: {filter} and base: {}",
            self.config.ldap_group_search_base
        );
        let attrs = [
            &self.config.ldap_groupname_attr,
            &self.config.ldap_group_member_attr,
        ];
        let adapters: Vec<Box<dyn Adapter<_, _>>> = vec![
            Box::new(EntriesOnly::new()),
            Box::new(PagedResults::new(STREAMING_PAGE_SIZE)),
        ];
        let mut search_stream = self
            .ldap
            .streaming_search_with(
                adapters,
                &self.config.ldap_group_search_base,
                Scope::Subtree,
                &filter,
                &attrs,
            )
            .await?;

        let mut memberships = Vec::new();
        while let Some(entry) = search_stream.next().await? {
            memberships.push(SearchEntry::construct(entry));
        }

        debug!("Performed LDAP group memberships search");

        Ok(memberships)
    }
}
