//! This module contains utility functions for LDAP operations. Those operations are designed to be
//! used from outside of the module.

use std::collections::{HashMap, HashSet};

use defguard_common::db::{
    Id,
    models::{User, group::Group},
};
use sqlx::PgPool;
use tokio::sync::broadcast::Sender;

use super::{LDAPConnection, error::LdapError};
use crate::{
    enrollment_management::try_send_ldap_enrollment_invite,
    enterprise::{
        ldap::{model::ldap_sync_allowed_for_user, with_ldap_status},
        license::get_cached_license,
        limits::get_counts,
    },
    grpc::GatewayEvent,
};

fn reached_user_license_limit() -> Option<(u32, u32)> {
    let user_count = get_counts().user();
    let user_limit = get_cached_license()
        .as_ref()
        .and_then(|license| license.limits.as_ref().map(|limits| limits.users));
    user_limit
        .filter(|limit| user_count >= *limit)
        .map(|limit| (user_count, limit))
}

/// Retrieves a user from LDAP if they are in the configured LDAP sync groups.
///
/// Creates a new user in Defguard if they do not exist and marks them as coming from LDAP.
pub(crate) async fn login_through_ldap(
    pool: &PgPool,
    username: &str,
    password: &str,
) -> Result<User<Id>, LdapError> {
    debug!("Logging in user {username} through LDAP");
    let mut ldap_connection = LDAPConnection::create().await?;
    login_through_ldap_with_connection(pool, &mut ldap_connection, username, password).await
}

pub(crate) async fn login_through_ldap_with_connection(
    pool: &PgPool,
    ldap_connection: &mut LDAPConnection,
    username: &str,
    password: &str,
) -> Result<User<Id>, LdapError> {
    debug!("Logging in user {username} through LDAP");
    let mut transaction = pool.begin().await?;

    let mut ldap_user = ldap_connection
        .get_user_by_credentials(username, password)
        .await?;
    if !ldap_connection.user_in_ldap_sync_groups(&ldap_user).await? {
        info!("User {username} is not in LDAP sync groups, not allowing to login through LDAP.");
        return Err(LdapError::UserNotInLDAPSyncGroups(
            username.to_string(),
            "LDAP",
        ));
    }
    debug!("User {ldap_user} logged in through LDAP");
    // The user is logging in through LDAP, so we can infer that there are no other login options
    // (Defguard password), so we should mark them as from_ldap.
    let (mut user, is_new_user) = if let Some(mut defguard_user) =
        User::find_by_username(&mut *transaction, &ldap_user.username).await?
    {
        debug!(
            "User {defguard_user} already exists in Defguard, marking them as coming from LDAP and \
            proceeding with login"
        );
        defguard_user.from_ldap = true;
        defguard_user.save(&mut *transaction).await?;
        (defguard_user, false)
    } else {
        debug!(
            "User {ldap_user} doesn't exist in Defguard, creating them first based on LDAP data"
        );
        if let Some((user_count, limit)) = reached_user_license_limit() {
            error!(
                "Skipping LDAP account creation for user {} (email: {}) because license user \
                limit has been reached ({user_count}/{limit})",
                ldap_user.username, ldap_user.email
            );
            // TODO: send email to admins
            return Err(LdapError::LicenseUserLimitReached(user_count, limit));
        }

        ldap_user.from_ldap = true;
        (ldap_user.save(&mut *transaction).await?, true)
    };

    transaction.commit().await?;

    // Attempt to send enrollment invite after the original DB transaction is committed,
    // so that the user row is visible to the new transaction inside try_send_ldap_enrollment_invite.
    // Only send for newly-created users - returning users must not receive a second invite.
    if is_new_user {
        let mut transaction = pool.begin().await?;
        try_send_ldap_enrollment_invite(&mut user, &mut transaction).await;
        transaction.commit().await?;
    }

    Ok(user)
}

/// Convenience wrapper around [`ldap_update_users_state`] to update a single user.
pub async fn ldap_update_user_state(
    user: &mut User<Id>,
    pool: &PgPool,
    wg_tx: &Sender<GatewayEvent>,
) {
    let vec = vec![user];
    Box::pin(ldap_update_users_state(vec, pool, wg_tx)).await;
}

/// See the [`LDAPConnection::update_users_state`] function for details.
pub(crate) async fn ldap_update_users_state(
    users: Vec<&mut User<Id>>,
    pool: &PgPool,
    wg_tx: &Sender<GatewayEvent>,
) {
    let _ = Box::pin(with_ldap_status(pool, async {
        debug!("Updating users state in LDAP");
        let mut ldap_connection = LDAPConnection::create().await?;
        ldap_connection
            .update_users_state(users, pool, wg_tx)
            .await?;
        Ok(())
    }))
    .await;
}

