using NATS.Server.Auth.Jwt;

namespace NATS.Server.Auth;

/// <summary>
/// Authenticator for JWT-based client connections.
/// Decodes user JWT, resolves account, verifies signature, checks revocation.
/// Reference: Go auth.go:588+ processClientOrLeafAuthentication.
/// </summary>
public sealed class JwtAuthenticator : IAuthenticator
{
    private readonly string[] _trustedKeys;
    private readonly IAccountResolver _resolver;

    public JwtAuthenticator(string[] trustedKeys, IAccountResolver resolver)
    {
        _trustedKeys = trustedKeys;
        _resolver = resolver;
    }

    public AuthResult? Authenticate(ClientAuthContext context)
    {
        var jwt = context.Opts.JWT;
        if (string.IsNullOrEmpty(jwt) || !NatsJwt.IsJwt(jwt))
            return null;

        // 1. Decode user claims
        var userClaims = NatsJwt.DecodeUserClaims(jwt);
        if (userClaims is null)
            return null;

        // 2. Check expiry
        if (userClaims.IsExpired())
            return null;

        // 3. Resolve issuing account
        var issuerAccount = !string.IsNullOrEmpty(userClaims.IssuerAccount)
            ? userClaims.IssuerAccount
            : userClaims.Issuer;

        if (string.IsNullOrEmpty(issuerAccount))
            return null;

        var accountJwt = _resolver.FetchAsync(issuerAccount).GetAwaiter().GetResult();
        if (accountJwt is null)
            return null;

        var accountClaims = NatsJwt.DecodeAccountClaims(accountJwt);
        if (accountClaims is null)
            return null;

        // 4. Verify account issuer is trusted
        if (!IsTrusted(accountClaims.Issuer))
            return null;

        // 5. Verify user JWT issuer is the account or a signing key
        var userIssuer = userClaims.Issuer;
        if (userIssuer != accountClaims.Subject)
        {
            // Check if issuer is a signing key of the account
            var signingKeys = accountClaims.Nats?.SigningKeys;
            if (signingKeys is null || !signingKeys.Contains(userIssuer))
                return null;
        }

        // 6. Verify nonce signature (unless bearer token)
        if (!userClaims.BearerToken)
        {
            if (context.Nonce is null || string.IsNullOrEmpty(context.Opts.Sig))
                return null;

            var userNkey = userClaims.Subject ?? context.Opts.Nkey;
            if (string.IsNullOrEmpty(userNkey))
                return null;

            if (!NatsJwt.VerifyNonce(context.Nonce, context.Opts.Sig, userNkey))
                return null;
        }

        // 7. Check user revocation
        var revocations = accountClaims.Nats?.Revocations;
        if (revocations is not null && userClaims.Subject is not null)
        {
            if (revocations.TryGetValue(userClaims.Subject, out var revokedAt))
            {
                if (userClaims.IssuedAt <= revokedAt)
                    return null;
            }

            // Check wildcard revocation
            if (revocations.TryGetValue("*", out revokedAt))
            {
                if (userClaims.IssuedAt <= revokedAt)
                    return null;
            }
        }

        // 7b. Check allowed connection types
        var (allowedTypes, hasUnknown) = JwtConnectionTypes.Convert(userClaims.Nats?.AllowedConnectionTypes);

        if (allowedTypes.Count == 0)
        {
            if (hasUnknown)
                return null; // unknown-only list should reject
        }
        else
        {
            var connType = string.IsNullOrWhiteSpace(context.ConnectionType)
                ? JwtConnectionTypes.Standard
                : context.ConnectionType.ToUpperInvariant();

            if (!allowedTypes.Contains(connType))
                return null;
        }

        // 8. Build permissions from JWT claims
        Permissions? permissions = null;
        var nats = userClaims.Nats;
        if (nats is not null)
        {
            var pubAllow = nats.Pub?.Allow;
            var pubDeny = nats.Pub?.Deny;
            var subAllow = nats.Sub?.Allow;
            var subDeny = nats.Sub?.Deny;

            // Expand permission templates
            var name = userClaims.Name ?? "";
            var subject = userClaims.Subject ?? "";
            var acctName = accountClaims.Name ?? "";
            var acctSubject = accountClaims.Subject ?? "";
            var userTags = nats.Tags ?? [];
            var acctTags = accountClaims.Nats?.Tags ?? [];

            if (pubAllow is { Length: > 0 })
                pubAllow = PermissionTemplates.ExpandAll(pubAllow, name, subject, acctName, acctSubject, userTags, acctTags).ToArray();
            if (pubDeny is { Length: > 0 })
                pubDeny = PermissionTemplates.ExpandAll(pubDeny, name, subject, acctName, acctSubject, userTags, acctTags).ToArray();
            if (subAllow is { Length: > 0 })
                subAllow = PermissionTemplates.ExpandAll(subAllow, name, subject, acctName, acctSubject, userTags, acctTags).ToArray();
            if (subDeny is { Length: > 0 })
                subDeny = PermissionTemplates.ExpandAll(subDeny, name, subject, acctName, acctSubject, userTags, acctTags).ToArray();

            if (pubAllow is not null || pubDeny is not null || subAllow is not null || subDeny is not null)
            {
                permissions = new Permissions
                {
                    Publish = (pubAllow is not null || pubDeny is not null)
                        ? new SubjectPermission { Allow = pubAllow, Deny = pubDeny }
                        : null,
                    Subscribe = (subAllow is not null || subDeny is not null)
                        ? new SubjectPermission { Allow = subAllow, Deny = subDeny }
                        : null,
                };
            }
        }

        // 9. Build result
        return new AuthResult
        {
            Identity = userClaims.Subject ?? "",
            AccountName = issuerAccount,
            Permissions = permissions,
            Expiry = userClaims.GetExpiry(),
            MaxJetStreamStreams = accountClaims.Nats?.JetStream?.MaxStreams ?? 0,
            JetStreamTier = accountClaims.Nats?.JetStream?.Tier,
        };
    }

    private bool IsTrusted(string? issuer)
    {
        if (string.IsNullOrEmpty(issuer)) return false;
        foreach (var key in _trustedKeys)
        {
            if (key == issuer)
                return true;
        }

        return false;
    }
}
