package com.airlock.iam.oauth2.domain.service.clientcredentialsgrant;

import com.airlock.iam.base.misc.time.RealTimeProvider;
import com.airlock.iam.base.misc.time.TimeProvider;
import com.airlock.iam.common.domain.model.jwt.signature.JsonWebSignatureToken;
import com.airlock.iam.common.domain.service.jwt.signature.JwtSignatureVerifier;
import com.airlock.iam.common.domain.service.jwt.signature.JwtSignatureVerifierException;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.oauth2.api.domain.model.OAuth2ClientId;
import com.airlock.iam.oauth2.api.domain.model.OAuth2Scope;
import com.airlock.iam.oauth2.domain.model.token.OAuth2ClientCredentialsGrantAccessToken;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.annotation.Nonnull;
import java.text.ParseException;
import java.util.Date;
import java.util.Optional;
import java.util.Set;
import lombok.NonNull;
import org.apache.commons.lang3.StringUtils;

public final class DefaultOAuth2ClientCredentialsGrantTokenReader implements OAuth2ClientCredentialsGrantTokenReader {
   private static final String SCOPE_CLAIM = "scope";
   private static final String INVALID_TOKEN_MESSAGE = "Invalid OAuth 2.0 Client Credentials Grant Access Token used.";
   @NonNull
   private final ELogger logger;
   @NonNull
   private final JwtSignatureVerifier signatureVerifier;
   @NonNull
   private final TimeProvider timeProvider;

   private DefaultOAuth2ClientCredentialsGrantTokenReader(@Nonnull ELogger logger, @Nonnull JwtSignatureVerifier signatureVerifier, TimeProvider timeProvider) {
      this.logger = logger;
      this.signatureVerifier = signatureVerifier;
      this.timeProvider = timeProvider == null ? new RealTimeProvider() : timeProvider;
   }

   @Override
   public Optional<OAuth2ClientCredentialsGrantAccessToken> clientCredentialsGrantAccessToken(String tokenString) {
      try {
         Optional<JsonWebSignatureToken> jwsToken = JsonWebSignatureToken.parse(tokenString);
         if (jwsToken.isEmpty()) {
            this.logger.info("Invalid OAuth 2.0 Client Credentials Grant Access Token used. Invalid token.");
            return Optional.empty();
         } else {
            boolean signatureValid = this.signatureVerifier.isSignatureValid(jwsToken.get());
            if (!signatureValid) {
               this.logger.info("Invalid OAuth 2.0 Client Credentials Grant Access Token used. Invalid signature.");
               return Optional.empty();
            } else {
               SignedJWT parse = SignedJWT.parse(tokenString);
               JWTClaimsSet jwtClaimsSet = parse.getJWTClaimsSet();
               return this.accessTokenFromClaimSet(jwtClaimsSet, tokenString).flatMap(this::verifiedExpiration);
            }
         }
      } catch (JwtSignatureVerifierException | ParseException e) {
         this.logger.info("Invalid OAuth 2.0 Client Credentials Grant Access Token used. " + e.getMessage());
         return Optional.empty();
      }
   }

   private Optional<OAuth2ClientCredentialsGrantAccessToken> verifiedExpiration(OAuth2ClientCredentialsGrantAccessToken token) {
      if (this.timeProvider.getCurrentTime().after(token.getExpiresAt())) {
         this.logger.info("Invalid OAuth 2.0 Client Credentials Grant Access Token used. Token has expired.");
         return Optional.empty();
      } else {
         this.logger.info("Successfully verified OAuth 2.0 Client Credentials Grant Access Token");
         return Optional.of(token);
      }
   }

