package com.airlock.iam.oauth2.domain.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.api.domain.model.claims.Claim;
import com.airlock.iam.common.api.domain.model.claims.ClaimValue;
import com.airlock.iam.common.domain.model.claims.ClaimSet;
import com.airlock.iam.common.domain.model.claims.StringClaimValue;
import com.airlock.iam.common.domain.model.jwt.JwtClaimsSet;
import com.airlock.iam.common.domain.model.jwt.signature.JsonWebSignatureToken;
import com.airlock.iam.common.domain.service.jwt.signature.JwtSigner;
import com.airlock.iam.common.domain.service.jwt.signature.JwtSignerException;
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.api.domain.model.clientauthentication.OAuth2ClientAuthentication;
import com.airlock.iam.oauth2.api.domain.model.tokenexchange.OAuth2TokenExchangeGrantRequest;
import com.airlock.iam.oauth2.api.domain.model.tokenexchange.OAuth2TokenExchangeTokenType;
import com.airlock.iam.oauth2.api.domain.model.tokenexchange.jwt.OAuth2TokenExchangeJwtClaimExtractionException;
import com.airlock.iam.oauth2.api.domain.model.tokenexchange.jwt.OAuth2TokenExchangeJwtClaimsExtractor;
import com.airlock.iam.oauth2.api.domain.model.tokenexchange.jwt.OAuth2TokenExchangeValidatedTokenData;
import com.airlock.iam.oauth2.domain.model.Issuer;
import com.airlock.iam.oauth2.domain.model.client.OAuth2Client;
import com.airlock.iam.oauth2.domain.model.logger.IntegrationLoggerFactory;
import com.airlock.iam.oauth2.domain.model.tokenexchange.OAuth2TokenExchangeGrantAccessToken;
import com.airlock.iam.oauth2.domain.model.tokenexchange.rules.jwt.OAuth2TokenExchangeJwtClaimValueExtractor;
import com.airlock.iam.oauth2.domain.service.tokenexchange.scope.OAuth2TokenExchangeScopeHandler;
import com.airlock.iam.security.util.crypto.random.AlphanumericRandomStringGenerator;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.JWTClaimsSet.Builder;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import jakarta.inject.Provider;
import java.time.Duration;
import java.time.Instant;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;

public final class OAuth2TokenExchangeGrantJwtTokenIssuer implements OAuth2TokenExchangeGrantTokenIssuer {
   private static final String ACTOR_CLAIM_NAME = "act";
   private static final String AUDIENCE_CLAIM_NAME = "aud";
   private static final String CLIENT_ID_CLAIM_NAME = "client_id";
   private static final String SCOPE_CLAIM_NAME = "scope";
   private final Provider<IntegrationLoggerFactory> loggerFactory;
   private final Provider<JwtSigner> signer;
   private final Provider<TimeProvider> timeProvider;
   private final Provider<Optional<Issuer>> issuerId;
   private final Provider<OAuth2TokenExchangeTokenType> issuedTokenType;
   private final Provider<OAuth2TokenExchangeGrantJwtTokenIssuerSettings> settings;
   private final Provider<OAuth2TokenExchangeJwtClaimValueExtractor<StringClaimValue>> subjectClaimExtractor;
   private final Provider<List<OAuth2TokenExchangeJwtClaimsExtractor<ClaimValue>>> customClaimExtractors;
   private final Provider<Optional<OAuth2TokenExchangeJwtClaimValueExtractor<ClaimValue>>> audienceClaimExtractor;
   private final Provider<Optional<OAuth2TokenExchangeJwtClaimValueExtractor<ClaimSet>>> actorClaimExtractor;
   private final Provider<Optional<OAuth2TokenExchangeJwtClaimValueExtractor<StringClaimValue>>> clientIdClaimExtractor;
   private final Provider<Optional<OAuth2TokenExchangeScopeHandler>> scopeHandler;

