using System.Security.Cryptography;
using NATS.Server.Protocol;

namespace NATS.Server.Auth;

/// <summary>
/// Central authentication orchestrator that builds the appropriate authenticators
/// from NatsOptions and tries them in priority order matching the Go server:
/// NKeys > Users > Token > SimpleUserPassword.
/// Reference: golang/nats-server/server/auth.go — checkClientAuth, configureAuthentication.
/// </summary>
public sealed class AuthService
{
    private readonly List<IAuthenticator> _authenticators;
    private readonly string? _noAuthUser;
    private readonly Dictionary<string, User>? _usersMap;

    public bool IsAuthRequired { get; }
    public bool NonceRequired { get; }

    private AuthService(List<IAuthenticator> authenticators, bool authRequired, bool nonceRequired,
        string? noAuthUser, Dictionary<string, User>? usersMap)
    {
        _authenticators = authenticators;
        IsAuthRequired = authRequired;
        NonceRequired = nonceRequired;
        _noAuthUser = noAuthUser;
        _usersMap = usersMap;
    }

    public static AuthService Build(NatsOptions options)
    {
        var authenticators = new List<IAuthenticator>();
        var authRequired = false;
        var nonceRequired = false;
        Dictionary<string, User>? usersMap = null;
        var users = NormalizeUsers(options.Users);
        var nkeys = NormalizeNKeys(options.NKeys);

        // TLS certificate mapping (highest priority when enabled)
        if (options.TlsMap && options.TlsVerify && users is { Count: > 0 })
        {
            authenticators.Add(new TlsMapAuthenticator(users));
            authRequired = true;
        }

        // JWT / Operator mode (highest priority after TLS)
        if (options.TrustedKeys is { Length: > 0 } && options.AccountResolver is not null)
        {
            authenticators.Add(new JwtAuthenticator(options.TrustedKeys, options.AccountResolver));
            authRequired = true;
            nonceRequired = true;
        }

        if (options.ExternalAuth is { Enabled: true, Client: not null } externalAuth)
        {
            authenticators.Add(new ExternalAuthCalloutAuthenticator(externalAuth.Client, externalAuth.Timeout));
            authRequired = true;
        }

        if (options.ProxyAuth is { Enabled: true } proxyAuth)
        {
            authenticators.Add(new ProxyAuthenticator(proxyAuth));
            authRequired = true;
        }

        // Priority order (matching Go): NKeys > Users > Token > SimpleUserPassword

        if (nkeys is { Count: > 0 })
        {
            authenticators.Add(new NKeyAuthenticator(nkeys));
            authRequired = true;
            nonceRequired = true;
        }

        if (users is { Count: > 0 })
        {
            authenticators.Add(new UserPasswordAuthenticator(users));
            authRequired = true;
            usersMap = new Dictionary<string, User>(StringComparer.Ordinal);
            foreach (var u in users)
                usersMap[u.Username] = u;
        }

        if (!string.IsNullOrEmpty(options.Authorization))
        {
            authenticators.Add(new TokenAuthenticator(options.Authorization));
            authRequired = true;
        }

        if (!string.IsNullOrEmpty(options.Username) && !string.IsNullOrEmpty(options.Password))
        {
            authenticators.Add(new SimpleUserPasswordAuthenticator(options.Username, options.Password));
            authRequired = true;
        }

        return new AuthService(authenticators, authRequired, nonceRequired, options.NoAuthUser, usersMap);
    }

    public AuthResult? Authenticate(ClientAuthContext context)
    {
        if (!IsAuthRequired)
            return new AuthResult { Identity = string.Empty };

        foreach (var authenticator in _authenticators)
        {
            var result = authenticator.Authenticate(context);
            if (result != null)
                return result;
        }

        if (_noAuthUser != null && IsNoCredentials(context))
            return ResolveNoAuthUser();

        return null;
    }

