// 
// Decompiled by Procyon v0.6.0
// 

package com.hypixel.hytale.server.core.auth;

import java.util.UUID;
import java.util.stream.Stream;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.Objects;
import java.util.List;
import java.util.ArrayList;
import java.util.Iterator;
import com.nimbusds.jose.JWSVerifier;
import com.nimbusds.jose.crypto.Ed25519Verifier;
import com.nimbusds.jose.jwk.OctetKeyPair;
import com.nimbusds.jose.jwk.JWK;
import java.util.Map;
import com.nimbusds.jwt.JWTClaimsSet;
import java.text.ParseException;
import java.time.Instant;
import com.nimbusds.jwt.SignedJWT;
import java.util.logging.Level;
import java.security.cert.X509Certificate;
import javax.annotation.Nullable;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nonnull;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.locks.ReentrantLock;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.JWSAlgorithm;
import com.hypixel.hytale.logger.HytaleLogger;

public class JWTValidator
{
    private static final HytaleLogger LOGGER;
    private static final long CLOCK_SKEW_SECONDS = 300L;
    private static final JWSAlgorithm SUPPORTED_ALGORITHM;
    private static final int MIN_SIGNATURE_LENGTH = 80;
    private static final int MAX_SIGNATURE_LENGTH = 90;
    private final SessionServiceClient sessionServiceClient;
    private final String expectedIssuer;
    private final String expectedAudience;
    private volatile JWKSet cachedJwkSet;
    private volatile long jwksCacheExpiry;
    private final long jwksCacheDurationMs;
    private final ReentrantLock jwksFetchLock;
    private volatile CompletableFuture<JWKSet> pendingFetch;
    
    public JWTValidator(@Nonnull final SessionServiceClient sessionServiceClient, @Nonnull final String expectedIssuer, @Nonnull final String expectedAudience) {
        this.jwksCacheDurationMs = TimeUnit.HOURS.toMillis(1L);
        this.jwksFetchLock = new ReentrantLock();
        this.pendingFetch = null;
        this.sessionServiceClient = sessionServiceClient;
        this.expectedIssuer = expectedIssuer;
        this.expectedAudience = expectedAudience;
    }
    
    @Nullable
    private static String validateJwtStructure(@Nonnull final String token, @Nonnull final String tokenType) {
        if (token.isEmpty()) {
            return tokenType + " is empty";
        }
        final String[] parts = token.split("\\.", -1);
        if (parts.length != 3) {
            return String.format("%s has invalid format (expected 3 parts, got %d)", tokenType, parts.length);
        }
        if (parts[2].isEmpty()) {
            return tokenType + " has empty signature - possible signature stripping attack";
        }
        final int sigLen = parts[2].length();
        if (sigLen < 80 || sigLen > 90) {
            return String.format("%s has invalid signature length: %d (expected %d-%d)", tokenType, sigLen, 80, 90);
        }
        return null;
    }
    
