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

import com.airlock.iam.base.api.domain.model.exception.FailureException;
import com.airlock.iam.common.domain.model.jwt.JwtClaimsSet;
import com.airlock.iam.common.domain.service.jwt.signature.JwtSigner;
import com.airlock.iam.common.domain.service.jwt.signature.JwtSignerException;
import com.airlock.iam.oauth2.domain.model.claims.ScopeFormat;
import com.airlock.iam.oauth2.domain.model.token.RawOAuth2JwtAccessToken;
import com.airlock.iam.oauth2.domain.model.token.RawOAuth2Token;
import com.nimbusds.jose.JOSEObjectType;
import com.nimbusds.jose.PlainHeader;
import com.nimbusds.jwt.PlainJWT;
import com.nimbusds.jwt.JWTClaimsSet.Builder;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.annotation.Nullable;
import java.time.Duration;
import java.util.Date;
import java.util.Map;
import java.util.Optional;
import java.util.Random;
import lombok.NonNull;

public final class OAuth2JwtAccessTokenGenerator extends OAuth2TokenGenerator {
   static final String RANDOM_CLAIM = "random";
   static final String SCOPE_CLAIM = "scope";
   static final String CONFIRMATION_CLAIM = "cnf";
   @NonNull
   private final Optional<JwtSigner> signer;
   private final int tokenRandomPartLength;
   @NonNull
   private final ScopeFormat scopeFormat;
   @NonNull
   private final Duration expiresIn;
   @NonNull
   private final Duration notValidBeforeSkew;

   public OAuth2JwtAccessTokenGenerator(
      @Nullable Random random,
      @Nullable JwtSigner signer,
      @NonNull Integer tokenRandomPartLength,
      @NonNull ScopeFormat scopeFormat,
      @NonNull Duration expiresIn,
      @NonNull Duration notValidBeforeSkew
   ) {
      super(random);
      if (tokenRandomPartLength == null) {
         throw new NullPointerException("tokenRandomPartLength is marked non-null but is null");
      }

      if (scopeFormat == null) {
         throw new NullPointerException("scopeFormat is marked non-null but is null");
      }

      if (expiresIn == null) {
         throw new NullPointerException("expiresIn is marked non-null but is null");
      }

      if (notValidBeforeSkew == null) {
         throw new NullPointerException("notValidBeforeSkew is marked non-null but is null");
      }

      this.signer = Optional.ofNullable(signer);
      this.tokenRandomPartLength = tokenRandomPartLength;
      this.scopeFormat = scopeFormat;
      this.expiresIn = expiresIn;
      this.notValidBeforeSkew = notValidBeforeSkew;
   }

   @Override
   public RawOAuth2Token generateToken(TokenGeneratorData tokenInfo) {
      Builder jwtClaimsSet = new Builder();
      tokenInfo.getClaims().forEach(claim -> jwtClaimsSet.claim(claim.name(), claim.value().toJSONSerializable()));
      jwtClaimsSet.jwtID(this.generateKey().getValue());
      jwtClaimsSet.claim("random", this.generateWafSafeRandomString(this.tokenRandomPartLength));
      jwtClaimsSet.claim("scope", this.scopeFormat.getScopeClaimValue(tokenInfo.getScopes()));
      this.addTimestampClaims(jwtClaimsSet, tokenInfo.getGenerationDate());
      addTokenBinding(tokenInfo, jwtClaimsSet);
      return RawOAuth2JwtAccessToken.parseToken(this.signed(jwtClaimsSet));
   }

   private void addTimestampClaims(Builder jwtClaimsSet, Date generationDate) {
      jwtClaimsSet.issueTime(generationDate);
      jwtClaimsSet.notBeforeTime(Date.from(generationDate.toInstant().minus(this.notValidBeforeSkew)));
      if (!this.expiresIn.isZero() && !this.expiresIn.isNegative()) {
         jwtClaimsSet.expirationTime(Date.from(generationDate.toInstant().plus(this.expiresIn)));
      }
   }