   private Optional<OAuth2ClientCredentialsGrantAccessToken> accessTokenFromClaimSet(JWTClaimsSet jwtClaimsSet, String tokenString) {
      String subject = jwtClaimsSet.getSubject();
      if (StringUtils.isEmpty(subject)) {
         this.logger.info("Invalid OAuth 2.0 Client Credentials Grant Access Token used. Missing subject claim.");
         return Optional.empty();
      }

      Date expirationTime = jwtClaimsSet.getExpirationTime();
      if (expirationTime == null) {
         this.logger.info("Invalid OAuth 2.0 Client Credentials Grant Access Token used. Missing expiration claim.");
         return Optional.empty();
      }

      Date issueTime = jwtClaimsSet.getIssueTime();
      if (issueTime == null) {
         this.logger.info("Invalid OAuth 2.0 Client Credentials Grant Access Token used. Missing issue time claim.");
         return Optional.empty();
      }

      Optional<Set<OAuth2Scope>> scopeValues = this.scopeValues(jwtClaimsSet);
      if (scopeValues.isEmpty()) {
         return Optional.empty();
      }

      OAuth2ClientId clientId = OAuth2ClientId.oAuth2ClientId(subject);
      return Optional.of(
         OAuth2ClientCredentialsGrantAccessToken.builder()
            .issuedTo(clientId)
            .issuedAt(issueTime)
            .expiresAt(expirationTime)
            .scopeValues(scopeValues.get())
            .tokenString(tokenString)
            .build()
      );
   }

   private Optional<Set<OAuth2Scope>> scopeValues(JWTClaimsSet jwtClaimsSet) {
      try {
         String[] scopeClaimValue = jwtClaimsSet.getStringArrayClaim("scope");
         if (scopeClaimValue == null) {
            this.logger.info("Invalid OAuth 2.0 Client Credentials Grant Access Token used. Missing scope claim.");
            return Optional.empty();
         } else {
            return Optional.ofNullable(OAuth2Scope.convertToScopes(scopeClaimValue));
         }
      } catch (ParseException e) {
         this.logger.info("Invalid OAuth 2.0 Client Credentials Grant Access Token used. Invalid scope claim.");
         return Optional.empty();
      }
   }

   @SuppressFBWarnings(justification = "generated code")
   public static DefaultOAuth2ClientCredentialsGrantTokenReader.DefaultOAuth2ClientCredentialsGrantTokenReaderBuilder builder() {
      return new DefaultOAuth2ClientCredentialsGrantTokenReader.DefaultOAuth2ClientCredentialsGrantTokenReaderBuilder();
   }

   @SuppressFBWarnings(justification = "generated code")
   public static class DefaultOAuth2ClientCredentialsGrantTokenReaderBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private ELogger logger;
      @SuppressFBWarnings(justification = "generated code")
      private JwtSignatureVerifier signatureVerifier;
      @SuppressFBWarnings(justification = "generated code")
      private TimeProvider timeProvider;

      @SuppressFBWarnings(justification = "generated code")
      DefaultOAuth2ClientCredentialsGrantTokenReaderBuilder() {
      }

      @SuppressFBWarnings(justification = "generated code")
      public DefaultOAuth2ClientCredentialsGrantTokenReader.DefaultOAuth2ClientCredentialsGrantTokenReaderBuilder logger(@Nonnull ELogger logger) {
         if (logger == null) {
            throw new NullPointerException("logger is marked non-null but is null");
         }

         this.logger = logger;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public DefaultOAuth2ClientCredentialsGrantTokenReader.DefaultOAuth2ClientCredentialsGrantTokenReaderBuilder signatureVerifier(
         @Nonnull JwtSignatureVerifier signatureVerifier
      ) {
         if (signatureVerifier == null) {
            throw new NullPointerException("signatureVerifier is marked non-null but is null");
         }

         this.signatureVerifier = signatureVerifier;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public DefaultOAuth2ClientCredentialsGrantTokenReader.DefaultOAuth2ClientCredentialsGrantTokenReaderBuilder timeProvider(TimeProvider timeProvider) {
         this.timeProvider = timeProvider;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public DefaultOAuth2ClientCredentialsGrantTokenReader build() {
         return new DefaultOAuth2ClientCredentialsGrantTokenReader(this.logger, this.signatureVerifier, this.timeProvider);
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "DefaultOAuth2ClientCredentialsGrantTokenReader.DefaultOAuth2ClientCredentialsGrantTokenReaderBuilder(logger="
            + this.logger
            + ", signatureVerifier="
            + this.signatureVerifier
            + ", timeProvider="
            + this.timeProvider
            + ")";
      }
   }
}