    @Nullable
    public JWTClaims validateToken(@Nonnull final String accessToken, @Nullable final X509Certificate clientCert) {
        final String structError = validateJwtStructure(accessToken, "Access token");
        if (structError != null) {
            JWTValidator.LOGGER.at(Level.WARNING).log(structError);
            return null;
        }
        try {
            final SignedJWT signedJWT = SignedJWT.parse(accessToken);
            final JWSAlgorithm algorithm = signedJWT.getHeader().getAlgorithm();
            if (!JWTValidator.SUPPORTED_ALGORITHM.equals(algorithm)) {
                JWTValidator.LOGGER.at(Level.WARNING).log("Unsupported JWT algorithm: %s (expected EdDSA)", algorithm);
                return null;
            }
            if (!this.verifySignatureWithRetry(signedJWT)) {
                JWTValidator.LOGGER.at(Level.WARNING).log("JWT signature verification failed");
                return null;
            }
            final JWTClaimsSet claimsSet = signedJWT.getJWTClaimsSet();
            final JWTClaims claims = new JWTClaims();
            claims.issuer = claimsSet.getIssuer();
            claims.audience = ((claimsSet.getAudience() != null && !claimsSet.getAudience().isEmpty()) ? claimsSet.getAudience().get(0) : null);
            claims.subject = claimsSet.getSubject();
            claims.username = claimsSet.getStringClaim("username");
            claims.ipAddress = claimsSet.getStringClaim("ip");
            claims.issuedAt = ((claimsSet.getIssueTime() != null) ? Long.valueOf(claimsSet.getIssueTime().toInstant().getEpochSecond()) : null);
            claims.expiresAt = ((claimsSet.getExpirationTime() != null) ? Long.valueOf(claimsSet.getExpirationTime().toInstant().getEpochSecond()) : null);
            claims.notBefore = ((claimsSet.getNotBeforeTime() != null) ? Long.valueOf(claimsSet.getNotBeforeTime().toInstant().getEpochSecond()) : null);
            final Map<String, Object> cnfClaim = claimsSet.getJSONObjectClaim("cnf");
            if (cnfClaim != null) {
                claims.certificateFingerprint = cnfClaim.get("x5t#S256");
            }
            if (!this.expectedIssuer.equals(claims.issuer)) {
                JWTValidator.LOGGER.at(Level.WARNING).log("Invalid issuer: expected %s, got %s", this.expectedIssuer, claims.issuer);
                return null;
            }
            if (!this.expectedAudience.equals(claims.audience)) {
                JWTValidator.LOGGER.at(Level.WARNING).log("Invalid audience: expected %s, got %s", this.expectedAudience, claims.audience);
                return null;
            }
            final long nowSeconds = Instant.now().getEpochSecond();
            if (claims.expiresAt == null) {
                JWTValidator.LOGGER.at(Level.WARNING).log("Access token missing expiration claim");
                return null;
            }
            if (nowSeconds >= claims.expiresAt + 300L) {
                JWTValidator.LOGGER.at(Level.WARNING).log("Token expired (exp: %d, now: %d)", claims.expiresAt, nowSeconds);
                return null;
            }
            if (claims.notBefore != null && nowSeconds < claims.notBefore - 300L) {
                JWTValidator.LOGGER.at(Level.WARNING).log("Token not yet valid (nbf: %d, now: %d)", claims.notBefore, nowSeconds);
                return null;
            }
            if (claims.issuedAt != null && claims.issuedAt > nowSeconds + 300L) {
                JWTValidator.LOGGER.at(Level.WARNING).log("Token issued in the future (iat: %d, now: %d)", claims.issuedAt, nowSeconds);
                return null;
            }
            if (!CertificateUtil.validateCertificateBinding(claims.certificateFingerprint, clientCert)) {
                JWTValidator.LOGGER.at(Level.WARNING).log("Certificate binding validation failed");
                return null;
            }
            if (claims.getSubjectAsUUID() == null) {
                JWTValidator.LOGGER.at(Level.WARNING).log("Access token has invalid or missing subject UUID");
                return null;
            }
            JWTValidator.LOGGER.at(Level.INFO).log("JWT validated successfully for user %s (UUID: %s)", claims.username, claims.subject);
            return claims;
        }
        catch (final ParseException e) {
            JWTValidator.LOGGER.at(Level.WARNING).withCause(e).log("Failed to parse JWT");
            return null;
        }
        catch (final Exception e2) {
            JWTValidator.LOGGER.at(Level.WARNING).withCause(e2).log("JWT validation error");
            return null;
        }
    }
    
