package com.airlock.iam.oauth2.infrastructure.service.tokenexchange;

import com.airlock.iam.base.api.domain.model.exception.FailureException;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.base.misc.result.Result;
import com.airlock.iam.base.misc.time.TimeProvider;
import com.airlock.iam.common.domain.service.jwt.jwks.JwksSignatureValidator;
import com.airlock.iam.common.domain.service.jwt.signature.JwtSignatureVerifier;
import com.airlock.iam.common.domain.service.jwt.signature.JwtSignatureVerifierException;
import com.airlock.iam.common.domain.service.jwt.signature.JwtSignatureVerifierImpl;
import com.airlock.iam.common.domain.service.jwt.signature.RemoteJWKSetLoader;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.oauth2.api.domain.model.tokenexchange.OAuth2TokenExchangeRawToken;
import com.airlock.iam.oauth2.api.domain.model.tokenexchange.OAuth2TokenExchangeTokenType;
import com.airlock.iam.oauth2.api.domain.model.tokenexchange.jwt.OAuth2TokenExchangeValidatedTokenData;
import com.airlock.iam.oauth2.domain.model.logger.IntegrationLoggerFactory;
import com.airlock.iam.oauth2.domain.model.logging.OAuth2LogTypes;
import com.airlock.iam.oauth2.domain.model.logging.OAuth2TokenRepresentation;
import com.airlock.iam.oauth2.domain.model.tokenexchange.OAuth2TokenExchangeSignedJwtToken;
import com.airlock.iam.oauth2.domain.model.tokenexchange.OAuth2TokenExchangeTokenIssuerId;
import com.airlock.iam.oauth2.domain.model.validation.OAuth2Error;
import com.airlock.iam.oauth2.domain.service.tokenexchange.RemoteDiscoveryLoader;
import com.airlock.iam.oauth2.domain.service.tokenexchange.validation.OAuth2TokenExchangeTokenValidationService;
import com.airlock.iam.oauth2.domain.service.tokenexchange.validation.OAuth2TokenExchangeValidationServiceSettings;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.net.URI;
import java.util.Optional;
import org.apache.commons.lang3.StringUtils;

public final class OAuth2TokenExchangeOidcDiscoveryTokenValidationServiceImpl implements OAuth2TokenExchangeTokenValidationService {
   private final Provider<IntegrationLoggerFactory> loggerFactory;
   private final Provider<OAuth2TokenExchangeValidationServiceSettings> settings;
   private final Provider<OAuth2TokenRepresentation> tokenRepresentation;
   private final Provider<TimeProvider> timeProvider;
   private final Provider<RemoteJWKSetLoader> jwksLoader;
   private final Provider<RemoteDiscoveryLoader> jwksUriLoader;

   @Inject
   public OAuth2TokenExchangeOidcDiscoveryTokenValidationServiceImpl(
      Provider<OAuth2TokenExchangeValidationServiceSettings> settings,
      Provider<IntegrationLoggerFactory> loggerFactory,
      Provider<OAuth2TokenRepresentation> tokenRepresentation,
      Provider<TimeProvider> timeProvider,
      Provider<RemoteJWKSetLoader> jwksLoader,
      Provider<RemoteDiscoveryLoader> jwksUriLoader
   ) {
      this.settings = settings;
      this.loggerFactory = loggerFactory;
      this.tokenRepresentation = tokenRepresentation;
      this.timeProvider = timeProvider;
      this.jwksLoader = jwksLoader;
      this.jwksUriLoader = jwksUriLoader;
   }

