using Novell.Directory.Ldap;
using Microsoft.Extensions.Configuration;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;

namespace OneForMe.Services;

public interface ILdapService
{
    Task<bool> AuthenticateAsync(string username, string password);
    Task<LdapUser> GetUserAsync(string username);
}

public partial class LdapService : ILdapService
{
    public Dictionary<string, HashAlgorithm> HashAlgorithms => new() { ["MD5"] = MD5.Create(), ["SHA1"] = SHA1.Create(), ["SHA256"] = SHA256.Create(), ["SHA384"] = SHA384.Create(), ["SHA512"] = SHA512.Create(), ["SSHA"] = SHA1.Create(), ["SSHA256"] = SHA256.Create(), ["SSHA384"] = SHA384.Create(), ["SSHA512"] = SHA512.Create()};
    private readonly IConfiguration _configuration;
    private readonly ILogger<LdapService> _logger;

    public LdapService(IConfiguration configuration, ILogger<LdapService> logger)
    {
        _configuration = configuration;
        _logger = logger;
    }

    public async Task<bool> AuthenticateAsync(string username, string password)
    {
        try
        {
            LdapUser user = await GetUserAsync(username);
            bool hashMatches = CompareStringToHashed(password, user.Password);
            _logger.LogInformation("User {username} authenticated successfully via LDAP", [username]);
            return hashMatches;
        }
        catch (LdapException ex)
        {
            _logger.LogWarning("LDAP authentication failed for user {username}: {ex.Message}", [username, ex.Message]);
            return false;
        }
        catch (Exception ex)
        {
            _logger.LogError("LDAP authentication error for user {username}: {ex.Message}", [username, ex.Message]);
            return false;
        }
    }

    public async Task<LdapUser> GetUserAsync(string username)
    {
        try
        {
            var connection = new LdapConnection();
            var ldapSettings = _configuration.GetSection("LdapSettings");
            var serverName = ldapSettings["ServerName"];
            var port = int.Parse(ldapSettings["Port"] ?? "389");
            var baseDn = ldapSettings["BaseDn"];
            var bindDn = ldapSettings["BindDn"];
            var bindPassword = ldapSettings["BindPassword"];

            connection.Connect(serverName, port);
            connection.Bind(bindDn, bindPassword);

            var searchFilter = $"(uid={username})";
            var results = connection.Search(
                baseDn,
                LdapConnection.ScopeSub,
                searchFilter,
                new[] { "uid", "mail", "cn", "sn", "userPassword" },
                false
            );

            LdapUser user = null;
            while (results.HasMore())
            {
                var entry = results.Next();
                LdapAttributeSet attributeSet = entry.GetAttributeSet();
                attributeSet.TryGetValue("uid", out LdapAttribute? uid);
                attributeSet.TryGetValue("mail", out LdapAttribute? mail);
                attributeSet.TryGetValue("cn", out LdapAttribute? cn);
                attributeSet.TryGetValue("sn", out LdapAttribute? sn);
                attributeSet.TryGetValue("userPassword", out LdapAttribute? userPassword);
                user = new LdapUser
                {
                    Username = uid?.StringValue ?? username, //entry.GetAttribute("uid")?.StringValue ?? username,
                    Email = mail?.StringValue ?? "", //entry.GetAttribute("mail")?.StringValue ?? $"{username}@example.com",
                    FullName = cn?.StringValue ?? "", //entry.GetAttribute("cn")?.StringValue ?? username,
                    LastName = sn?.StringValue ?? "", //entry.GetAttribute("sn")?.StringValue ?? "",
                    Password = userPassword?.StringValue ?? "" //entry.GetAttribute("userPassword")?.StringValue ?? ""
                };
            }

            connection.Disconnect();
            return user;
        }
        catch (Exception ex)
        {
            _logger.LogError($"Error retrieving LDAP user {username}: {ex.Message}");
            return null;
        }
    }

        public bool CompareStringToHashed(string sourcePassword, string targetPasswordHashed)
    {
        string algorithmName = CurlyBracesExtractor().Match(targetPasswordHashed).Groups[1].Value;
        HashAlgorithm hashAlgorithm = HashAlgorithms[algorithmName.ToUpperInvariant()];
        byte[] sourcePasswordBytes = Encoding.UTF8.GetBytes(sourcePassword);
        byte[] targetPasswordHashedBytes = Convert.FromBase64String(CurlyBracesRemover().Replace(targetPasswordHashed, ""));
        int hashSize = hashAlgorithm.HashSize / 8;
        if (targetPasswordHashedBytes.Length > hashSize) // Has salt
        {
            int saltLength = targetPasswordHashedBytes.Length - hashSize;
            byte[] salt = new byte[saltLength];
            Array.Copy(targetPasswordHashedBytes, hashSize, salt, 0, saltLength);
            Array.Resize(ref targetPasswordHashedBytes, hashSize);
            byte[] newSourcePasswordBytes = new byte[sourcePasswordBytes.Length + salt.Length];
            Array.Copy(sourcePasswordBytes, 0, newSourcePasswordBytes, 0, sourcePasswordBytes.Length);
            Array.Copy(salt, 0, newSourcePasswordBytes, sourcePasswordBytes.Length, salt.Length);
            sourcePasswordBytes = newSourcePasswordBytes;
        }
        sourcePasswordBytes = hashAlgorithm.ComputeHash(sourcePasswordBytes);
        return CryptographicOperations.FixedTimeEquals(sourcePasswordBytes, targetPasswordHashedBytes);
    }

    [GeneratedRegex(@"\{.*?\}")]
    private static partial Regex CurlyBracesRemover();
    [GeneratedRegex(@"\{([^}]*)\}")]
    private static partial Regex CurlyBracesExtractor();
}

public class LdapUser
{
    public string Username { get; set; }
    public string Password { get; set; } 
    public string Email { get; set; }
    public string FullName { get; set; }
    public string LastName { get; set; }
}