    private boolean verifySignature(final SignedJWT signedJWT, final JWKSet jwkSet) {
        try {
            final String keyId = signedJWT.getHeader().getKeyID();
            OctetKeyPair ed25519Key = null;
            for (final JWK jwk : jwkSet.getKeys()) {
                if (jwk instanceof final OctetKeyPair okp) {
                    if (keyId == null || keyId.equals(jwk.getKeyID())) {
                        ed25519Key = okp;
                        break;
                    }
                    continue;
                }
            }
            if (ed25519Key == null) {
                JWTValidator.LOGGER.at(Level.WARNING).log("No Ed25519 key found for kid=%s", keyId);
                return false;
            }
            final Ed25519Verifier verifier = new Ed25519Verifier(ed25519Key);
            final boolean valid = signedJWT.verify(verifier);
            if (valid) {
                JWTValidator.LOGGER.at(Level.FINE).log("JWT signature verified with key kid=%s", keyId);
            }
            return valid;
        }
        catch (final Exception e) {
            JWTValidator.LOGGER.at(Level.WARNING).withCause(e).log("JWT signature verification failed");
            return false;
        }
    }
    
    @Nullable
    private JWKSet getJwkSet() {
        return this.getJwkSet(false);
    }
    
    @Nullable
    private JWKSet getJwkSet(final boolean forceRefresh) {
        final long now = System.currentTimeMillis();
        if (!forceRefresh && this.cachedJwkSet != null && now < this.jwksCacheExpiry) {
            return this.cachedJwkSet;
        }
        this.jwksFetchLock.lock();
        try {
            if (!forceRefresh && this.cachedJwkSet != null && now < this.jwksCacheExpiry) {
                return this.cachedJwkSet;
            }
            final CompletableFuture<JWKSet> existing = this.pendingFetch;
            if (existing != null && !existing.isDone()) {
                this.jwksFetchLock.unlock();
                try {
                    return existing.join();
                }
                finally {
                    this.jwksFetchLock.lock();
                }
            }
            if (forceRefresh) {
                JWTValidator.LOGGER.at(Level.INFO).log("Force refreshing JWKS cache (key rotation or verification failure)");
            }
            this.pendingFetch = CompletableFuture.supplyAsync(this::fetchJwksFromService);
        }
        finally {
            this.jwksFetchLock.unlock();
        }
        return this.pendingFetch.join();
    }
    
    @Nullable
    private JWKSet fetchJwksFromService() {
        final SessionServiceClient.JwksResponse jwksResponse = this.sessionServiceClient.getJwks();
        if (jwksResponse == null || jwksResponse.keys == null || jwksResponse.keys.length == 0) {
            JWTValidator.LOGGER.at(Level.WARNING).log("Failed to fetch JWKS or no keys available");
            return this.cachedJwkSet;
        }
        try {
            final ArrayList<JWK> jwkList = new ArrayList<JWK>();
            for (final SessionServiceClient.JwkKey key : jwksResponse.keys) {
                final JWK jwk = this.convertToJWK(key);
                if (jwk != null) {
                    jwkList.add(jwk);
                }
            }
            if (jwkList.isEmpty()) {
                JWTValidator.LOGGER.at(Level.WARNING).log("No valid JWKs found in JWKS response");
                return this.cachedJwkSet;
            }
            final JWKSet newSet = new JWKSet(jwkList);
            this.cachedJwkSet = newSet;
            this.jwksCacheExpiry = System.currentTimeMillis() + this.jwksCacheDurationMs;
            JWTValidator.LOGGER.at(Level.INFO).log("JWKS loaded with %d keys", jwkList.size());
            return newSet;
        }
        catch (final Exception e) {
            JWTValidator.LOGGER.at(Level.WARNING).withCause(e).log("Failed to parse JWKS");
            return this.cachedJwkSet;
        }
    }
    
    private boolean verifySignatureWithRetry(final SignedJWT signedJWT) {
        final JWKSet jwkSet = this.getJwkSet();
        if (jwkSet == null) {
            return false;
        }
        if (this.verifySignature(signedJWT, jwkSet)) {
            return true;
        }
        JWTValidator.LOGGER.at(Level.INFO).log("Signature verification failed with cached JWKS, retrying with fresh keys");
        final JWKSet freshJwkSet = this.getJwkSet(true);
        return freshJwkSet != null && freshJwkSet != jwkSet && this.verifySignature(signedJWT, freshJwkSet);
    }
    