   @Override
   public Result<OAuth2TokenExchangeValidatedTokenData, OAuth2Error> validate(OAuth2TokenExchangeRawToken token) {
      OAuth2TokenExchangeTokenType tokenUriType = token.getType();
      if (!this.isAllowedTokenType(tokenUriType)) {
         this.getLogger()
            .info(
               OAuth2LogTypes.OAUTH2_MESSAGE,
               String.format("%s token type %s not supported.", StringUtils.capitalize(this.tokenKind()), LogUtil.logSafeQuote(tokenUriType.toURI()))
            );
         return Result.failure(OAuth2Error.INVALID_REQUEST);
      } else {
         Optional<OAuth2TokenExchangeSignedJwtToken> parsedToken = OAuth2TokenExchangeSignedJwtToken.parse(token);
         if (parsedToken.isEmpty()) {
            this.getLogger()
               .info(
                  OAuth2LogTypes.OAUTH2_MESSAGE,
                  String.format(
                     "Could not parse %s token as signed JWT: %s",
                     this.tokenKind(),
                     ((OAuth2TokenRepresentation)this.tokenRepresentation.get()).apply(token.getValue())
                  )
               );
            return Result.failure(OAuth2Error.INVALID_REQUEST);
         } else if (!parsedToken.get().isValidAt(((TimeProvider)this.timeProvider.get()).getCurrentInstant())) {
            this.getLogger()
               .info(OAuth2LogTypes.OAUTH2_MESSAGE, "%s token is either expired or not yet valid.".formatted(StringUtils.capitalize(this.tokenKind())));
            return Result.failure(OAuth2Error.INVALID_REQUEST);
         } else {
            Optional<OAuth2TokenExchangeTokenIssuerId> issuerId = parsedToken.flatMap(OAuth2TokenExchangeSignedJwtToken::getIssuerId);
            if (issuerId.isEmpty()) {
               this.getLogger().info(OAuth2LogTypes.OAUTH2_MESSAGE, "Issuer is missing from %s token.".formatted(this.tokenKind()));
               return Result.failure(OAuth2Error.INVALID_REQUEST);
            } else if (!this.isAllowedIssuer(issuerId.get())) {
               this.getLogger()
                  .info(
                     OAuth2LogTypes.OAUTH2_MESSAGE,
                     String.format(
                        "%s token issuer %s is not configured as an allowed issuer.",
                        StringUtils.capitalize(this.tokenKind()),
                        LogUtil.logSafeQuote(issuerId.get().getIssuerId())
                     )
                  );
               return Result.failure(OAuth2Error.INVALID_REQUEST);
            } else if (!this.isSignatureValid(parsedToken.get())) {
               this.getLogger()
                  .info(OAuth2LogTypes.OAUTH2_MESSAGE, "%s token signature is missing or invalid.".formatted(StringUtils.capitalize(this.tokenKind())));
               return Result.failure(OAuth2Error.INVALID_REQUEST);
            } else {
               return Result.result(new OAuth2TokenExchangeValidatedTokenData(parsedToken.get().getAllClaims()));
            }
         }
      }
   }

   private boolean isAllowedTokenType(OAuth2TokenExchangeTokenType tokenUriType) {
      return ((OAuth2TokenExchangeValidationServiceSettings)this.settings.get()).getAllowedTokenTypes().contains(tokenUriType);
   }

   private String tokenKind() {
      return ((OAuth2TokenExchangeValidationServiceSettings)this.settings.get()).getTokenKind().getDescription();
   }

   private ELogger getLogger() {
      return ((IntegrationLoggerFactory)this.loggerFactory.get()).create(OAuth2TokenExchangeOidcDiscoveryTokenValidationServiceImpl.class);
   }

   private boolean isAllowedIssuer(OAuth2TokenExchangeTokenIssuerId issuer) {
      return ((OAuth2TokenExchangeValidationServiceSettings)this.settings.get()).getAllowedIssuers().stream().anyMatch(issuer::equals);
   }