   @Inject
   private OAuth2TokenExchangeGrantJwtTokenIssuer(
      Provider<JwtSigner> signer,
      Provider<TimeProvider> timeProvider,
      Provider<OAuth2TokenExchangeGrantJwtTokenIssuerSettings> settings,
      Provider<IntegrationLoggerFactory> loggerFactory,
      Provider<Optional<Issuer>> issuerId,
      Provider<OAuth2TokenExchangeTokenType> issuedTokenType,
      @Named("subjectClaimExtractor") Provider<OAuth2TokenExchangeJwtClaimValueExtractor<StringClaimValue>> subjectClaimExtractor,
      Provider<List<OAuth2TokenExchangeJwtClaimsExtractor<ClaimValue>>> customClaimExtractors,
      @Named("audienceClaimExtractor") Provider<Optional<OAuth2TokenExchangeJwtClaimValueExtractor<ClaimValue>>> audienceClaimExtractor,
      @Named("actorClaimExtractor") Provider<Optional<OAuth2TokenExchangeJwtClaimValueExtractor<ClaimSet>>> actorClaimExtractor,
      @Named("clientIdClaimExtractor") Provider<Optional<OAuth2TokenExchangeJwtClaimValueExtractor<StringClaimValue>>> clientIdClaimExtractor,
      Provider<Optional<OAuth2TokenExchangeScopeHandler>> scopeHandler
   ) {
      this.loggerFactory = loggerFactory;
      this.signer = signer;
      this.timeProvider = timeProvider;
      this.settings = settings;
      this.issuerId = issuerId;
      this.issuedTokenType = issuedTokenType;
      this.subjectClaimExtractor = subjectClaimExtractor;
      this.customClaimExtractors = customClaimExtractors;
      this.audienceClaimExtractor = audienceClaimExtractor;
      this.actorClaimExtractor = actorClaimExtractor;
      this.clientIdClaimExtractor = clientIdClaimExtractor;
      this.scopeHandler = scopeHandler;
   }

   @Override
   public Result<OAuth2TokenExchangeGrantAccessToken, OAuth2TokenExchangeGrantTokenIssuerError> issueAccessToken(
      OAuth2Client client,
      OAuth2ClientAuthentication authentication,
      OAuth2TokenExchangeGrantRequest grantRequest,
      OAuth2TokenExchangeValidatedTokenData subjectTokenData
   ) {
      return this.processScope(grantRequest, subjectTokenData, client)
         .flatMap(
            grantedScopes -> this.generateJwt(grantRequest, subjectTokenData, grantedScopes)
               .mapResult(jwtClaimsSet -> this.generateAccessToken(jwtClaimsSet, client.clientId(), grantedScopes))
         );
   }

   private OAuth2TokenExchangeGrantAccessToken generateAccessToken(JWTClaimsSet jwtClaimsSet, OAuth2ClientId clientId, Set<OAuth2Scope> grantedScopes) {
      try {
         JsonWebSignatureToken jws = ((JwtSigner)this.signer.get()).sign(JwtClaimsSet.of(jwtClaimsSet.toString()));
         OAuth2TokenExchangeGrantAccessToken accessToken = OAuth2TokenExchangeGrantAccessToken.builder()
            .issuedTokenType((OAuth2TokenExchangeTokenType)this.issuedTokenType.get())
            .tokenLifetime(Duration.between(jwtClaimsSet.getIssueTime().toInstant(), jwtClaimsSet.getExpirationTime().toInstant()))
            .accessToken(jws.tokenString())
            .scope(grantedScopes)
            .build();
         this.getLogger()
            .info(
               String.format(
                  "OAuth 2.0 token exchange grant access token issued to client %s with scope %s.",
                  LogUtil.logSafeQuote(clientId.id()),
                  OAuth2Scope.convertToScopeNames(accessToken.getScope())
               )
            );
         return accessToken;
      } catch (JwtSignerException e) {
         throw new FailureException(e);
      }
   }

