// Copyright 2012-2025 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Adapted from server/auth.go (standalone functions) in the NATS server Go source.

using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using ZB.MOM.NatsNet.Server;

namespace ZB.MOM.NatsNet.Server.Auth;

/// <summary>
/// Authentication helper methods ported from Go auth.go.
/// Server-dependent methods (configureAuthorization, checkAuthentication, etc.)
/// will be added in later sessions when the full Server type is available.
/// </summary>
public static partial class AuthHandler
{
    /// <summary>
    /// Regex matching valid bcrypt password prefixes ($2a$, $2b$, $2x$, $2y$).
    /// Mirrors Go <c>validBcryptPrefix</c>.
    /// </summary>
    private static readonly Regex ValidBcryptPrefix = ValidBcryptPrefixRegex();

    [GeneratedRegex(@"^\$2[abxy]\$\d{2}\$.*")]
    private static partial Regex ValidBcryptPrefixRegex();

    /// <summary>
    /// Checks if a password string is a bcrypt hash.
    /// Mirrors Go <c>isBcrypt</c>.
    /// </summary>
    public static bool IsBcrypt(string password)
    {
        if (password.StartsWith('$'))
        {
            return ValidBcryptPrefix.IsMatch(password);
        }
        return false;
    }

    /// <summary>
    /// Compares a server password (possibly bcrypt-hashed) against a client-provided password.
    /// Uses constant-time comparison for plaintext passwords.
    /// Mirrors Go <c>comparePasswords</c>.
    /// </summary>
    public static bool ComparePasswords(string serverPassword, string clientPassword)
    {
        if (IsBcrypt(serverPassword))
        {
            return BCrypt.Net.BCrypt.Verify(clientPassword, serverPassword);
        }

        // Constant-time comparison for plaintext passwords.
        var spass = Encoding.UTF8.GetBytes(serverPassword);
        var cpass = Encoding.UTF8.GetBytes(clientPassword);
        return CryptographicOperations.FixedTimeEquals(spass, cpass);
    }

    /// <summary>
    /// Validates the ResponsePermission defaults within a Permissions struct.
    /// If Response is set but MaxMsgs/Expires are zero, applies defaults.
    /// Also ensures Publish is set with an empty Allow if not already defined.
    /// Mirrors Go <c>validateResponsePermissions</c>.
    /// </summary>
    public static void ValidateResponsePermissions(Permissions? p)
    {
        if (p?.Response == null)
        {
            return;
        }

        p.Publish ??= new SubjectPermission();
        p.Publish.Allow ??= [];

        if (p.Response.MaxMsgs == 0)
        {
            p.Response.MaxMsgs = ServerConstants.DefaultAllowResponseMaxMsgs;
        }
        if (p.Response.Expires == TimeSpan.Zero)
        {
            p.Response.Expires = ServerConstants.DefaultAllowResponseExpiration;
        }
    }

    /// <summary>
    /// Known connection type strings (uppercased).
    /// Mirrors Go jwt.ConnectionType* constants.
    /// </summary>
    public static class ConnectionTypes
    {
        public const string Standard = "STANDARD";
        public const string Websocket = "WEBSOCKET";
        public const string Leafnode = "LEAFNODE";
        public const string LeafnodeWs = "LEAFNODE_WS";
        public const string Mqtt = "MQTT";
        public const string MqttWs = "MQTT_WS";
        public const string InProcess = "IN_PROCESS";

        private static readonly HashSet<string> Known =
        [
            Standard,
            Websocket,
            Leafnode,
            LeafnodeWs,
            Mqtt,
            MqttWs,
            InProcess,
        ];

        public static bool IsKnown(string ct) => Known.Contains(ct);
    }

    /// <summary>
    /// Validates allowed connection type map entries. Normalises to uppercase
    /// and rejects unknown types.
    /// Mirrors Go <c>validateAllowedConnectionTypes</c>.
    /// </summary>
    public static Exception? ValidateAllowedConnectionTypes(HashSet<string>? m)
    {
        if (m == null) return null;

        // We must iterate a copy since we may modify the set.
        var entries = m.ToList();
        foreach (var ct in entries)
        {
            var ctuc = ct.ToUpperInvariant();
            if (!ConnectionTypes.IsKnown(ctuc))
            {
                return new ArgumentException($"unknown connection type \"{ct}\"");
            }
            if (ctuc != ct)
            {
                m.Remove(ct);
                m.Add(ctuc);
            }
        }
        return null;
    }

