using Novell.Directory.Ldap;
using Microsoft.Extensions.Options;
using Berufsschule_HAM.Models;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Text.Json;

namespace Berufsschule_HAM.Services;
public partial class LdapService : IDisposable
{
    private readonly LdapConfig _opts;
    private readonly LdapConnection _conn;
    private ILogger _logger;

    public LdapService(IOptions<LdapConfig> options, ILogger<LdapService> logger)
    {
        _opts = options.Value;
        _conn = new LdapConnection { SecureSocketLayer = _opts.UseSsl };
        _logger = logger;
        ConnectAndBind().Wait();
    }

    private async Task ConnectAndBind()
    {
        if (!_conn.Connected)
        {
            Console.WriteLine(_opts.Host);
            Console.WriteLine(_opts.Port);
            try
            {
                await _conn.ConnectAsync(_opts.Host, _opts.Port);
            }
            catch (SystemException ex)
            {
                _logger.LogCritical("Unable to connect to LDAP: {ex.Message}\n{ex.StackTrace}", [ex.Message, ex.StackTrace]);
                throw;
            }
        }
        await _conn.BindAsync(_opts.BindDn, _opts.BindPassword);
    }

    public string AssetsBaseDn => string.IsNullOrEmpty(_opts.AssetsOu) ? _opts.BaseDn : $"{_opts.AssetsOu},{_opts.BaseDn}";
    public string LocationsBaseDn => string.IsNullOrEmpty(_opts.LocationsOu) ? _opts.BaseDn : $"{_opts.LocationsOu},{_opts.BaseDn}";
    public string UsersBaseDn => string.IsNullOrEmpty(_opts.UsersOu) ? _opts.BaseDn : $"{_opts.UsersOu},{_opts.BaseDn}";
    public string GroupsBaseDn => string.IsNullOrEmpty(_opts.GroupsOu) ? _opts.BaseDn : $"{_opts.GroupsOu},{_opts.BaseDn}";
    public string MigrationsBaseDn => string.IsNullOrEmpty(_opts.MigrationsOu) ? _opts.BaseDn : $"{_opts.MigrationsOu},{_opts.BaseDn}";
    public string[] UsersAttributes => ["cn", "sn", "title", "uid", "jpegPhoto", "userPassword", "description"];
    public string[] AssetsAttributes => ["CN", "description", "l", "owner", "serialNumber", "name"];
    public string[] LocationsAttributes => ["l", "description"];
    public string[] GroupsAttributes => ["cn", "gidNumber", "description"];
    public async Task<IEnumerable<LocationModel>> ListLocationsAsync()
    {
        IEnumerable<Dictionary<string, string>> locations = await ListObjectBy(LocationsBaseDn, "", LocationsAttributes);
        List<LocationModel> models = [];
        locations.ToList().ForEach(x => models.Add(new LocationModel(x) {Location = x[LocationsAttributes[0]]}));        
        return models;
    }

    public async Task<IEnumerable<UserModel>> ListUsersAsync()
    {
        IEnumerable<Dictionary<string, string>> users = await ListObjectBy(UsersBaseDn, "", UsersAttributes);
        List<UserModel> models = [];
        users.ToList().ForEach(x => models.Add(new UserModel(x) {Uid = x["uid"]}));        
        return models;
    }

    public async Task<MigrationModel> GetMigrationVersionAsync()
    {
        Dictionary<string, string> objects;
        try
        {
            objects = (await ListObjectBy(_opts.BaseDn, _opts.MigrationsOu, ["description"])).First();
        }
        catch (Exception)
        {
            objects = [];
        }
        return new MigrationModel(objects);
    }

    public async Task<bool> UpdateMigrationVersionAsync(MigrationModel migrationModel)
    {
        await ConnectAndBind();
        try
        {
            string dn = MigrationsBaseDn; //PrependRDN($"", MigrationsBaseDn);
            string targetText = JsonSerializer.Serialize(migrationModel);
            _logger.LogInformation("Setting the LDAP migration description to {targetText} for {dn}", [targetText, dn]);
            var modification = new LdapModification(
                LdapModification.Replace,
                new LdapAttribute("description", targetText)
            );
            await _conn.ModifyAsync(dn, modification);
        }
        catch (Exception)
        {
            return false;
        }
        return true;
    }

    public async Task<IEnumerable<UserModel>> ListUsersAsync(string[] attributes)
    {
        List<UserModel> returnValue = [];
        (await ListObjectBy(UsersBaseDn, "", attributes))
            .ToList()
            .ForEach(x =>
                returnValue.Add(
                    new UserModel(x)
                    {
                        Uid = x["uid"]
                    }
                )
            );
        return returnValue;
    }

