using adas_core.Authentication.Interfaces;
using adas_core.Domain.Enums;
using adas_core.Domain.Exceptions;
using adas_core.Domain.Models.MongoModels;
using adas_core.LdapLogin.Configuration;
using FluentValidation;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using Novell.Directory.Ldap;

namespace adas_core.LdapLogin;

/// <summary>
/// Implementation of ILoginService that authenticates users against an LDAP server. 
/// It retrieves user information and authorities based on the LDAP entry and maps them to the application's user model. 
/// The service also handles the creation of new users in the application if they do not already exist, based on the LDAP information. 
/// It uses configuration settings for connecting to the LDAP server and for mapping LDAP attributes to user properties and authorities.
/// </summary>
public class LdapLoginService : ILoginService
{
    /// <summary>
    /// The IAuthorityService is used to manage user authorities in the application.
    /// </summary>
    private readonly IAuthorityService _authorityService;

    /// <summary>
    /// The LdapConfig contains the necessary configuration for connecting to the LDAP server, such as server address, port, search base, and attribute mappings.
    /// </summary>
    private readonly LdapConfig _ldapConfig;

    /// <summary>
    /// The ILogger is used for logging information, warnings, and errors related to LDAP login operations.
    /// </summary>
    private readonly ILogger<LdapLoginService> _logger;

    /// <summary>
    /// The IUserService is used to manage user information in the application, such as retrieving existing users or creating new users based on LDAP information.
    /// </summary>
    private readonly Lazy<IUserService> _userService;


    /// <summary>
    /// Constructor for LdapLoginService. It initializes the service with the necessary dependencies and validates the LDAP configuration.
    /// </summary>
    /// <param name="ldapConfig">The LDAP configuration options.</param>
    /// <param name="validator">The validator for the LDAP configuration.</param>
    /// <param name="userService">The user service for managing user information.</param>
    /// <param name="authorityService">The authority service for managing user authorities.</param>
    /// <param name="logger">The logger for logging LDAP login operations.</param>
    public LdapLoginService(
        IOptions<LdapConfig> ldapConfig,
        IValidator<LdapConfig> validator,
        Lazy<IUserService> userService,
        IAuthorityService authorityService,
        ILogger<LdapLoginService> logger)
    {
        _userService = userService;
        _authorityService = authorityService;
        _logger = logger;
        _ldapConfig = ldapConfig.Value;
        validator.Validate(_ldapConfig, options => options.ThrowOnFailures());
    }

    /// <summary>
    /// Indicates that this login service allows password authentication, as it connects to an LDAP server which typically requires a username and password for authentication.
    /// </summary>
    public bool AllowPassword => true;

    /// <summary>
    /// Indicates that this login service does not allow token-based authentication, as it is designed to authenticate users against an LDAP server using their credentials rather than tokens.
    /// </summary>
    public UserEnum.LoginMethod Method => UserEnum.LoginMethod.Ldap;

    /// <summary>
    /// Authenticates a user against the LDAP server using the provided username and password.
    /// </summary>
    /// <param name="username">The username of the user to authenticate.</param>
    /// <param name="password">The password of the user to authenticate.</param>
    /// <returns>The authenticated user.</returns>
    /// <exception cref="LoginServicesException">Thrown when there is an error during the login process.</exception>
    /// <exception cref="UserNotFoundException">Thrown when the user is not found in the LDAP directory.</exception>
    public async Task<User> Login(string username, string password)
    {
        // Check for LDAP config
        if (_ldapConfig.Server == null) throw new LoginServicesException("LDAP Config not found");
        var conn = new LdapConnection();
        try
        {
            await conn.ConnectAsync(_ldapConfig.Server, _ldapConfig.Port ?? 389);
        }
        catch (Exception e)
        {
            _logger.LogError("[LDAP] Error connecting to {server}, port {port}, Exception: {e}", _ldapConfig.Server,
                _ldapConfig.Port, e.Message);
            throw;
        }

        if (_ldapConfig.LdapUser != null)
        {
            _logger.LogInformation("[LDAP] _ldapConfig.LdapUser is enabled with {LdapUser}", _ldapConfig.LdapUser);
            await conn.BindAsync(_ldapConfig.LdapUser, _ldapConfig.LdapPassword);
        }
        else
        {
            var ldapUser = (!string.IsNullOrEmpty(_ldapConfig.UserDomainName)
                ? _ldapConfig.UserDomainName + @"\"
                : "") + username;

            try
            {
                await conn.BindAsync(ldapUser, password);
            }
            catch (LdapException e)
            {
                _logger.LogError("[LDAP] Error binding ldapUser: {LdapUser} and password", ldapUser);

                throw new UserNotFoundException(username, e);
            }
        }

        var results = await conn.SearchAsync(
            _ldapConfig.SearchBase,
            LdapConnection.ScopeSub,
            $"({_ldapConfig.UserNameProperty}={username})",
            null,
            false);


        LdapEntry? entry = null;

        while (await results.HasMoreAsync())
        {
            LdapEntry? current = null;

            try
            {
                current = await results.NextAsync();
            }
            catch (LdapException ex)
            {
                _logger.LogWarning("[LDAP] Skipping invalid entry: {error}", ex.Message);
                continue;
            }

            if (current != null)
            {
                entry = current;
                break;
            }
        }


        if (entry == null) throw new LoginServicesException("LDAP User not found");
        var userEntryLdap = GetUser(entry);
        var user = await GetOrCreateUser(userEntryLdap, entry);
        _logger.LogInformation("[LDAP] entry is {entry} and user {user}", entry, user);
        // user.Authorization.AddRange(GetAuthorities(entry));
        conn.Disconnect();
        return user ?? throw new LoginServicesException("LDAP User not found");
    }