   private boolean isSignatureValid(OAuth2TokenExchangeSignedJwtToken subjectToken) {
      OAuth2TokenExchangeTokenIssuerId issuerId = subjectToken.getIssuerId().orElseThrow();

      try {
         URI jwksUri = issuerId.deriveOpenIdConnectDiscoveryUri()
            .map(((RemoteDiscoveryLoader)this.jwksUriLoader.get())::getJwksUriFromDiscoveryEndpoint)
            .orElseThrow(
               () -> new FailureException(
                  String.format(
                     "%s token issuer ID %s is not a valid URI.", StringUtils.capitalize(this.tokenKind()), LogUtil.logSafeQuote(issuerId.getIssuerId())
                  )
               )
            );
         JwtSignatureVerifier signatureVerifier = new JwtSignatureVerifierImpl(new JwksSignatureValidator((RemoteJWKSetLoader)this.jwksLoader.get(), jwksUri));
         return signatureVerifier.isSignatureValid(subjectToken.getValue());
      } catch (JwtSignatureVerifierException e) {
         this.getLogger().info(OAuth2LogTypes.OAUTH2_MESSAGE, "Unable to verify %s token signature.".formatted(this.tokenKind()));
      } catch (FailureException e) {
         this.getLogger()
            .warn(
               OAuth2LogTypes.OAUTH2_MESSAGE,
               String.format("Failed to fetch JWKS for issuer %s: %s", LogUtil.logSafeQuote(issuerId.getIssuerId()), e.getMessage())
            );
      }

      return false;
   }

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

   @SuppressFBWarnings(justification = "generated code")
   public static class OAuth2TokenExchangeOidcDiscoveryTokenValidationServiceImplBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private Provider<OAuth2TokenExchangeValidationServiceSettings> settings;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<IntegrationLoggerFactory> loggerFactory;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<OAuth2TokenRepresentation> tokenRepresentation;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<TimeProvider> timeProvider;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<RemoteJWKSetLoader> jwksLoader;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<RemoteDiscoveryLoader> jwksUriLoader;

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

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2TokenExchangeOidcDiscoveryTokenValidationServiceImpl.OAuth2TokenExchangeOidcDiscoveryTokenValidationServiceImplBuilder settings(
         Provider<OAuth2TokenExchangeValidationServiceSettings> settings
      ) {
         this.settings = settings;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2TokenExchangeOidcDiscoveryTokenValidationServiceImpl.OAuth2TokenExchangeOidcDiscoveryTokenValidationServiceImplBuilder loggerFactory(
         Provider<IntegrationLoggerFactory> loggerFactory
      ) {
         this.loggerFactory = loggerFactory;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2TokenExchangeOidcDiscoveryTokenValidationServiceImpl.OAuth2TokenExchangeOidcDiscoveryTokenValidationServiceImplBuilder tokenRepresentation(
         Provider<OAuth2TokenRepresentation> tokenRepresentation
      ) {
         this.tokenRepresentation = tokenRepresentation;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2TokenExchangeOidcDiscoveryTokenValidationServiceImpl.OAuth2TokenExchangeOidcDiscoveryTokenValidationServiceImplBuilder timeProvider(
         Provider<TimeProvider> timeProvider
      ) {
         this.timeProvider = timeProvider;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2TokenExchangeOidcDiscoveryTokenValidationServiceImpl.OAuth2TokenExchangeOidcDiscoveryTokenValidationServiceImplBuilder jwksLoader(
         Provider<RemoteJWKSetLoader> jwksLoader
      ) {
         this.jwksLoader = jwksLoader;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2TokenExchangeOidcDiscoveryTokenValidationServiceImpl.OAuth2TokenExchangeOidcDiscoveryTokenValidationServiceImplBuilder jwksUriLoader(
         Provider<RemoteDiscoveryLoader> jwksUriLoader
      ) {
         this.jwksUriLoader = jwksUriLoader;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2TokenExchangeOidcDiscoveryTokenValidationServiceImpl build() {
         return new OAuth2TokenExchangeOidcDiscoveryTokenValidationServiceImpl(
            this.settings, this.loggerFactory, this.tokenRepresentation, this.timeProvider, this.jwksLoader, this.jwksUriLoader
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "OAuth2TokenExchangeOidcDiscoveryTokenValidationServiceImpl.OAuth2TokenExchangeOidcDiscoveryTokenValidationServiceImplBuilder(settings="
            + this.settings
            + ", loggerFactory="
            + this.loggerFactory
            + ", tokenRepresentation="
            + this.tokenRepresentation
            + ", timeProvider="
            + this.timeProvider
            + ", jwksLoader="
            + this.jwksLoader
            + ", jwksUriLoader="
            + this.jwksUriLoader
            + ")";
      }
   }
}