   private String signed(Builder jwtClaimsSetBuilder) {
      try {
         if (this.signer.isPresent()) {
            JwtClaimsSet jwtClaimsSet = JwtClaimsSet.of(jwtClaimsSetBuilder.build().toString());
            return this.signer.get().sign(jwtClaimsSet).tokenString();
         } else {
            PlainHeader header = new com.nimbusds.jose.PlainHeader.Builder().type(JOSEObjectType.JWT).build();
            PlainJWT plainJWT = new PlainJWT(header, jwtClaimsSetBuilder.build());
            return plainJWT.serialize();
         }
      } catch (JwtSignerException e) {
         throw new FailureException(e);
      }
   }

   private static void addTokenBinding(TokenGeneratorData tokenInfo, Builder jwtClaimsSet) {
      tokenInfo.getBinding().ifPresent(binding -> jwtClaimsSet.claim("cnf", Map.of(binding.getMethod(), binding.getProof())));
   }

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

   @SuppressFBWarnings(justification = "generated code")
   public static class OAuth2JwtAccessTokenGeneratorBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private Random random;
      @SuppressFBWarnings(justification = "generated code")
      private JwtSigner signer;
      @SuppressFBWarnings(justification = "generated code")
      private Integer tokenRandomPartLength;
      @SuppressFBWarnings(justification = "generated code")
      private ScopeFormat scopeFormat;
      @SuppressFBWarnings(justification = "generated code")
      private Duration expiresIn;
      @SuppressFBWarnings(justification = "generated code")
      private Duration notValidBeforeSkew;

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

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2JwtAccessTokenGenerator.OAuth2JwtAccessTokenGeneratorBuilder random(@Nullable Random random) {
         this.random = random;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2JwtAccessTokenGenerator.OAuth2JwtAccessTokenGeneratorBuilder signer(@Nullable JwtSigner signer) {
         this.signer = signer;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2JwtAccessTokenGenerator.OAuth2JwtAccessTokenGeneratorBuilder tokenRandomPartLength(@NonNull Integer tokenRandomPartLength) {
         if (tokenRandomPartLength == null) {
            throw new NullPointerException("tokenRandomPartLength is marked non-null but is null");
         }

         this.tokenRandomPartLength = tokenRandomPartLength;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2JwtAccessTokenGenerator.OAuth2JwtAccessTokenGeneratorBuilder scopeFormat(@NonNull ScopeFormat scopeFormat) {
         if (scopeFormat == null) {
            throw new NullPointerException("scopeFormat is marked non-null but is null");
         }

         this.scopeFormat = scopeFormat;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2JwtAccessTokenGenerator.OAuth2JwtAccessTokenGeneratorBuilder expiresIn(@NonNull Duration expiresIn) {
         if (expiresIn == null) {
            throw new NullPointerException("expiresIn is marked non-null but is null");
         }

         this.expiresIn = expiresIn;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2JwtAccessTokenGenerator.OAuth2JwtAccessTokenGeneratorBuilder notValidBeforeSkew(@NonNull Duration notValidBeforeSkew) {
         if (notValidBeforeSkew == null) {
            throw new NullPointerException("notValidBeforeSkew is marked non-null but is null");
         }

         this.notValidBeforeSkew = notValidBeforeSkew;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2JwtAccessTokenGenerator build() {
         return new OAuth2JwtAccessTokenGenerator(
            this.random, this.signer, this.tokenRandomPartLength, this.scopeFormat, this.expiresIn, this.notValidBeforeSkew
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "OAuth2JwtAccessTokenGenerator.OAuth2JwtAccessTokenGeneratorBuilder(random="
            + this.random
            + ", signer="
            + this.signer
            + ", tokenRandomPartLength="
            + this.tokenRandomPartLength
            + ", scopeFormat="
            + this.scopeFormat
            + ", expiresIn="
            + this.expiresIn
            + ", notValidBeforeSkew="
            + this.notValidBeforeSkew
            + ")";
      }
   }
}