   private Result<Set<OAuth2Scope>, OAuth2TokenExchangeGrantTokenIssuerError> processScope(
      OAuth2TokenExchangeGrantRequest grantRequest, OAuth2TokenExchangeValidatedTokenData subjectTokenData, OAuth2Client client
   ) {
      return ((Optional)this.scopeHandler.get())
         .<Result<Set<OAuth2Scope>, OAuth2TokenExchangeGrantTokenIssuerError>>map(handler -> handler.processScope(grantRequest, subjectTokenData, client))
         .orElse(Result.result(Set.of()));
   }

   private Result<JWTClaimsSet, OAuth2TokenExchangeGrantTokenIssuerError> generateJwt(
      OAuth2TokenExchangeGrantRequest grantRequest, OAuth2TokenExchangeValidatedTokenData subjectTokenData, Set<OAuth2Scope> grantedScopes
   ) {
      Instant now = ((TimeProvider)this.timeProvider.get()).getCurrentInstant();

      try {
         Optional<String> subjectClaimValue = this.subjectClaimValue(grantRequest, subjectTokenData);
         if (subjectClaimValue.isEmpty()) {
            return Result.failure(
               OAuth2TokenExchangeGrantTokenIssuerError.tokenExchangeGrantIssuerError("Subject claim value is either not of type string, empty or blank.")
            );
         }

         Optional<Object> audienceClaimValue = this.audienceClaimValue(grantRequest, subjectTokenData);
         if (((Optional)this.audienceClaimExtractor.get()).isPresent() && audienceClaimValue.isEmpty()) {
            return Result.failure(OAuth2TokenExchangeGrantTokenIssuerError.tokenExchangeGrantIssuerError("Audience claim value is empty, missing or invalid."));
         }

         Builder jwtClaimSetBuilder = this.claimSetWithCustomClaims(grantRequest, subjectTokenData, grantedScopes)
            .jwtID(AlphanumericRandomStringGenerator.generateAlphanumericRandomString(27))
            .subject(subjectClaimValue.get())
            .issueTime(Date.from(now))
            .expirationTime(Date.from(now.plus(((OAuth2TokenExchangeGrantJwtTokenIssuerSettings)this.settings.get()).getIssuedAccessTokenLifetime())))
            .claim("scope", ((OAuth2TokenExchangeGrantJwtTokenIssuerSettings)this.settings.get()).getScopeFormat().getScopeClaimValue(grantedScopes));
         ((Optional)this.issuerId.get()).ifPresent(value -> jwtClaimSetBuilder.issuer(value.getValue()));
         audienceClaimValue.ifPresent(value -> jwtClaimSetBuilder.claim("aud", value));
         this.actorClaimValue(grantRequest, subjectTokenData).ifPresent(value -> jwtClaimSetBuilder.claim("act", value));
         this.clientIdClaimValue(grantRequest, subjectTokenData).ifPresent(value -> jwtClaimSetBuilder.claim("client_id", value));
         return Result.result(jwtClaimSetBuilder.build());
      } catch (OAuth2TokenExchangeJwtClaimExtractionException e) {
         return Result.failure(OAuth2TokenExchangeGrantTokenIssuerError.tokenExchangeGrantIssuerError(e.getMessage()));
      }
   }

   private Optional<String> subjectClaimValue(OAuth2TokenExchangeGrantRequest grantRequest, OAuth2TokenExchangeValidatedTokenData subjectTokenData) throws OAuth2TokenExchangeJwtClaimExtractionException {
      return ((OAuth2TokenExchangeJwtClaimValueExtractor)this.subjectClaimExtractor.get())
         .extract(grantRequest, subjectTokenData)
         .map(StringClaimValue::value)
         .filter(StringUtils::isNotBlank);
   }

   private Optional<Object> actorClaimValue(OAuth2TokenExchangeGrantRequest grantRequest, OAuth2TokenExchangeValidatedTokenData subjectTokenData) throws OAuth2TokenExchangeJwtClaimExtractionException {
      Optional<OAuth2TokenExchangeJwtClaimValueExtractor<ClaimSet>> extractor = (Optional<OAuth2TokenExchangeJwtClaimValueExtractor<ClaimSet>>)this.actorClaimExtractor
         .get();
      return extractor.isEmpty() ? Optional.empty() : extractor.get().extract(grantRequest, subjectTokenData).map(ClaimSet::toJSONSerializable);
   }