    /// <summary>
    /// Validates the no_auth_user setting against configured users/nkeys.
    /// Mirrors Go <c>validateNoAuthUser</c>.
    /// </summary>
    public static Exception? ValidateNoAuthUser(ServerOptions o, string noAuthUser)
    {
        if (string.IsNullOrEmpty(noAuthUser))
        {
            return null;
        }
        if (o.TrustedOperators.Count > 0)
        {
            return new InvalidOperationException("no_auth_user not compatible with Trusted Operator");
        }
        if (o.Nkeys == null && o.Users == null)
        {
            return new InvalidOperationException(
                $"no_auth_user: \"{noAuthUser}\" present, but users/nkeys are not defined");
        }
        if (o.Users != null)
        {
            foreach (var u in o.Users)
            {
                if (u.Username == noAuthUser) return null;
            }
        }
        if (o.Nkeys != null)
        {
            foreach (var u in o.Nkeys)
            {
                if (u.Nkey == noAuthUser) return null;
            }
        }
        return new InvalidOperationException(
            $"no_auth_user: \"{noAuthUser}\" not present as user or nkey in authorization block or account configuration");
    }

    /// <summary>
    /// Validates the auth section of options: pinned certs, connection types, and no_auth_user.
    /// Mirrors Go <c>validateAuth</c>.
    /// </summary>
    public static Exception? ValidateAuth(ServerOptions o)
    {
        // validatePinnedCerts will be added when the full server module is ported.
        if (o.Users != null)
        {
            foreach (var u in o.Users)
            {
                var err = ValidateAllowedConnectionTypes(u.AllowedConnectionTypes);
                if (err != null) return err;
            }
        }
        if (o.Nkeys != null)
        {
            foreach (var u in o.Nkeys)
            {
                var err = ValidateAllowedConnectionTypes(u.AllowedConnectionTypes);
                if (err != null) return err;
            }
        }
        return ValidateNoAuthUser(o, o.NoAuthUser);
    }

    /// <summary>
    /// Splits a DNS alt name into lowercase labels.
    /// Mirrors Go <c>dnsAltNameLabels</c>.
    /// </summary>
    public static string[] DnsAltNameLabels(string dnsAltName)
    {
        return dnsAltName.ToLowerInvariant().Split('.');
    }

    /// <summary>
    /// Checks if DNS alt name labels match any of the provided URLs (RFC 6125).
    /// The wildcard '*' only matches the leftmost label.
    /// Mirrors Go <c>dnsAltNameMatches</c>.
    /// </summary>
    public static bool DnsAltNameMatches(string[] dnsAltNameLabels, IReadOnlyList<Uri?> urls)
    {
        foreach (var url in urls)
        {
            if (url == null)
            {
                continue;
            }

            var hostLabels = url.Host.ToLowerInvariant().Split('.');

            // Following RFC 6125: wildcard never matches multiple labels, only leftmost.
            if (hostLabels.Length != dnsAltNameLabels.Length)
            {
                continue;
            }

            var i = 0;
            // Only match wildcard on leftmost label.
            if (dnsAltNameLabels[0] == "*")
            {
                i++;
            }

            var matched = true;
            for (; i < dnsAltNameLabels.Length; i++)
            {
                if (dnsAltNameLabels[i] != hostLabels[i])
                {
                    matched = false;
                    break;
                }
            }
            if (matched) return true;
        }
        return false;
    }

    /// <summary>
    /// Wipes a byte slice by filling with 'x'. Used for clearing sensitive data.
    /// Mirrors Go <c>wipeSlice</c>.
    /// </summary>
    public static void WipeSlice(Span<byte> buf)
    {
        JwtProcessor.WipeSlice(buf);
    }