/// Adds user to LDAP, if no password was specified, a temporary random password will be used.
/// This will set the `ldap_pass_randomized` field to `true` in the user.
///
/// If the user already exists, the creation will be skipped.
pub async fn ldap_add_user(user: &mut User<Id>, password: Option<&str>, pool: &PgPool) {
    let _: Result<(), LdapError> = with_ldap_status(pool, async {
        debug!("Creating user {user} in LDAP");
        if !ldap_sync_allowed_for_user(user, pool).await? {
            debug!(
                "User {user} is not allowed to be synced to LDAP as he is not in the specified \
                sync groups, skipping"
            );
            return Ok(());
        }
        let mut ldap_connection = LDAPConnection::create().await?;
        // convert to ldap module wrapper
        if ldap_connection.user_exists(user).await? {
            debug!("User {user} already exists in LDAP, skipping creation");
            return Ok(());
        }
        match ldap_connection.add_user(user, password, pool).await {
            Ok(()) => Ok(()),
            // This user might exist in LDAP, just try to set the password.
            Err(err) => {
                warn!("There was an error while trying to create the user {user} in LDAP: {err}");
                debug!(
                    "Trying to set password for user {user} in LDAP, in case he already existed",
                );
                if let Some(password) = password {
                    ldap_connection.set_password(user, password).await?;
                    debug!("Password set for user {user} in LDAP");
                } else {
                    debug!(
                        "No password provided, skipping password setting for user {user} in LDAP"
                    );
                }
                Ok(())
            }
        }
    })
    .await;
}

/// Applies user modifications to LDAP. May update the user object if
/// his RDN in Defguard needs updating. Fails and sets the sync status to desynced
/// if the user does not exist in LDAP despite updating his state.
///
/// Warning: This function does not check if the user is allowed to be synced to LDAP. You must do
/// that manually before calling this function. For example, by calling the
/// [`User::ldap_sync_allowed`] method on the user.
pub(crate) async fn ldap_handle_user_modify(
    old_username: &str,
    current_user: &mut User<Id>,
    pool: &PgPool,
    wg_tx: &Sender<GatewayEvent>,
) {
    let _: Result<(), LdapError> = Box::pin(with_ldap_status(pool, async {
        debug!("Handling user modify for {old_username} in LDAP");

        let mut ldap_connection = LDAPConnection::create().await?;
        if ldap_connection.user_exists(current_user).await? {
            debug!("User {current_user} exists in LDAP, modifying it");
        } else {
            debug!(
                "User {current_user} doesn't exist in LDAP, updating his state first as it may be \
                stale"
            );
            ldap_connection
                .update_users_state(vec![current_user], pool, wg_tx)
                .await?;
        }
        ldap_connection
            .modify_user(old_username, current_user)
            .await
    }))
    .await;
}

/// Deletes single user from LDAP. Convenience wrapper around [`ldap_delete_users`].
///
/// WARNING: You must check whether the user should be deleted from LDAP or not before calling this
/// function.
/// For example, by calling the [`User::ldap_sync_allowed`] method on the user.
//
// The mentioned method can't be called here since the user is already dropped from the database
pub(crate) async fn ldap_delete_user<I>(user: &User<I>, pool: &PgPool) {
    ldap_delete_users(vec![user], pool).await;
}

/// Deletes multiple users from LDAP.
///
/// WARNING: You must check whether the users should be deleted from LDAP or not before calling
/// this function.
/// For example, by calling the [`User::ldap_sync_allowed`] method on each user.
//
// The mentioned method can't be called here since the user is already dropped from the database
pub(crate) async fn ldap_delete_users<I>(users: Vec<&User<I>>, pool: &PgPool) {
    let _: Result<(), LdapError> = with_ldap_status(pool, async {
        debug!("Deleting {:?} users from LDAP", users.len());
        let mut ldap_connection = LDAPConnection::create().await?;
        for user in users {
            debug!("Deleting user {user} from LDAP");
            ldap_connection.delete_user(user).await?;
            debug!("User {user} deleted from LDAP");
        }

        Ok(())
    })
    .await;
}

/// Remove singular user from multiple groups in ldap.
pub(crate) async fn ldap_remove_user_from_groups(
    user: &User<Id>,
    groups: HashSet<&str>,
    pool: &PgPool,
) {
    let map = HashMap::from([(user, groups)]);
    ldap_remove_users_from_groups(map, pool).await;
}