   private Optional<String> clientIdClaimValue(OAuth2TokenExchangeGrantRequest grantRequest, OAuth2TokenExchangeValidatedTokenData subjectTokenData) throws OAuth2TokenExchangeJwtClaimExtractionException {
      Optional<OAuth2TokenExchangeJwtClaimValueExtractor<StringClaimValue>> extractor = (Optional<OAuth2TokenExchangeJwtClaimValueExtractor<StringClaimValue>>)this.clientIdClaimExtractor
         .get();
      return extractor.isEmpty() ? Optional.empty() : extractor.get().extract(grantRequest, subjectTokenData).map(StringClaimValue::value);
   }

   private Optional<Object> audienceClaimValue(OAuth2TokenExchangeGrantRequest grantRequest, OAuth2TokenExchangeValidatedTokenData subjectTokenData) throws OAuth2TokenExchangeJwtClaimExtractionException {
      Optional<OAuth2TokenExchangeJwtClaimValueExtractor<ClaimValue>> extractor = (Optional<OAuth2TokenExchangeJwtClaimValueExtractor<ClaimValue>>)this.audienceClaimExtractor
         .get();
      return extractor.isEmpty() ? Optional.empty() : extractor.get().extract(grantRequest, subjectTokenData).map(ClaimValue::toJSONSerializable);
   }

   private Builder claimSetWithCustomClaims(
      OAuth2TokenExchangeGrantRequest grantRequest, OAuth2TokenExchangeValidatedTokenData subjectTokenData, Set<OAuth2Scope> grantedScopes
   ) throws OAuth2TokenExchangeJwtClaimExtractionException {
      Builder builder = new Builder();

      for (OAuth2TokenExchangeJwtClaimsExtractor<ClaimValue> extractor : (List)this.customClaimExtractors.get()) {
         for (Claim<ClaimValue> claim : extractor.extract(grantRequest, subjectTokenData, grantedScopes)) {
            if (!JWTClaimsSet.getRegisteredNames().contains(claim.name())) {
               builder.claim(claim.name(), claim.value().toJSONSerializable());
            }
         }
      }

      return builder;
   }

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

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

