using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Novell.Directory.Ldap;

namespace ScadaLink.Security;

public class LdapAuthService
{
    private readonly SecurityOptions _options;
    private readonly ILogger<LdapAuthService> _logger;

    public LdapAuthService(IOptions<SecurityOptions> options, ILogger<LdapAuthService> logger)
    {
        _options = options?.Value ?? throw new ArgumentNullException(nameof(options));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public async Task<LdapAuthResult> AuthenticateAsync(string username, string password, CancellationToken ct = default)
    {
        if (string.IsNullOrWhiteSpace(username))
            return new LdapAuthResult(false, null, null, null, "Username is required.");

        if (string.IsNullOrWhiteSpace(password))
            return new LdapAuthResult(false, null, null, null, "Password is required.");

        // Enforce TLS unless explicitly allowed for dev/test
        if (!_options.LdapUseTls && !_options.AllowInsecureLdap)
        {
            return new LdapAuthResult(false, null, null, null,
                "Insecure LDAP connections are not allowed. Enable TLS or set AllowInsecureLdap for dev/test.");
        }

        try
        {
            using var connection = new LdapConnection();

            if (_options.LdapUseTls)
            {
                connection.SecureSocketLayer = true;
            }

            await Task.Run(() => connection.Connect(_options.LdapServer, _options.LdapPort), ct);

            if (_options.LdapUseTls && !connection.SecureSocketLayer)
            {
                await Task.Run(() => connection.StartTls(), ct);
            }

            // Resolve the user's actual DN, then bind with their credentials
            var bindDn = await ResolveUserDnAsync(connection, username, ct);
            await Task.Run(() => connection.Bind(bindDn, password), ct);

            // Re-bind as service account for attribute/group lookup (user may lack search rights)
            if (!string.IsNullOrWhiteSpace(_options.LdapServiceAccountDn))
            {
                await Task.Run(() =>
                    connection.Bind(_options.LdapServiceAccountDn, _options.LdapServiceAccountPassword), ct);
            }

            // Query for user attributes and group memberships
            var displayName = username;
            var groups = new List<string>();

            try
            {
                var searchFilter = $"(uid={EscapeLdapFilter(username)})";
                var searchResults = await Task.Run(() =>
                    connection.Search(
                        _options.LdapSearchBase,
                        LdapConnection.ScopeSub,
                        searchFilter,
                        new[] { _options.LdapDisplayNameAttribute, _options.LdapGroupAttribute },
                        false), ct);

                while (searchResults.HasMore())
                {
                    try
                    {
                        var entry = searchResults.Next();
                        var dnAttr = entry.GetAttribute(_options.LdapDisplayNameAttribute);
                        if (dnAttr != null)
                            displayName = dnAttr.StringValue;

                        var groupAttr = entry.GetAttribute(_options.LdapGroupAttribute);
                        if (groupAttr != null)
                        {
                            foreach (var groupDn in groupAttr.StringValueArray)
                            {
                                groups.Add(ExtractFirstRdnValue(groupDn));
                            }
                        }
                    }
                    catch (LdapException)
                    {
                        // No more results
                        break;
                    }
                }
            }
            catch (LdapException ex)
            {
                _logger.LogWarning(ex, "Failed to query LDAP attributes for user {Username}; authentication succeeded but group lookup failed", username);
                // Auth succeeded even if attribute lookup failed
            }

            connection.Disconnect();

            return new LdapAuthResult(true, displayName, username, groups, null);
        }
        catch (LdapException ex)
        {
            _logger.LogWarning(ex, "LDAP authentication failed for user {Username}", username);
            return new LdapAuthResult(false, null, username, null, "Invalid username or password.");
        }
        catch (Exception ex) when (ex is not OperationCanceledException)
        {
            _logger.LogError(ex, "Unexpected error during LDAP authentication for user {Username}", username);
            return new LdapAuthResult(false, null, username, null, "An unexpected error occurred during authentication.");
        }
    }

    /// <summary>
    /// Resolves the user's full DN. When a service account is configured, performs a
    /// search-then-bind lookup. Otherwise falls back to constructing the DN directly.
    /// </summary>
    private async Task<string> ResolveUserDnAsync(LdapConnection connection, string username, CancellationToken ct)
    {
        // If username already looks like a DN, use it as-is
        if (username.Contains('='))
            return username;

        // If a service account is configured, search for the user's actual DN
        if (!string.IsNullOrWhiteSpace(_options.LdapServiceAccountDn))
        {
            await Task.Run(() =>
                connection.Bind(_options.LdapServiceAccountDn, _options.LdapServiceAccountPassword), ct);

            var searchFilter = $"(uid={EscapeLdapFilter(username)})";
            var searchResults = await Task.Run(() =>
                connection.Search(
                    _options.LdapSearchBase,
                    LdapConnection.ScopeSub,
                    searchFilter,
                    new[] { "dn" },
                    false), ct);

            if (searchResults.HasMore())
            {
                var entry = searchResults.Next();
                return entry.Dn;
            }

            throw new LdapException("User not found", LdapException.NoSuchObject, $"No entry found for uid={username}");
        }

        // Fallback: construct DN directly
        return string.IsNullOrWhiteSpace(_options.LdapSearchBase)
            ? $"cn={username}"
            : $"cn={username},{_options.LdapSearchBase}";
    }

    private static string EscapeLdapFilter(string input)
    {
        return input
            .Replace("\\", "\\5c")
            .Replace("*", "\\2a")
            .Replace("(", "\\28")
            .Replace(")", "\\29")
            .Replace("\0", "\\00");
    }

    private static string ExtractFirstRdnValue(string dn)
    {
        // Extract the value of the first RDN from a DN.
        // Handles cn=, ou=, or any attribute: "ou=SCADA-Admins,ou=groups,dc=..." → "SCADA-Admins"
        var equalsIndex = dn.IndexOf('=');
        if (equalsIndex < 0)
            return dn;

        var valueStart = equalsIndex + 1;
        var commaIndex = dn.IndexOf(',', valueStart);
        return commaIndex > valueStart ? dn[valueStart..commaIndex] : dn[valueStart..];
    }
}