    /// <summary>
    /// This method is not implemented in the LdapLoginService, as the login process is handled through the Login(string username, string password) method.
    /// </summary>
    /// <param name="context">The HTTP context of the request.</param>
    /// <returns>A task representing the asynchronous operation.</returns>
    /// <exception cref="LoginServicesException">Thrown when the method is not implemented.</exception>
    public Task<User> Login(HttpContext context)
    {
        throw new LoginServicesException("Not implemented");
    }

    /// <summary>
    /// This method is not implemented in the LdapLoginService, as the authentication process is handled through the Login(string username, string password) method.
    /// </summary>
    /// <param name="username">The username of the user to authenticate.</param>
    /// <param name="password">The password of the user to authenticate.</param>
    /// <returns>A task representing the asynchronous operation.</returns>
    /// <exception cref="LoginServicesException">Thrown when the method is not implemented.</exception>
    public Task<User> Authenticate(string username, string password)
    {
        throw new LoginServicesException("Not implemented");
    }

    /// <summary>
    /// This method is not implemented in the LdapLoginService, as the user retrieval process is handled through the Login(string username, string password) method and the GetOrCreateUser(User userEntryLdap, LdapEntry entry) method.
    /// </summary>
    /// <param name="id">The ID of the user to retrieve.</param>
    /// <returns>A task representing the asynchronous operation.</returns>
    /// <exception cref="LoginServicesException">Thrown when the method is not implemented.</exception>
    public Task<User?> GetById(ObjectId id)
    {
        throw new LoginServicesException("Not implemented");
    }

    /// <summary>
    /// This method is not implemented in the LdapLoginService, as the user retrieval process is handled through the Login(string username, string password) method and the GetOrCreateUser(User userEntryLdap, LdapEntry entry) method.
    /// </summary>
    /// <param name="email">The email of the user to retrieve.</param>
    /// <returns>A task representing the asynchronous operation.</returns>
    /// <exception cref="LoginServicesException">Thrown when the method is not implemented.</exception>
    public Task<User?> GetByEmail(string email)
    {
        throw new LoginServicesException("Not implemented");
    }

    /// <summary>
    /// This method is not implemented in the LdapLoginService, as the user retrieval process is handled through the Login(string username, string password) method and the GetOrCreateUser(User userEntryLdap, LdapEntry entry) method.
    /// </summary>
    /// <param name="username">The username of the user to retrieve.</param>
    /// <returns>A task representing the asynchronous operation.</returns>
    /// <exception cref="LoginServicesException">Thrown when the method is not implemented.</exception>
    public Task<User?> GetByUsername(string username)
    {
        throw new LoginServicesException("Not implemented");
    }

    /// <summary>
    /// This method is not implemented in the LdapLoginService, as the user retrieval process is handled through the Login(string username, string password) method and the GetOrCreateUser(User userEntryLdap, LdapEntry entry) method.
    /// </summary>
    /// <returns>A task representing the asynchronous operation.</returns>
    /// <exception cref="LoginServicesException">Thrown when the method is not implemented.</exception>
    public Task<List<User>> GetAllUsers()
    {
        throw new LoginServicesException("Not implemented");
    }

