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

import com.airlock.iam.base.api.domain.model.exception.ViolationException;
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.elog.api.ELogger;
import com.airlock.iam.oauth2.domain.model.acr.OpenIdConnectAcrValue;
import com.airlock.iam.oauth2.domain.model.oidc.OidcUserAgentSessionId;
import com.nimbusds.jose.JOSEObjectType;
import com.nimbusds.jose.PlainHeader;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.PlainJWT;
import com.nimbusds.jwt.JWTClaimsSet.Builder;
import com.nimbusds.oauth2.sdk.ParseException;
import com.nimbusds.openid.connect.sdk.Nonce;
import com.nimbusds.openid.connect.sdk.claims.ACR;
import com.nimbusds.openid.connect.sdk.claims.IDTokenClaimsSet;
import com.nimbusds.openid.connect.sdk.claims.SessionID;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Map.Entry;
import lombok.NonNull;
import org.apache.commons.lang3.StringUtils;

@SuppressFBWarnings("UWF_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR")
public class IdTokenBuilder {
   private final ELogger logger;
   private final JWTClaimsSet claims;

   private static IdTokenBuilder build(
      @NonNull ELogger logger,
      @NonNull String issuer,
      @NonNull String subject,
      @NonNull String audience,
      @NonNull Date expirationTime,
      @NonNull Date issueTime,
      Date authenticationTime,
      String nonce,
      String acrValue,
      String oidcUserAgentSessionId,
      String jwtId,
      @NonNull Map<String, Object> claims
   ) throws IdTokenBuilder.IdTokenException {
      if (logger == null) {
         throw new NullPointerException("logger is marked non-null but is null");
      } else if (issuer == null) {
         throw new NullPointerException("issuer is marked non-null but is null");
      } else if (subject == null) {
         throw new NullPointerException("subject is marked non-null but is null");
      } else if (audience == null) {
         throw new NullPointerException("audience is marked non-null but is null");
      } else if (expirationTime == null) {
         throw new NullPointerException("expirationTime is marked non-null but is null");
      } else if (issueTime == null) {
         throw new NullPointerException("issueTime is marked non-null but is null");
      } else if (claims == null) {
         throw new NullPointerException("claims is marked non-null but is null");
      } else {
         return tokenBuilder(
            logger,
            issuer,
            subject,
            audience,
            expirationTime,
            issueTime,
            authenticationTime,
            Optional.ofNullable(nonce),
            Optional.ofNullable(acrValue),
            Optional.ofNullable(oidcUserAgentSessionId),
            Optional.ofNullable(jwtId),
            claims
         );
      }
   }

   public static IdTokenBuilder buildFromTokenData(
      @NonNull ELogger logger, @NonNull String issuer, @NonNull OpenIdConnectIdTokenData tokenData, @NonNull Map<String, Object> customClaims
   ) throws IdTokenBuilder.IdTokenException {
      if (logger == null) {
         throw new NullPointerException("logger is marked non-null but is null");
      }

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

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

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

      tokenData.getClaims().forEach(claim -> customClaims.put(claim.name(), claim.value().toJSONSerializable()));
      return tokenBuilder(
         logger,
         issuer,
         tokenData.getClaims()
            .stream()
            .filter(claim -> "sub".equals(claim.name()))
            .map(claim -> claim.value().toJSONSerializable())
            .map(Object::toString)
            .findFirst()
            .orElseThrow(() -> new ViolationException("No 'sub' claim found in claims")),
         tokenData.getClientId().id(),
         tokenData.getExpiration(),
         new Date(),
         tokenData.getUserAuthenticationDate(),
         tokenData.getNonce(),
         tokenData.getAcr().map(OpenIdConnectAcrValue::name),
         tokenData.getOidcUserAgentSessionId().map(OidcUserAgentSessionId::id),
         Optional.empty(),
         customClaims
      );
   }