    @Nullable
    private JWK convertToJWK(final SessionServiceClient.JwkKey key) {
        if (!"OKP".equals(key.kty)) {
            JWTValidator.LOGGER.at(Level.WARNING).log("Unsupported key type: %s (expected OKP)", key.kty);
            return null;
        }
        try {
            final String json = String.format("{\"kty\":\"OKP\",\"crv\":\"%s\",\"x\":\"%s\",\"kid\":\"%s\",\"alg\":\"EdDSA\"}", key.crv, key.x, key.kid);
            return JWK.parse(json);
        }
        catch (final Exception e) {
            JWTValidator.LOGGER.at(Level.WARNING).withCause(e).log("Failed to parse Ed25519 key");
            return null;
        }
    }
    
    public void invalidateJwksCache() {
        this.jwksFetchLock.lock();
        try {
            this.cachedJwkSet = null;
            this.jwksCacheExpiry = 0L;
            this.pendingFetch = null;
        }
        finally {
            this.jwksFetchLock.unlock();
        }
    }
    
    @Nullable
    public IdentityTokenClaims validateIdentityToken(@Nonnull final String identityToken) {
        final String structError = validateJwtStructure(identityToken, "Identity token");
        if (structError != null) {
            JWTValidator.LOGGER.at(Level.WARNING).log(structError);
            return null;
        }
        try {
            final SignedJWT signedJWT = SignedJWT.parse(identityToken);
            final JWSAlgorithm algorithm = signedJWT.getHeader().getAlgorithm();
            if (!JWTValidator.SUPPORTED_ALGORITHM.equals(algorithm)) {
                JWTValidator.LOGGER.at(Level.WARNING).log("Unsupported identity token algorithm: %s (expected EdDSA)", algorithm);
                return null;
            }
            if (!this.verifySignatureWithRetry(signedJWT)) {
                JWTValidator.LOGGER.at(Level.WARNING).log("Identity token signature verification failed");
                return null;
            }
            final JWTClaimsSet claimsSet = signedJWT.getJWTClaimsSet();
            final IdentityTokenClaims claims = new IdentityTokenClaims();
            claims.issuer = claimsSet.getIssuer();
            claims.subject = claimsSet.getSubject();
            claims.issuedAt = ((claimsSet.getIssueTime() != null) ? Long.valueOf(claimsSet.getIssueTime().toInstant().getEpochSecond()) : null);
            claims.expiresAt = ((claimsSet.getExpirationTime() != null) ? Long.valueOf(claimsSet.getExpirationTime().toInstant().getEpochSecond()) : null);
            claims.notBefore = ((claimsSet.getNotBeforeTime() != null) ? Long.valueOf(claimsSet.getNotBeforeTime().toInstant().getEpochSecond()) : null);
            claims.scope = claimsSet.getStringClaim("scope");
            final Map<String, Object> profile = claimsSet.getJSONObjectClaim("profile");
            if (profile != null) {
                claims.username = profile.get("username");
                claims.skin = profile.get("skin");
                final Object entitlements = profile.get("entitlements");
                if (entitlements instanceof List) {
                    final List<?> list = (List<?>)entitlements;
                    final IdentityTokenClaims identityTokenClaims = claims;
                    final Stream<Object> stream = list.stream();
                    final Class<String> obj = String.class;
                    Objects.requireNonNull(obj);
                    final Stream<Object> filter = stream.filter(obj::isInstance);
                    final Class<String> obj2 = String.class;
                    Objects.requireNonNull(obj2);
                    identityTokenClaims.entitlements = filter.map((Function<? super Object, ?>)obj2::cast).toArray(String[]::new);
                }
            }
            if (!this.expectedIssuer.equals(claims.issuer)) {
                JWTValidator.LOGGER.at(Level.WARNING).log("Invalid identity token issuer: expected %s, got %s", this.expectedIssuer, claims.issuer);
                return null;
            }
            final long nowSeconds = Instant.now().getEpochSecond();
            if (claims.expiresAt == null) {
                JWTValidator.LOGGER.at(Level.WARNING).log("Identity token missing expiration claim");
                return null;
            }
            if (nowSeconds >= claims.expiresAt + 300L) {
                JWTValidator.LOGGER.at(Level.WARNING).log("Identity token expired (exp: %d, now: %d)", claims.expiresAt, nowSeconds);
                return null;
            }
            if (claims.notBefore != null && nowSeconds < claims.notBefore - 300L) {
                JWTValidator.LOGGER.at(Level.WARNING).log("Identity token not yet valid (nbf: %d, now: %d)", claims.notBefore, nowSeconds);
                return null;
            }
            if (claims.issuedAt != null && claims.issuedAt > nowSeconds + 300L) {
                JWTValidator.LOGGER.at(Level.WARNING).log("Identity token issued in the future (iat: %d, now: %d)", claims.issuedAt, nowSeconds);
                return null;
            }
            if (claims.getSubjectAsUUID() == null) {
                JWTValidator.LOGGER.at(Level.WARNING).log("Identity token has invalid or missing subject UUID");
                return null;
            }
            JWTValidator.LOGGER.at(Level.INFO).log("Identity token validated successfully for user %s (UUID: %s)", claims.username, claims.subject);
            return claims;
        }
        catch (final ParseException e) {
            JWTValidator.LOGGER.at(Level.WARNING).withCause(e).log("Failed to parse identity token");
            return null;
        }
        catch (final Exception e2) {
            JWTValidator.LOGGER.at(Level.WARNING).withCause(e2).log("Identity token validation error");
            return null;
        }
    }
    