/// Add singular user to multiple groups in LDAP. Convenience wrapper around
/// [`ldap_add_users_to_groups`].
pub(crate) async fn ldap_add_user_to_groups(user: &User<Id>, groups: HashSet<&str>, pool: &PgPool) {
    let map = HashMap::from([(user, groups)]);
    ldap_add_users_to_groups(map, pool).await;
}

/// Bulk add users to groups in ldap.
pub(crate) async fn ldap_add_users_to_groups(
    user_groups: HashMap<&User<Id>, HashSet<&str>>,
    pool: &PgPool,
) {
    let _: Result<(), LdapError> = with_ldap_status(pool, async {
        let mut ldap_connection = LDAPConnection::create().await?;
        let sync_groups = ldap_connection.config.ldap_sync_groups.clone();
        let sync_groups_lookup = sync_groups
            .iter()
            .map(String::as_str)
            .collect::<HashSet<_>>();

        for (user, groups) in user_groups {
            let adding_to_sync_groups = groups
                .iter()
                .any(|group| sync_groups_lookup.contains(*group));
            if !ldap_sync_allowed_for_user(user, pool).await? && !adding_to_sync_groups {
                debug!(
                    "User {user} is not allowed to be synced to LDAP as he is not in the \
                    specified sync groups, skipping"
                );
                continue;
            }

            for group in groups {
                ldap_connection.add_user_to_group(user, group).await?;
            }
        }

        Ok(())
    })
    .await;
}

/// Bulk remove users from groups in LDAP.
pub(crate) async fn ldap_remove_users_from_groups(
    user_groups: HashMap<&User<Id>, HashSet<&str>>,
    pool: &PgPool,
) {
    let _: Result<(), LdapError> = with_ldap_status(pool, async {
        let mut ldap_connection = LDAPConnection::create().await?;
        let sync_groups = ldap_connection.config.ldap_sync_groups.clone();
        let sync_groups_lookup = sync_groups
            .iter()
            .map(String::as_str)
            .collect::<HashSet<_>>();

        for (user, groups) in user_groups {
            let removing_from_sync_groups = groups
                .iter()
                .any(|group| sync_groups_lookup.contains(*group));
            if !ldap_sync_allowed_for_user(user, pool).await? && !removing_from_sync_groups {
                debug!(
                    "User {user} is not allowed to be synced to LDAP as he is not in the
                    specified sync groups, skipping"
                );
                continue;
            }
            for group in groups {
                if ldap_connection.group_exists(group).await? {
                    ldap_connection.remove_user_from_group(user, group).await?;
                } else {
                    debug!("Group {group} doesn't exist in LDAP, skipping removal of user {user}");
                }
            }
        }

        Ok(())
    })
    .await;
}

pub async fn ldap_change_password(user: &mut User<Id>, password: &str, pool: &PgPool) {
    let _: Result<(), LdapError> = with_ldap_status(pool, async {
        debug!("Changing password for user {user} in LDAP");
        if !ldap_sync_allowed_for_user(user, pool).await? {
            debug!(
                "User {user} is not allowed to be synced to LDAP as he is not in the specified
                sync groups, skipping"
            );
            return Ok(());
        }
        let mut ldap_connection = LDAPConnection::create().await?;
        if ldap_connection.user_exists(user).await? {
            debug!("User {user} exists in LDAP, changing password");
            ldap_connection.set_password(user, password).await?;
            debug!(
                "Password changed for user {user} in LDAP, marking the LDAP password as set in
                Defguard"
            );
            user.ldap_pass_randomized = false;
            user.save(pool).await?;
            debug!("LDAP password state updated in Defguard for user {user}");
        } else {
            debug!("User {user} doesn't exist in LDAP, creating it with the provided password");
            let user_groups = user.member_of_names(pool).await?;
            ldap_connection.add_user(user, Some(password), pool).await?;
            for group in user_groups {
                ldap_connection.add_user_to_group(user, &group).await?;
            }
            debug!("User {user} created in LDAP with the provided password");
        }

        Ok(())
    })
    .await;
}

pub(crate) async fn ldap_modify_group(groupname: &str, group: &Group<Id>, pool: &PgPool) {
    let _: Result<(), LdapError> = with_ldap_status(pool, async {
        debug!("Modifying group {groupname} in LDAP");
        let mut ldap_connection = LDAPConnection::create().await?;
        ldap_connection.modify_group(groupname, group).await
    })
    .await;
}

pub(crate) async fn ldap_delete_group(groupname: &str, pool: &PgPool) {
    let _: Result<(), LdapError> = with_ldap_status(pool, async {
        debug!("Deleting group {groupname} from LDAP");
        let mut ldap_connection = LDAPConnection::create().await?;
        ldap_connection.delete_group(groupname).await
    })
    .await;
}