    /// <summary>
    /// Returns the closed-client state for an auth error.
    /// Mirrors Go <c>getAuthErrClosedState</c> in server/auth.go.
    /// </summary>
    public static ClosedState GetAuthErrClosedState(Exception? err)
    {
        return err switch
        {
            AuthProxyNotTrustedException => ClosedState.ProxyNotTrusted,
            AuthProxyRequiredException   => ClosedState.ProxyRequired,
            _                            => ClosedState.AuthenticationViolation,
        };
    }

    /// <summary>
    /// Validates that proxy protocol configuration is consistent.
    /// If <see cref="ServerOptions.ProxyRequired"/> is set, <see cref="ServerOptions.ProxyProtocol"/> must also be enabled.
    /// Note: Full NKey-format validation of trusted proxy keys is deferred until proxy auth is fully implemented.
    /// Partially mirrors Go <c>validateProxies</c> in server/auth.go.
    /// </summary>
    public static Exception? ValidateProxies(ServerOptions opts)
    {
        if (opts.ProxyRequired && !opts.ProxyProtocol)
            return new InvalidOperationException("proxy_required requires proxy_protocol to be enabled");
        return null;
    }

    /// <summary>
    /// Extracts the DC= attribute values from a certificate's distinguished name.
    /// Mirrors Go <c>getTLSAuthDCs</c> in server/auth.go.
    /// </summary>
    public static string GetTlsAuthDcs(System.Security.Cryptography.X509Certificates.X509Certificate2 cert)
    {
        var subject = cert.Subject;
        var dcs = new System.Text.StringBuilder();
        foreach (var part in subject.Split(','))
        {
            var trimmed = part.Trim();
            if (trimmed.StartsWith("DC=", StringComparison.OrdinalIgnoreCase))
            {
                if (dcs.Length > 0) dcs.Append('.');
                dcs.Append(trimmed[3..]);
            }
        }
        return dcs.ToString();
    }

    /// <summary>
    /// Checks whether a client's TLS certificate subject matches using the provided matcher function.
    /// Mirrors Go <c>checkClientTLSCertSubject</c> in server/auth.go.
    /// </summary>
    public static bool CheckClientTlsCertSubject(
        System.Security.Cryptography.X509Certificates.X509Certificate2? cert,
        Func<string, bool> matcher)
    {
        if (cert == null) return false;
        return matcher(cert.Subject);
    }

    /// <summary>
    /// Expands template variables ({{account}}, {{tag.*}}) in JWT user permission limits.
    /// Mirrors Go <c>processUserPermissionsTemplate</c> in server/auth.go.
    /// </summary>
    public static (Permissions Result, Exception? Error) ProcessUserPermissionsTemplate(
        Permissions lim,
        string accountName,
        Dictionary<string, string>? tags)
    {
        ExpandSubjectList(lim.Publish?.Allow, accountName, tags);
        ExpandSubjectList(lim.Publish?.Deny, accountName, tags);
        ExpandSubjectList(lim.Subscribe?.Allow, accountName, tags);
        ExpandSubjectList(lim.Subscribe?.Deny, accountName, tags);
        return (lim, null);
    }

    private static readonly Regex TemplateVar =
        new(@"\{\{(\w+(?:\.\w+)*)\}\}", RegexOptions.Compiled);

    private static void ExpandSubjectList(List<string>? subjects, string accountName, Dictionary<string, string>? tags)
    {
        if (subjects == null) return;
        for (var i = 0; i < subjects.Count; i++)
            subjects[i] = ExpandTemplate(subjects[i], accountName, tags);
    }

    private static string ExpandTemplate(string subject, string accountName, Dictionary<string, string>? tags)
    {
        return TemplateVar.Replace(subject, m =>
        {
            var key = m.Groups[1].Value;
            if (key.Equals("account", StringComparison.OrdinalIgnoreCase)) return accountName;
            if (key.StartsWith("tag.", StringComparison.OrdinalIgnoreCase) && tags != null)
            {
                var tagKey = key[4..];
                return tags.TryGetValue(tagKey, out var v) ? v : m.Value;
            }
            return m.Value;
        });
    }
}