    public async Task<IEnumerable<GroupModel>> ListGroupsAsync(string[] attributes)
    {
        List<GroupModel> returnValue = [];
        (await ListObjectBy(GroupsBaseDn, "", attributes))
            .ToList()
            .ForEach(x =>
                returnValue.Add(
                    new GroupModel(x)
                    {
                        Cn = x["cn"]
                    }
                )
            );
        return returnValue;
    }
    public async Task<UserModel> GetUserByUidAsync(string uid)
    {
        return new UserModel((await ListObjectBy(UsersBaseDn, $"uid={uid}", ["cn", "sn", "title", "uid", "jpegPhoto", "userPassword", "description"])).First()) { Uid = uid };
    }

    public async Task<UserModel> GetUserByUidAsync(string uid, string[] attributes)
    {
        return new UserModel((await ListObjectBy(UsersBaseDn, $"uid={uid}", attributes)).First()) { Uid = uid };
    }

    public async Task<GroupModel> GetGroupByCnAsync(string cn, string[] attributes)
    {
        return new GroupModel((await ListObjectBy(GroupsBaseDn, $"cn={cn}", attributes)).First()) { Cn = cn };
    }

    public async Task<LocationModel> GetLocationByCnAsync(string cn)
    {
        return await GetLocationByCnAsync(cn, LocationsAttributes);
    }
    public async Task<LocationModel> GetLocationByCnAsync(string location, string[] attributes)
    {
        return new LocationModel((await ListObjectBy(LocationsBaseDn, $"l={location}", attributes)).First()) { Location = location };
    }

    public async Task<AssetModel> GetAssetByCnAsync(string cn)
    {
        return await GetAssetByCnAsync(cn, AssetsAttributes);
    }
    public async Task<AssetModel> GetAssetByCnAsync(string cn, string[] attributes)
    {
        return new AssetModel((await ListObjectBy(AssetsBaseDn, $"cn={cn}", attributes)).First()) { Cn = cn };
    }

    public async Task<IEnumerable<AssetModel>> ListDeviceAsync()
    {
        IEnumerable<Dictionary<string, string>> devices = await ListObjectBy(AssetsBaseDn, "(objectClass=device)", AssetsAttributes);
        List<AssetModel> models = [];
        devices.ToList().ForEach(x => models.Add(new AssetModel(x) {Cn = x["cn"]}));        
        return models;
    }

    public async Task CreateUser(string uid, LdapAttributeSet attributeSet)
    {
        string dn = PrependRDN($"uid={uid}", UsersBaseDn);
        await CreateObject(dn, attributeSet);
    }

    public async Task CreateGroup(string cn, LdapAttributeSet attributeSet)
    {
        string dn = PrependRDN($"cn={cn}", GroupsBaseDn);
        await CreateObject(dn, attributeSet);
    }

public async Task CreateAsset(LdapAttributeSet attributeSet)
{
    string? cn = attributeSet.GetAttribute("cn")?.StringValue;

    if (string.IsNullOrEmpty(cn))
        throw new ArgumentException("AttributeSet must contain a cn attribute.");

    string dn = PrependRDN($"cn={cn}", AssetsBaseDn);
    await CreateObject(dn, attributeSet);
}

    public async Task CreateLocation(LdapAttributeSet attributeSet)
    {
        string? cn = attributeSet.GetAttribute("l")?.StringValue;

        if (string.IsNullOrEmpty(cn))
            throw new ArgumentException("AttributeSet must contain an l attribute.");

        string dn = PrependRDN($"l={cn}", LocationsBaseDn);
        await CreateObject(dn, attributeSet);
    }

    public async Task<UserAuthenticationResult> AuthenticateUser(string username, string password)
    {
        await ConnectAndBind();
        try
        {
            try
            {
                UserModel user = await GetUserByUidAsync(username);
                if (user.UserPassword is null)
                {
                    return new() { Success = false, AuthenticationState = UserNotAuthenticatedReason.InvalidCredentials };
                }
                if (CompareStringToSha256(password, user.UserPassword))
                {
                    return new() { Success = true };
                }
                return new() { Success = false, AuthenticationState = UserNotAuthenticatedReason.InvalidCredentials };
            }
            catch (InvalidOperationException)
            {
                return new() { Success = false, AuthenticationState = UserNotAuthenticatedReason.InvalidCredentials };
            }
        }
        catch (LdapException)
        {
            return new() { Success = false, AuthenticationState = UserNotAuthenticatedReason.InvalidCredentials };
        }
    }