    private static bool IsNoCredentials(ClientAuthContext context)
    {
        var opts = context.Opts;
        return string.IsNullOrEmpty(opts.Username)
            && string.IsNullOrEmpty(opts.Password)
            && string.IsNullOrEmpty(opts.Token)
            && string.IsNullOrEmpty(opts.Nkey)
            && string.IsNullOrEmpty(opts.Sig)
            && string.IsNullOrEmpty(opts.JWT);
    }

    private AuthResult? ResolveNoAuthUser()
    {
        if (_noAuthUser == null)
            return null;

        if (_usersMap != null && _usersMap.TryGetValue(_noAuthUser, out var user))
        {
            return new AuthResult
            {
                Identity = user.Username,
                AccountName = user.Account,
                Permissions = user.Permissions,
                Expiry = user.ConnectionDeadline,
            };
        }

        return new AuthResult { Identity = _noAuthUser };
    }

    public byte[] GenerateNonce()
    {
        Span<byte> raw = stackalloc byte[11];
        RandomNumberGenerator.Fill(raw);
        return raw.ToArray();
    }

    public static bool ValidateMqttCredentials(
        string? configuredUsername,
        string? configuredPassword,
        string? providedUsername,
        string? providedPassword)
    {
        if (string.IsNullOrEmpty(configuredUsername) && string.IsNullOrEmpty(configuredPassword))
            return true;

        return string.Equals(configuredUsername, providedUsername, StringComparison.Ordinal)
            && string.Equals(configuredPassword, providedPassword, StringComparison.Ordinal);
    }

    public string EncodeNonce(byte[] nonce)
    {
        return Convert.ToBase64String(nonce)
            .TrimEnd('=')
            .Replace('+', '-')
            .Replace('/', '_');
    }

    private static IReadOnlyList<User>? NormalizeUsers(IReadOnlyList<User>? users)
    {
        if (users is null)
            return null;

        var normalized = new List<User>(users.Count);
        foreach (var user in users)
        {
            normalized.Add(new User
            {
                Username = user.Username,
                Password = user.Password,
                Account = string.IsNullOrWhiteSpace(user.Account) ? Account.GlobalAccountName : user.Account,
                Permissions = NormalizePermissions(user.Permissions),
                ConnectionDeadline = user.ConnectionDeadline,
                AllowedConnectionTypes = user.AllowedConnectionTypes,
                ProxyRequired = user.ProxyRequired,
            });
        }

        return normalized;
    }

    private static IReadOnlyList<NKeyUser>? NormalizeNKeys(IReadOnlyList<NKeyUser>? nkeys)
    {
        if (nkeys is null)
            return null;

        var normalized = new List<NKeyUser>(nkeys.Count);
        foreach (var nkey in nkeys)
        {
            normalized.Add(new NKeyUser
            {
                Nkey = nkey.Nkey,
                Account = string.IsNullOrWhiteSpace(nkey.Account) ? Account.GlobalAccountName : nkey.Account,
                Permissions = NormalizePermissions(nkey.Permissions),
                SigningKey = nkey.SigningKey,
                Issued = nkey.Issued,
                AllowedConnectionTypes = nkey.AllowedConnectionTypes,
                ProxyRequired = nkey.ProxyRequired,
            });
        }

        return normalized;
    }

    private static Permissions? NormalizePermissions(Permissions? permissions)
    {
        if (permissions?.Response is null)
            return permissions;

        var publish = permissions.Publish;
        if (publish?.Allow is null)
        {
            publish = new SubjectPermission
            {
                Allow = [],
                Deny = publish?.Deny,
            };
        }

        var response = permissions.Response;
        var maxMsgs = response.MaxMsgs == 0 ? NatsProtocol.DefaultAllowResponseMaxMsgs : response.MaxMsgs;
        var expires = response.Expires == TimeSpan.Zero ? NatsProtocol.DefaultAllowResponseExpiration : response.Expires;

        return new Permissions
        {
            Publish = publish,
            Subscribe = permissions.Subscribe,
            Response = new ResponsePermission { MaxMsgs = maxMsgs, Expires = expires },
        };
    }
}
