using System.Security.Cryptography.X509Certificates;

namespace NATS.Server.Auth;

/// <summary>
/// Authenticates clients by mapping TLS certificate subject DN to configured users.
/// Corresponds to Go server/auth.go checkClientTLSCertSubject.
/// </summary>
public sealed class TlsMapAuthenticator : IAuthenticator
{
    private readonly Dictionary<string, User> _usersByDn;
    private readonly Dictionary<string, User> _usersByCn;

    public TlsMapAuthenticator(IReadOnlyList<User> users)
    {
        _usersByDn = new Dictionary<string, User>(StringComparer.OrdinalIgnoreCase);
        _usersByCn = new Dictionary<string, User>(StringComparer.OrdinalIgnoreCase);
        foreach (var user in users)
        {
            _usersByDn[user.Username] = user;
            _usersByCn[user.Username] = user;
        }
    }

    public AuthResult? Authenticate(ClientAuthContext context)
    {
        var cert = context.ClientCertificate;
        if (cert == null)
            return null;

        var dn = cert.SubjectName;
        var dnString = dn.Name; // RFC 2253 format

        // Try exact DN match first
        if (_usersByDn.TryGetValue(dnString, out var user))
            return BuildResult(user);

        // Try CN extraction
        var cn = ExtractCn(dn);
        if (cn != null && _usersByCn.TryGetValue(cn, out user))
            return BuildResult(user);

        // Try SAN-based values
        var email = cert.GetNameInfo(X509NameType.EmailName, forIssuer: false);
        if (!string.IsNullOrWhiteSpace(email) && _usersByCn.TryGetValue(email, out user))
            return BuildResult(user);

        var dns = cert.GetNameInfo(X509NameType.DnsName, forIssuer: false);
        if (!string.IsNullOrWhiteSpace(dns) && _usersByCn.TryGetValue(dns, out user))
            return BuildResult(user);

        var uri = cert.GetNameInfo(X509NameType.UrlName, forIssuer: false);
        if (!string.IsNullOrWhiteSpace(uri) && _usersByCn.TryGetValue(uri, out user))
            return BuildResult(user);

        // Match using full RDN + DC components if present.
        var dcs = GetTlsAuthDcs(dn);
        if (!string.IsNullOrEmpty(dcs))
        {
            var rdnWithDcs = string.IsNullOrEmpty(dnString) ? dcs : $"{dnString},{dcs}";
            if (_usersByDn.TryGetValue(rdnWithDcs, out user))
                return BuildResult(user);
        }

        return null;
    }

    internal static string GetTlsAuthDcs(X500DistinguishedName dn)
    {
        if (string.IsNullOrWhiteSpace(dn.Name))
            return string.Empty;

        var dcs = new List<string>();
        foreach (var rdn in dn.Name.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries))
        {
            if (!rdn.StartsWith("DC=", StringComparison.OrdinalIgnoreCase))
                continue;

            dcs.Add("DC=" + rdn[3..].Trim());
        }

        return string.Join(",", dcs);
    }

    internal static string[] DnsAltNameLabels(string dnsAltName)
    {
        if (string.IsNullOrWhiteSpace(dnsAltName))
            return [];

        return dnsAltName.ToLowerInvariant().Split('.', StringSplitOptions.RemoveEmptyEntries);
    }

    internal static bool DnsAltNameMatches(string[] dnsAltNameLabels, IReadOnlyList<Uri?> urls)
    {
        foreach (var url in urls)
        {
            if (url == null)
                continue;

            var hostLabels = url.DnsSafeHost.ToLowerInvariant().Split('.', StringSplitOptions.RemoveEmptyEntries);
            if (hostLabels.Length != dnsAltNameLabels.Length)
                continue;

            var i = 0;
            if (dnsAltNameLabels.Length > 0 && dnsAltNameLabels[0] == "*")
                i = 1;

            var matched = true;
            for (; i < dnsAltNameLabels.Length; i++)
            {
                if (!string.Equals(dnsAltNameLabels[i], hostLabels[i], StringComparison.Ordinal))
                {
                    matched = false;
                    break;
                }
            }

            if (matched)
                return true;
        }

        return false;
    }

    private static string? ExtractCn(X500DistinguishedName dn)
    {
        var dnString = dn.Name;
        foreach (var rdn in dnString.Split(',', StringSplitOptions.TrimEntries))
        {
            if (rdn.StartsWith("CN=", StringComparison.OrdinalIgnoreCase))
                return rdn[3..];
        }
        return null;
    }

    private static AuthResult BuildResult(User user)
    {
        return new AuthResult
        {
            Identity = user.Username,
            AccountName = user.Account,
            Permissions = user.Permissions,
            Expiry = user.ConnectionDeadline,
        };
    }
}