   private static IdTokenBuilder tokenBuilder(
      @NonNull ELogger logger,
      @NonNull String issuer,
      @NonNull String subject,
      @NonNull String audience,
      @NonNull Date expirationTime,
      @NonNull Date issueTime,
      Date authenticationTime,
      Optional<String> nonce,
      Optional<String> acrValue,
      Optional<String> sessionId,
      Optional<String> jwtId,
      @NonNull Map<String, Object> customClaims
   ) throws IdTokenBuilder.IdTokenException {
      if (logger == null) {
         throw new NullPointerException("logger is marked non-null but is null");
      }

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

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

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

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

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

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

      try {
         Builder claimBuilder = new Builder();
         claimBuilder.issuer(issuer);
         claimBuilder.subject(subject);
         claimBuilder.expirationTime(expirationTime);
         claimBuilder.audience(audience);
         claimBuilder.issueTime(issueTime);
         jwtId.filter(StringUtils::isNotBlank).ifPresent(claimBuilder::jwtID);

         for (Entry<String, Object> claim : customClaims.entrySet()) {
            claimBuilder.claim(claim.getKey(), claim.getValue());
         }

         IDTokenClaimsSet idTokenClaims = new IDTokenClaimsSet(claimBuilder.build());
         idTokenClaims.setAuthenticationTime(authenticationTime);
         Optional var10000 = sessionId.filter(StringUtils::isNotBlank).map(SessionID::new);
         IDTokenClaimsSet var17 = idTokenClaims;
         var10000.ifPresent(x$0 -> var17.setSessionID(x$0));
         nonce.filter(StringUtils::isNotBlank).map(Nonce::new).ifPresent(idTokenClaims::setNonce);
         acrValue.filter(StringUtils::isNotBlank).map(ACR::new).ifPresent(idTokenClaims::setACR);
         return new IdTokenBuilder(logger, idTokenClaims.toJWTClaimsSet());
      } catch (ParseException e) {
         throw new IdTokenBuilder.IdTokenException("Invalid claims for ID Token: " + e.getMessage(), e);
      }
   }

   public IdToken sign(JwtSigner signer) throws IdTokenBuilder.IdTokenException {
      try {
         return IdToken.of(signer.sign(JwtClaimsSet.of(this.claims.toString())).tokenString(), this.logger);
      } catch (JwtSignerException e) {
         throw new IdTokenBuilder.IdTokenException("Error while signing: ", e);
      }
   }

   public IdToken unsigned() {
      PlainHeader header = new com.nimbusds.jose.PlainHeader.Builder().type(JOSEObjectType.JWT).build();
      PlainJWT plainJWT = new PlainJWT(header, this.claims);
      return IdToken.of(plainJWT.serialize(), this.logger);
   }

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

   @SuppressFBWarnings(justification = "generated code")
   private IdTokenBuilder(ELogger logger, JWTClaimsSet claims) {
      this.logger = logger;
      this.claims = claims;
   }

   @SuppressFBWarnings(justification = "generated code")
   public static class IdTokenBuilderBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private ELogger logger;
      @SuppressFBWarnings(justification = "generated code")
      private String issuer;
      @SuppressFBWarnings(justification = "generated code")
      private String subject;
      @SuppressFBWarnings(justification = "generated code")
      private String audience;
      @SuppressFBWarnings(justification = "generated code")
      private Date expirationTime;
      @SuppressFBWarnings(justification = "generated code")
      private Date issueTime;
      @SuppressFBWarnings(justification = "generated code")
      private Date authenticationTime;
      @SuppressFBWarnings(justification = "generated code")
      private String nonce;
      @SuppressFBWarnings(justification = "generated code")
      private String acrValue;
      @SuppressFBWarnings(justification = "generated code")
      private String oidcUserAgentSessionId;
      @SuppressFBWarnings(justification = "generated code")
      private String jwtId;
      @SuppressFBWarnings(justification = "generated code")
      private ArrayList<String> claims$key;
      @SuppressFBWarnings(justification = "generated code")
      private ArrayList<Object> claims$value;

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