    /// <summary>
    /// This method retrieves an existing user from the application based on the information obtained from the LDAP entry, or creates a new user if one does not already exist. 
    /// It also checks and updates the user's authorities based on the LDAP entry and the application's configuration.
    /// </summary>
    /// <param name="userEntryLdap">The user information obtained from the LDAP entry.</param>
    /// <param name="entry">The LDAP entry containing the user's information.</param>
    /// <returns>The existing or newly created user with updated authorities.</returns>
    private async Task<User?> GetOrCreateUser(User userEntryLdap, LdapEntry entry)
    {
        var userToReturn = (await _userService.Value.GetUserByUserName(userEntryLdap.UserName) ??
                            await _userService.Value.GetUserByName(userEntryLdap.Name)) ??
                           await _userService.Value.CreateUser(userEntryLdap);

        if (userToReturn == null) return userToReturn;
        userToReturn.Authorization = [];

        var authorities = await CheckAuthorities(userToReturn, entry);

        userToReturn.Authorization.AddRange(authorities);
        //foreach (var authorization in userToReturn.Authorization)
        //{
        //    Enum.TryParse<RolesType>(authorization.Rol, out var compareRole);
        //    if (compareRole == RolesType.Admin) userToReturn.Rol = RolesType.Admin;
        //}
        return userToReturn;
    }

    /// <summary>
    /// This method checks the authorities of a user based on the LDAP entry and the application's configuration.
    /// </summary>
    /// <param name="user">The user whose authorities are being checked.</param>
    /// <param name="entry">The LDAP entry containing the user's information.</param>
    /// <returns>A list of updated authorities for the user.</returns>
    private async Task<List<Authorization>> CheckAuthorities(User user, LdapEntry entry)
    {
        try
        {
            var authorizationMap = GetAuthoritiesMap(entry, user);
            var authorizationWhiteList = GetAuthoritiesWhiteList(entry, user);
            _logger.LogInformation(
                "[LDAP] user {user} authorizationMap count is {authorizationMap}, authorizationWhiteList count is {authorizationWhiteList}",
                user.UserName, authorizationMap.Count, authorizationWhiteList.Count);

            // Primero, creamos un HashSet con los DisplayID de la lista blanca para búsqueda eficiente
            var whiteListDisplayIds = new HashSet<string?>(authorizationWhiteList.Select(a => a.DisplayId));

            // Filtramos los elementos de authorizationMap que no están en la lista blanca, basándonos en DisplayID
            var uniqueMapAuthorizations = authorizationMap.Where(a => !whiteListDisplayIds.Contains(a.DisplayId));

            // Finalmente, combinamos los elementos únicos de authorizationMap con los de authorizationWhiteList
            var combinedList = authorizationWhiteList.Concat(uniqueMapAuthorizations).ToList();

            var userAuthorities = await _authorityService.GetUserAuthorities(user.Id);
            foreach (var auth in combinedList)
            {
                var authFound = userAuthorities.Find(c => c.DisplayId == auth.DisplayId);
                if (authFound is { CanUpdate: true })
                {
                    authFound.Rol = auth.Rol;
                    await _authorityService.updateOne(authFound);
                }
                else
                {
                    await _authorityService.InsertOne(auth);
                }
            }

            return await _authorityService.GetUserAuthorities(user.Id);
        }
        catch (Exception e)
        {
            _logger.LogError("[LDAP] CheckAuthorities for user {user} has exception {ex}", user.UserName, e.Message);

            return [];
        }
    }