    @Nullable
    public SessionTokenClaims validateSessionToken(@Nonnull final String sessionToken) {
        final String structError = validateJwtStructure(sessionToken, "Session token");
        if (structError != null) {
            JWTValidator.LOGGER.at(Level.WARNING).log(structError);
            return null;
        }
        try {
            final SignedJWT signedJWT = SignedJWT.parse(sessionToken);
            final JWSAlgorithm algorithm = signedJWT.getHeader().getAlgorithm();
            if (!JWTValidator.SUPPORTED_ALGORITHM.equals(algorithm)) {
                JWTValidator.LOGGER.at(Level.WARNING).log("Unsupported session token algorithm: %s (expected EdDSA)", algorithm);
                return null;
            }
            if (!this.verifySignatureWithRetry(signedJWT)) {
                JWTValidator.LOGGER.at(Level.WARNING).log("Session token signature verification failed");
                return null;
            }
            final JWTClaimsSet claimsSet = signedJWT.getJWTClaimsSet();
            final SessionTokenClaims claims = new SessionTokenClaims();
            claims.issuer = claimsSet.getIssuer();
            claims.subject = claimsSet.getSubject();
            claims.issuedAt = ((claimsSet.getIssueTime() != null) ? Long.valueOf(claimsSet.getIssueTime().toInstant().getEpochSecond()) : null);
            claims.expiresAt = ((claimsSet.getExpirationTime() != null) ? Long.valueOf(claimsSet.getExpirationTime().toInstant().getEpochSecond()) : null);
            claims.notBefore = ((claimsSet.getNotBeforeTime() != null) ? Long.valueOf(claimsSet.getNotBeforeTime().toInstant().getEpochSecond()) : null);
            claims.scope = claimsSet.getStringClaim("scope");
            if (!this.expectedIssuer.equals(claims.issuer)) {
                JWTValidator.LOGGER.at(Level.WARNING).log("Invalid session token issuer: expected %s, got %s", this.expectedIssuer, claims.issuer);
                return null;
            }
            final long nowSeconds = Instant.now().getEpochSecond();
            if (claims.expiresAt == null) {
                JWTValidator.LOGGER.at(Level.WARNING).log("Session token missing expiration claim");
                return null;
            }
            if (nowSeconds >= claims.expiresAt + 300L) {
                JWTValidator.LOGGER.at(Level.WARNING).log("Session token expired (exp: %d, now: %d)", claims.expiresAt, nowSeconds);
                return null;
            }
            if (claims.notBefore != null && nowSeconds < claims.notBefore - 300L) {
                JWTValidator.LOGGER.at(Level.WARNING).log("Session token not yet valid (nbf: %d, now: %d)", claims.notBefore, nowSeconds);
                return null;
            }
            if (claims.issuedAt != null && claims.issuedAt > nowSeconds + 300L) {
                JWTValidator.LOGGER.at(Level.WARNING).log("Session token issued in the future (iat: %d, now: %d)", claims.issuedAt, nowSeconds);
                return null;
            }
            if (claims.getSubjectAsUUID() == null) {
                JWTValidator.LOGGER.at(Level.WARNING).log("Session token has invalid or missing subject UUID");
                return null;
            }
            JWTValidator.LOGGER.at(Level.INFO).log("Session token validated successfully (UUID: %s)", claims.subject);
            return claims;
        }
        catch (final ParseException e) {
            JWTValidator.LOGGER.at(Level.WARNING).withCause(e).log("Failed to parse session token");
            return null;
        }
        catch (final Exception e2) {
            JWTValidator.LOGGER.at(Level.WARNING).withCause(e2).log("Session token validation error");
            return null;
        }
    }
    