      @SuppressFBWarnings(justification = "generated code")
      public IdTokenBuilder.IdTokenBuilderBuilder 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 IdTokenBuilder.IdTokenBuilderBuilder issuer(@NonNull String issuer) {
         if (issuer == null) {
            throw new NullPointerException("issuer is marked non-null but is null");
         }

         this.issuer = issuer;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public IdTokenBuilder.IdTokenBuilderBuilder subject(@NonNull String subject) {
         if (subject == null) {
            throw new NullPointerException("subject is marked non-null but is null");
         }

         this.subject = subject;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public IdTokenBuilder.IdTokenBuilderBuilder audience(@NonNull String audience) {
         if (audience == null) {
            throw new NullPointerException("audience is marked non-null but is null");
         }

         this.audience = audience;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public IdTokenBuilder.IdTokenBuilderBuilder expirationTime(@NonNull Date expirationTime) {
         if (expirationTime == null) {
            throw new NullPointerException("expirationTime is marked non-null but is null");
         }

         this.expirationTime = expirationTime;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public IdTokenBuilder.IdTokenBuilderBuilder issueTime(@NonNull Date issueTime) {
         if (issueTime == null) {
            throw new NullPointerException("issueTime is marked non-null but is null");
         }

         this.issueTime = issueTime;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public IdTokenBuilder.IdTokenBuilderBuilder authenticationTime(Date authenticationTime) {
         this.authenticationTime = authenticationTime;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public IdTokenBuilder.IdTokenBuilderBuilder nonce(String nonce) {
         this.nonce = nonce;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public IdTokenBuilder.IdTokenBuilderBuilder acrValue(String acrValue) {
         this.acrValue = acrValue;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public IdTokenBuilder.IdTokenBuilderBuilder oidcUserAgentSessionId(String oidcUserAgentSessionId) {
         this.oidcUserAgentSessionId = oidcUserAgentSessionId;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public IdTokenBuilder.IdTokenBuilderBuilder jwtId(String jwtId) {
         this.jwtId = jwtId;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public IdTokenBuilder.IdTokenBuilderBuilder claim(String claimKey, Object claimValue) {
         if (this.claims$key == null) {
            this.claims$key = new ArrayList<>();
            this.claims$value = new ArrayList<>();
         }

         this.claims$key.add(claimKey);
         this.claims$value.add(claimValue);
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public IdTokenBuilder.IdTokenBuilderBuilder claims(Map<? extends String, ? extends Object> claims) {
         if (claims == null) {
            throw new NullPointerException("claims cannot be null");
         }

         if (this.claims$key == null) {
            this.claims$key = new ArrayList<>();
            this.claims$value = new ArrayList<>();
         }

         for (Entry<? extends String, ? extends Object> $lombokEntry : claims.entrySet()) {
            this.claims$key.add($lombokEntry.getKey());
            this.claims$value.add($lombokEntry.getValue());
         }

         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public IdTokenBuilder.IdTokenBuilderBuilder clearClaims() {
         if (this.claims$key != null) {
            this.claims$key.clear();
            this.claims$value.clear();
         }

         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public IdTokenBuilder build() throws IdTokenBuilder.IdTokenException {
         return IdTokenBuilder.build(
            this.logger,
            this.issuer,
            this.subject,
            this.audience,
            this.expirationTime,
            this.issueTime,
            this.authenticationTime,
            this.nonce,
            this.acrValue,
            this.oidcUserAgentSessionId,
            this.jwtId,
            switch (this.claims$key == null ? 0 : this.claims$key.size()) {
               case 0 -> Collections.emptyMap();
               case 1 -> Collections.singletonMap(this.claims$key.get(0), this.claims$value.get(0));
               default -> {
                  Map<String, Object> claims = new LinkedHashMap<>(
                     this.claims$key.size() < 1073741824 ? 1 + this.claims$key.size() + (this.claims$key.size() - 3) / 3 : Integer.MAX_VALUE
                  );

                  for (int $i = 0; $i < this.claims$key.size(); $i++) {
                     claims.put(this.claims$key.get($i), this.claims$value.get($i));
                  }

                  yield Collections.unmodifiableMap(claims);
               }
            }
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "IdTokenBuilder.IdTokenBuilderBuilder(logger="
            + this.logger
            + ", issuer="
            + this.issuer
            + ", subject="
            + this.subject
            + ", audience="
            + this.audience
            + ", expirationTime="
            + this.expirationTime
            + ", issueTime="
            + this.issueTime
            + ", authenticationTime="
            + this.authenticationTime
            + ", nonce="
            + this.nonce
            + ", acrValue="
            + this.acrValue
            + ", oidcUserAgentSessionId="
            + this.oidcUserAgentSessionId
            + ", jwtId="
            + this.jwtId
            + ", claims$key="
            + this.claims$key
            + ", claims$value="
            + this.claims$value
            + ")";
      }
   }

   public static class IdTokenException extends Exception {
      public IdTokenException(String message, Throwable cause) {
         super(message, cause);
      }
   }
}