    public bool CompareStringToSha256(string sourcePassword, string targetPasswordHashed)
    {
        byte[] sourcePasswordBytes = SHA256.HashData(Encoding.UTF8.GetBytes(sourcePassword));
        byte[] targetPasswordHashedBytes = Convert.FromBase64String(CurlyBracesRemover().Replace(targetPasswordHashed, ""));
        if (sourcePasswordBytes.Length != targetPasswordHashedBytes.Length)
        {
            return false;
        }
        for (int i = 0; i < sourcePasswordBytes.Length; i++)
        {
            if (sourcePasswordBytes[i] != targetPasswordHashedBytes[i])
            {
                return false;
            }
        }
        return true;
    }

    private string PrependRDN(string rdn, string dn)
    {
        return rdn + "," + dn;
    }

    public async Task<IEnumerable<Dictionary<string, string>>> ListObjectBy(string baseDn, string filter, string[] attributes)
    {
        return await Task.Run(async () =>
        {
            await ConnectAndBind();
            var search = await _conn.SearchAsync(
                baseDn,
                LdapConnection.ScopeSub,
                $"{filter}",
                attributes,
                false);
            var list = new List<Dictionary<string, string>>();
            while (await search.HasMoreAsync())
            {
                try
                {
                    var e = await search.NextAsync();
                    var attributeSet = e.GetAttributeSet();
                    if (attributeSet.Count == 0) { continue; }
                    Dictionary<string, string> attributeMap = [];
                    foreach (LdapAttribute attribute in attributeSet)
                    {
                        attributeMap[attribute.Name] = attribute.StringValue;
                    }
                    list.Add(attributeMap);
                }
                catch (LdapException) { }
            }
            return list;
        });
    }

    public async Task DeleteUserAsync(string uid)
    {
        string dn = PrependRDN($"uid={uid}", UsersBaseDn);
        await DeleteObjectByDnAsync(dn);
    }

    public async Task DeleteGroupAsync(string cn)
    {
        string dn = PrependRDN($"cn={cn}", GroupsBaseDn);
        await DeleteObjectByDnAsync(dn);
    }

    public async Task DeleteLocationAsync(string location)
    {
        string dn = PrependRDN($"l={location}", LocationsBaseDn);
        await DeleteObjectByDnAsync(dn);
    }

    public async Task DeleteAssetAsync(string cn)
    {
        string dn = PrependRDN($"cn={cn}", AssetsBaseDn);
        await DeleteObjectByDnAsync(dn);
    }

    public async Task UpdateUser(string uid, string attributeName, string attributeValue)
    {
        await UpdateObject(UsersBaseDn, "uid", uid, attributeName, attributeValue);
    }

    public async Task UpdateGroup(string cn, string attributeName, string attributeValue)
    {
        await UpdateObject(GroupsBaseDn, "cn", cn, attributeName, attributeValue);
    }

    public async Task UpdateLocation(string location, string attributeName, string attributeValue)
    {
        await UpdateObject(LocationsBaseDn, "l", location, attributeName, attributeValue);
    }

    public async Task UpdateAsset(string cn, string attributeName, string attributeValue)
    {
        await UpdateObject(AssetsBaseDn, "cn", cn, attributeName, attributeValue);
    }

    public async Task UpdateObject(string baseDn, string rdnKey, string rdnValue, string attributeName, string attributeValue)
    {
        await ConnectAndBind();
        string dn = PrependRDN($"{rdnKey}={rdnValue}", baseDn);
        if (attributeName == rdnKey)
        {
            await _conn.RenameAsync(dn, $"{rdnKey}={attributeValue}", true);
        }
        else
        {
            var modification = new LdapModification(
                LdapModification.Replace,
                new LdapAttribute(attributeName, attributeValue)
            );
            await _conn.ModifyAsync(dn, modification);
        }
    }

    public async Task DeleteObjectByDnAsync(string dn)
    {
        await _conn.DeleteAsync(dn);
    }

    public async Task CreateObject(string dn, LdapAttributeSet attributeSet)
    {
        await ConnectAndBind();
        LdapEntry ldapEntry = new(dn, attributeSet);
        await _conn.AddAsync(ldapEntry);
    }

    public void Dispose()
    {
        if (_conn != null && _conn.Connected)
        {
            _conn.Disconnect();
        }
    }

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