    static {
        LOGGER = HytaleLogger.forEnclosingClass();
        SUPPORTED_ALGORITHM = JWSAlgorithm.EdDSA;
    }
    
    public static class SessionTokenClaims
    {
        public String issuer;
        public String subject;
        public Long issuedAt;
        public Long expiresAt;
        public Long notBefore;
        public String scope;
        
        @Nullable
        public UUID getSubjectAsUUID() {
            if (this.subject == null) {
                return null;
            }
            try {
                return UUID.fromString(this.subject);
            }
            catch (final IllegalArgumentException e) {
                return null;
            }
        }
        
        @Nonnull
        public String[] getScopes() {
            if (this.scope == null || this.scope.isEmpty()) {
                return new String[0];
            }
            return this.scope.split(" ");
        }
        
        public boolean hasScope(@Nonnull final String targetScope) {
            for (final String s : this.getScopes()) {
                if (s.equals(targetScope)) {
                    return true;
                }
            }
            return false;
        }
    }
    
    public static class IdentityTokenClaims
    {
        public String issuer;
        public String subject;
        public String username;
        public String[] entitlements;
        public String skin;
        public Long issuedAt;
        public Long expiresAt;
        public Long notBefore;
        public String scope;
        
        @Nullable
        public UUID getSubjectAsUUID() {
            if (this.subject == null) {
                return null;
            }
            try {
                return UUID.fromString(this.subject);
            }
            catch (final IllegalArgumentException e) {
                return null;
            }
        }
        
        @Nonnull
        public String[] getScopes() {
            if (this.scope == null || this.scope.isEmpty()) {
                return new String[0];
            }
            return this.scope.split(" ");
        }
        
        public boolean hasScope(@Nonnull final String targetScope) {
            for (final String s : this.getScopes()) {
                if (s.equals(targetScope)) {
                    return true;
                }
            }
            return false;
        }
    }
    
    public static class JWTClaims
    {
        public String issuer;
        public String audience;
        public String subject;
        public String username;
        public String ipAddress;
        public Long issuedAt;
        public Long expiresAt;
        public Long notBefore;
        public String certificateFingerprint;
        
        @Nullable
        public UUID getSubjectAsUUID() {
            if (this.subject == null) {
                return null;
            }
            try {
                return UUID.fromString(this.subject);
            }
            catch (final IllegalArgumentException e) {
                return null;
            }
        }
    }
}