    /// <summary>
    /// This method retrieves a list of authorities for a user based on a whitelist defined in the application's configuration.
    /// </summary>
    /// <param name="entry">The LDAP entry containing the user's information.</param>
    /// <param name="user">The user whose authorities are being retrieved.</param>
    /// <returns>A list of authorities for the user based on the whitelist.</returns>
    private List<Authorization> GetAuthoritiesWhiteList(LdapEntry entry, User user)
    {
        try
        {
            var userWhiteList = new List<Authorization>();
            var userNameProperty = _ldapConfig.UserNameProperty;

            var whiteList = _ldapConfig.WhiteList.FindAll(u =>
                (u.Name != null && entry.Dn.Contains(u.Name, StringComparison.CurrentCultureIgnoreCase)) ||
                (u.Username != null &&
                 userNameProperty != null &&
                 entry.GetAttributeSet().TryGetValue(userNameProperty, out var attr) &&
                 attr.StringValue != null &&
                 attr.StringValue.Equals(u.Username, StringComparison.CurrentCultureIgnoreCase))
            );

            if (whiteList.Count == 0) return userWhiteList;

            foreach (var authorityMap in whiteList)
            {
                if (!Enum.TryParse<PermissionEnum.RolesType>(authorityMap.Rol, out _))
                    continue;

                userWhiteList.Add(new Authorization
                {
                    UserId = user.Id,
                    DisplayId = authorityMap.DisplayId,
                    Rol = authorityMap.Rol
                });
            }

            return userWhiteList;
        }
        catch (Exception e)
        {
            _logger.LogError("[LDAP] GetAuthoritiesWhiteList for user {user} has exception {ex}", user.UserName,
                e.Message);

            return [];
        }
    }

    /// <summary>
    /// This method retrieves a list of authorities for a user based on the LDAP entry and the application's configuration for mapping LDAP groups to authorities.
    /// </summary>
    /// <param name="ldapEntry">The LDAP entry containing the user's information.</param>
    /// <returns>The existing or newly created user with updated authorities.</returns>
    private User GetUser(LdapEntry ldapEntry)
    {
        var attributes = ldapEntry.GetAttributeSet();

        var user = new User();

        // UserName
        if (!string.IsNullOrWhiteSpace(_ldapConfig.UserNameProperty) &&
            attributes.TryGetValue(_ldapConfig.UserNameProperty, out var userAttr) &&
            userAttr?.StringValue != null)
        {
            user.UserName = userAttr.StringValue;
        }
        else
        {
            user.UserName = "";
        }

        _logger.LogInformation("[LDAP] GetUser UserName is {UserName} ", user.UserName);

        // First name
        if (!string.IsNullOrWhiteSpace(_ldapConfig.FirstNameProperty) &&
            attributes.TryGetValue(_ldapConfig.FirstNameProperty, out var firstNameAttr) &&
            firstNameAttr?.StringValue != null)
        {
            user.Name = firstNameAttr.StringValue;
        }

        // Last name
        if (!string.IsNullOrWhiteSpace(_ldapConfig.LastNameProperty) &&
            attributes.TryGetValue(_ldapConfig.LastNameProperty, out var lastNameAttr) &&
            lastNameAttr?.StringValue != null)
        {
            user.Name = string.IsNullOrEmpty(user.Name)
                ? lastNameAttr.StringValue
                : $"{user.Name} {lastNameAttr.StringValue}";
        }

        return user;
    }

    /// <summary>
    /// This method retrieves a list of authorities for a user based on the LDAP entry and the application's configuration for mapping LDAP groups to authorities.
    /// </summary>
    /// <param name="ldapEntry">The LDAP entry containing the user's information.</param>
    /// <param name="user">The user whose authorities are being retrieved.</param>
    /// <returns>A list of authorities for the user based on the LDAP entry and the application's configuration.</returns>
    private List<Authorization> GetAuthoritiesMap(LdapEntry ldapEntry, User user)
    {
        try
        {
            var authorities = new List<Authorization>();

            if (!_ldapConfig.AuthoritiesMap.Any())
                return authorities;

            var attributes = ldapEntry.GetAttributeSet();

            if (!string.IsNullOrWhiteSpace(_ldapConfig.GroupsProperty) ||
                !attributes.TryGetValue(_ldapConfig.GroupsProperty!, out var groupsAttr) ||
                groupsAttr?.StringValueArray == null)
            {
                return authorities;
            }

            var groups = groupsAttr.StringValueArray.ToList();

            foreach (var authorityMap in _ldapConfig.AuthoritiesMap)
            {
                if (groups.Exists(g =>
                        string.Equals(g, authorityMap.Group, StringComparison.CurrentCultureIgnoreCase)))
                {
                    if (!Enum.TryParse<PermissionEnum.RolesType>(authorityMap.Rol, out _))
                        continue;

                    authorities.Add(new Authorization
                    {
                        UserId = user.Id,
                        DisplayId = authorityMap.DisplayId,
                        Rol = authorityMap.Rol
                    });
                }
            }

            return authorities;
        }
        catch (Exception e)
        {
            _logger.LogError("Error getting Authorities Map. Return new empty list. Exception: {e}", e);
            return [];
        }
    }
}