   @SuppressFBWarnings(justification = "generated code")
   public static class OAuth2TokenExchangeGrantJwtTokenIssuerBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private Provider<JwtSigner> signer;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<TimeProvider> timeProvider;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<OAuth2TokenExchangeGrantJwtTokenIssuerSettings> settings;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<IntegrationLoggerFactory> loggerFactory;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<Optional<Issuer>> issuerId;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<OAuth2TokenExchangeTokenType> issuedTokenType;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<OAuth2TokenExchangeJwtClaimValueExtractor<StringClaimValue>> subjectClaimExtractor;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<List<OAuth2TokenExchangeJwtClaimsExtractor<ClaimValue>>> customClaimExtractors;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<Optional<OAuth2TokenExchangeJwtClaimValueExtractor<ClaimValue>>> audienceClaimExtractor;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<Optional<OAuth2TokenExchangeJwtClaimValueExtractor<ClaimSet>>> actorClaimExtractor;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<Optional<OAuth2TokenExchangeJwtClaimValueExtractor<StringClaimValue>>> clientIdClaimExtractor;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<Optional<OAuth2TokenExchangeScopeHandler>> scopeHandler;

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

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2TokenExchangeGrantJwtTokenIssuer.OAuth2TokenExchangeGrantJwtTokenIssuerBuilder signer(Provider<JwtSigner> signer) {
         this.signer = signer;
         return this;
      }

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

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2TokenExchangeGrantJwtTokenIssuer.OAuth2TokenExchangeGrantJwtTokenIssuerBuilder settings(
         Provider<OAuth2TokenExchangeGrantJwtTokenIssuerSettings> settings
      ) {
         this.settings = settings;
         return this;
      }

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

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2TokenExchangeGrantJwtTokenIssuer.OAuth2TokenExchangeGrantJwtTokenIssuerBuilder issuerId(Provider<Optional<Issuer>> issuerId) {
         this.issuerId = issuerId;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2TokenExchangeGrantJwtTokenIssuer.OAuth2TokenExchangeGrantJwtTokenIssuerBuilder issuedTokenType(
         Provider<OAuth2TokenExchangeTokenType> issuedTokenType
      ) {
         this.issuedTokenType = issuedTokenType;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2TokenExchangeGrantJwtTokenIssuer.OAuth2TokenExchangeGrantJwtTokenIssuerBuilder subjectClaimExtractor(
         Provider<OAuth2TokenExchangeJwtClaimValueExtractor<StringClaimValue>> subjectClaimExtractor
      ) {
         this.subjectClaimExtractor = subjectClaimExtractor;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2TokenExchangeGrantJwtTokenIssuer.OAuth2TokenExchangeGrantJwtTokenIssuerBuilder customClaimExtractors(
         Provider<List<OAuth2TokenExchangeJwtClaimsExtractor<ClaimValue>>> customClaimExtractors
      ) {
         this.customClaimExtractors = customClaimExtractors;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2TokenExchangeGrantJwtTokenIssuer.OAuth2TokenExchangeGrantJwtTokenIssuerBuilder audienceClaimExtractor(
         Provider<Optional<OAuth2TokenExchangeJwtClaimValueExtractor<ClaimValue>>> audienceClaimExtractor
      ) {
         this.audienceClaimExtractor = audienceClaimExtractor;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2TokenExchangeGrantJwtTokenIssuer.OAuth2TokenExchangeGrantJwtTokenIssuerBuilder actorClaimExtractor(
         Provider<Optional<OAuth2TokenExchangeJwtClaimValueExtractor<ClaimSet>>> actorClaimExtractor
      ) {
         this.actorClaimExtractor = actorClaimExtractor;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2TokenExchangeGrantJwtTokenIssuer.OAuth2TokenExchangeGrantJwtTokenIssuerBuilder clientIdClaimExtractor(
         Provider<Optional<OAuth2TokenExchangeJwtClaimValueExtractor<StringClaimValue>>> clientIdClaimExtractor
      ) {
         this.clientIdClaimExtractor = clientIdClaimExtractor;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2TokenExchangeGrantJwtTokenIssuer.OAuth2TokenExchangeGrantJwtTokenIssuerBuilder scopeHandler(
         Provider<Optional<OAuth2TokenExchangeScopeHandler>> scopeHandler
      ) {
         this.scopeHandler = scopeHandler;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2TokenExchangeGrantJwtTokenIssuer build() {
         return new OAuth2TokenExchangeGrantJwtTokenIssuer(
            this.signer,
            this.timeProvider,
            this.settings,
            this.loggerFactory,
            this.issuerId,
            this.issuedTokenType,
            this.subjectClaimExtractor,
            this.customClaimExtractors,
            this.audienceClaimExtractor,
            this.actorClaimExtractor,
            this.clientIdClaimExtractor,
            this.scopeHandler
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "OAuth2TokenExchangeGrantJwtTokenIssuer.OAuth2TokenExchangeGrantJwtTokenIssuerBuilder(signer="
            + this.signer
            + ", timeProvider="
            + this.timeProvider
            + ", settings="
            + this.settings
            + ", loggerFactory="
            + this.loggerFactory
            + ", issuerId="
            + this.issuerId
            + ", issuedTokenType="
            + this.issuedTokenType
            + ", subjectClaimExtractor="
            + this.subjectClaimExtractor
            + ", customClaimExtractors="
            + this.customClaimExtractors
            + ", audienceClaimExtractor="
            + this.audienceClaimExtractor
            + ", actorClaimExtractor="
            + this.actorClaimExtractor
            + ", clientIdClaimExtractor="
            + this.clientIdClaimExtractor
            + ", scopeHandler="
            + this.scopeHandler
            + ")";
      }
   }
}
