package com.airlock.iam.common.domain.service.jwt;

import com.airlock.iam.base.misc.date.Dates;
import com.airlock.iam.base.misc.json.JsonCoding;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.base.misc.time.TimeProvider;
import com.airlock.iam.common.api.domain.model.claims.ClaimsValidationResult;
import com.airlock.iam.common.api.domain.model.claims.ClaimsValidator;
import com.airlock.iam.common.domain.model.claims.ClaimsValidationResultImpl;
import com.airlock.iam.common.domain.model.jwt.encryption.JweToken;
import com.airlock.iam.common.domain.model.jwt.signature.JsonWebSignatureToken;
import com.airlock.iam.common.domain.service.jwt.encryption.JwtTicketDecrypter;
import com.airlock.iam.common.domain.service.jwt.signature.JwtSignatureVerifier;
import com.airlock.iam.common.domain.service.jwt.signature.JwtSignatureVerifierException;
import com.airlock.iam.core.misc.util.ticket.KeyMultiValue;
import com.airlock.iam.core.misc.util.ticket.Ticket;
import com.airlock.iam.core.misc.util.ticket.TicketDecoder;
import com.airlock.iam.core.misc.util.ticket.TicketEncodingException;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.text.ParseException;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Map.Entry;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import lombok.NonNull;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Strings;

public class JwtTicketDecoderImpl implements TicketDecoder {
   private static final ELogger LOG = ELoggerFactory.getLogger(JwtTicketDecoderImpl.class);
   private static final String BEARER_PREFIX = "Bearer ";
   private final Optional<String> usernameTicketKey;
   private final JwtSignatureVerifier signatureVerifier;
   private final TimeProvider timeProvider;
   private boolean expirationTimeIsMandatory;
   private boolean notBeforeIsMandatory;
   private boolean issuedAtIsMandatory;
   private boolean issuerIsMandatory;
   private boolean audienceIsMandatory;
   private boolean jwtIdIsMandatory;
   private boolean isSubjectMandatory;
   private int validNotBeforeSkew;
   private Optional<String> notBeforeTicketKey;
   private Optional<String> issuedAtTicketKey;
   private List<String> allowedIssuers;
   private Optional<String> issuerTicketKey;
   private List<String> allowedAudiences;
   private Optional<String> audienceTicketKey;
   private Optional<String> jwtIdTicketKey;
   private Optional<JwtTicketDecrypter> decrypter;
   private List<ClaimsValidator> additionalClaimValidators;
   private List<String> claimsStoredAsJson;

   public static JwtTicketDecoderImpl.JwtTicketDecoderImplBuilder builder(
      @NonNull String usernameTicketKeyVal, @NonNull JwtSignatureVerifier signatureVerifierVal, @NonNull TimeProvider timeProviderVal
   ) {
      if (usernameTicketKeyVal == null) {
         throw new NullPointerException("usernameTicketKeyVal is marked non-null but is null");
      } else if (signatureVerifierVal == null) {
         throw new NullPointerException("signatureVerifierVal is marked non-null but is null");
      } else if (timeProviderVal == null) {
         throw new NullPointerException("timeProviderVal is marked non-null but is null");
      } else {
         return builder(signatureVerifierVal, timeProviderVal)
            .expirationTimeIsMandatory(true)
            .usernameTicketKey(Optional.of(usernameTicketKeyVal))
            .isSubjectMandatory(true);
      }
   }

   public static JwtTicketDecoderImpl.JwtTicketDecoderImplBuilder builder(
      @NonNull JwtSignatureVerifier signatureVerifierVal, @NonNull TimeProvider timeProviderVal
   ) {
      if (signatureVerifierVal == null) {
         throw new NullPointerException("signatureVerifierVal is marked non-null but is null");
      } else if (timeProviderVal == null) {
         throw new NullPointerException("timeProviderVal is marked non-null but is null");
      } else {
         return new JwtTicketDecoderImpl(Optional.empty(), signatureVerifierVal, timeProviderVal)
            .toBuilder()
            .notBeforeIsMandatory(true)
            .expirationTimeIsMandatory(true)
            .issuedAtIsMandatory(true)
            .issuerIsMandatory(true)
            .isSubjectMandatory(false)
            .notBeforeTicketKey(Optional.empty())
            .issuedAtTicketKey(Optional.empty())
            .allowedIssuers(Collections.emptyList())
            .issuerTicketKey(Optional.empty())
            .allowedAudiences(Collections.emptyList())
            .audienceTicketKey(Optional.empty())
            .jwtIdTicketKey(Optional.empty())
            .decrypter(Optional.empty())
            .claimsStoredAsJson(Collections.emptyList())
            .additionalClaimValidators(Collections.emptyList());
      }
   }

   private List<String> ticketKeys() {
      return Stream.of(
            this.usernameTicketKey.orElse(""),
            this.notBeforeTicketKey.orElse(""),
            this.issuedAtTicketKey.orElse(""),
            this.issuerTicketKey.orElse(""),
            this.audienceTicketKey.orElse(""),
            this.jwtIdTicketKey.orElse("")
         )
         .filter(key -> !key.isEmpty())
         .collect(Collectors.toList());
   }

   private boolean isRegisteredClaim(String claim) {
      return JWTClaimsSet.getRegisteredNames().contains(claim);
   }

   @Override
   public Ticket decode(String ticketString, String urlEncodingScheme) throws TicketEncodingException {
      Objects.requireNonNull(ticketString, "ticketString must not be null");
      LOG.debug("Starting to decode the JWT ticket.");
      String jwt = this.removeBearerPrefixIfPresent(ticketString);
      JsonWebSignatureToken jwsToken = this.decryptIfDecryptionEnabled(jwt).orElseThrow(() -> new TicketEncodingException("Invalid JWT"));
      JWTClaimsSet claimsSet = this.verifySignature(jwsToken);
      this.verifyRegisteredClaims(claimsSet);
      this.ensureCustomClaimsNotReserved(claimsSet);
      this.verifyAdditionalClaims(claimsSet);
      Ticket ticket = this.ticketWithRegisteredClaims(claimsSet);
      this.addCustomClaims(ticket, claimsSet);
      return ticket;
   }

   private String removeBearerPrefixIfPresent(String jwt) {
      if (Strings.CI.startsWith(jwt, "Bearer ")) {
         LOG.debug("Removing 'Bearer ' prefix");
      }

      return Strings.CI.removeStart(jwt, "Bearer ");
   }

   private Optional<JsonWebSignatureToken> decryptIfDecryptionEnabled(String jwt) throws TicketEncodingException {
      return this.decrypter.isPresent() ? Optional.of(this.decrypter.get().decrypt(JweToken.of(jwt))) : JsonWebSignatureToken.parse(jwt);
   }

   private JWTClaimsSet verifySignature(JsonWebSignatureToken jwsToken) throws TicketEncodingException {
      try {
         if (this.signatureVerifier.isSignatureValid(jwsToken)) {
            LOG.debug("Successfully verified the JWT signature.");
            return this.claimsSet(this.parseSignedJwt(jwsToken));
         } else {
            throw new TicketEncodingException("The signature cannot be verified!");
         }
      } catch (JwtSignatureVerifierException e) {
         throw new TicketEncodingException("The signature cannot be verified!", e);
      }
   }

   private SignedJWT parseSignedJwt(JsonWebSignatureToken jwsToken) throws TicketEncodingException {
      try {
         return SignedJWT.parse(jwsToken.tokenString());
      } catch (ParseException e) {
         throw new TicketEncodingException(
            "Unable to parse the decoded JWT as a signed JWT. A reason could be that the JWT is encrypted or not signed at all. Message: " + e.getMessage(), e
         );
      }
   }

   private JWTClaimsSet claimsSet(SignedJWT signedJwt) throws TicketEncodingException {
      try {
         return signedJwt.getJWTClaimsSet();
      } catch (ParseException e) {
         throw new TicketEncodingException("Unable to parse the JWT claims set: " + e.getMessage(), e);
      }
   }

   private void verifyRegisteredClaims(JWTClaimsSet claimsSet) throws TicketEncodingException {
      this.verifyExpiration(this.expiration(claimsSet));
      this.verifySubject(this.subject(claimsSet));
      this.verifyNotBeforeIfConfigured(this.notBefore(claimsSet));
      this.verifyIssuedAtIfConfigured(this.issuedAt(claimsSet));
      this.verifyIssuerIfConfigured(this.issuer(claimsSet));
      this.verifyAudienceIfConfigured(this.audience(claimsSet));
      this.verifyJwtIdIfConfigured(this.jwtId(claimsSet));
   }

   private void ensureCustomClaimsNotReserved(JWTClaimsSet claimsSet) throws TicketEncodingException {
      List<String> collidingCustomClaims = claimsSet.getClaims()
         .keySet()
         .stream()
         .filter(claim -> !this.isRegisteredClaim(claim))
         .filter(this.ticketKeys()::contains)
         .toList();
      if (!collidingCustomClaims.isEmpty()) {
         throw new TicketEncodingException(
            "The JWT contains custom claim(s) that are reserved for a ticket key of a registered claim: " + collidingCustomClaims
         );
      }
   }

   private void verifyAdditionalClaims(JWTClaimsSet claimsSet) throws TicketEncodingException {
      List<ClaimsValidationResult> invalidClaims = this.additionalClaimValidators
         .stream()
         .map(validator -> validator.validate(claimsSet.getClaims()))
         .filter(res -> !res.isValid())
         .collect(Collectors.toList());
      if (!invalidClaims.isEmpty()) {
         throw new TicketEncodingException(
            "The JWT contains custom claim(s) that do not pass the custom validation: " + ClaimsValidationResultImpl.invalidClaimsAsString(invalidClaims)
         );
      }
   }

   private Ticket ticketWithRegisteredClaims(JWTClaimsSet claimsSet) {
      Ticket.TicketBuilder ticketBuilder = Ticket.builder();
      this.expiration(claimsSet).map(Date::getTime).ifPresent(ticketBuilder::expiryDate);
      this.subject(claimsSet).flatMap(this::subjectTicketEntryIfConfigured).ifPresent(ticketBuilder::keyMultiValue);
      this.notBefore(claimsSet).flatMap(this::notBeforeTicketEntryIfConfigured).ifPresent(ticketBuilder::keyMultiValue);
      this.issuedAt(claimsSet).flatMap(this::issuedAtTicketEntryIfConfigured).ifPresent(ticketBuilder::keyMultiValue);
      this.issuer(claimsSet).flatMap(this::issuerTicketEntryIfConfigured).ifPresent(ticketBuilder::keyMultiValue);
      this.audience(claimsSet).flatMap(this::audienceTicketEntryIfConfigured).ifPresent(ticketBuilder::keyMultiValue);
      this.jwtId(claimsSet).flatMap(this::jwtIdTicketEntryIfConfigured).ifPresent(ticketBuilder::keyMultiValue);
      return ticketBuilder.build();
   }

   private Optional<Date> expiration(JWTClaimsSet claimsSet) {
      return Optional.ofNullable(claimsSet.getExpirationTime());
   }

   private void verifyExpiration(Optional<Date> expiration) throws TicketEncodingException {
      if (this.expirationTimeIsMandatory) {
         TicketEncodingException.checkPresent(expiration, "The ticket does not include an expiration date (exp claim), but the claim is mandatory.");
      }

      if (expiration.isPresent() && this.timeProvider.getCurrentTime().after(expiration.get())) {
         throw new TicketEncodingException(
            "The jwt has expired: the exp (expiration) claim indicates a time before the current system time. The exp value is: " + expiration.get()
         );
      }
   }

   private Optional<String> subject(JWTClaimsSet claimsSet) {
      return Optional.ofNullable(claimsSet.getSubject());
   }

   private void verifySubject(Optional<String> subject) throws TicketEncodingException {
      if (this.isSubjectMandatory) {
         TicketEncodingException.checkPresent(subject, "The ticket does not include a subject (sub claim), but a subject is mandatory.");
      }
   }

   private Optional<KeyMultiValue> subjectTicketEntryIfConfigured(String subject) {
      if (this.usernameTicketKey.isPresent()) {
         LOG.debug("Using the ticket key " + this.usernameTicketKey.get() + " to store the JWT subject " + subject);
         return Optional.of(new KeyMultiValue(this.usernameTicketKey.get(), subject));
      } else {
         return Optional.empty();
      }
   }

   private Optional<Date> notBefore(JWTClaimsSet claimsSet) {
      return Optional.ofNullable(claimsSet.getNotBeforeTime());
   }

   private void verifyNotBeforeIfConfigured(Optional<Date> notBefore) throws TicketEncodingException {
      if (notBefore.isPresent()) {
         if (this.nowPlusNotBeforeSkew().before(notBefore.get())) {
            throw new TicketEncodingException(
               "The jwt is not yet valid: the nbf (not before) claim indicates a time after the current system time. The nbf value is: " + notBefore.get()
            );
         }

         LOG.debug("The jwt nbf claim was verified: the nbf (not before) claim indicates a time before the current system time.");
      } else {
         if (this.notBeforeIsMandatory) {
            throw new TicketEncodingException("The not before claim is configured as mandatory, but is not present in the JWT.");
         }

         LOG.debug("The nbf (not before) claim is not present and not configured as mandatory. Therefore it is not validated.");
      }
   }

   private Optional<KeyMultiValue> notBeforeTicketEntryIfConfigured(Date notBefore) {
      return this.notBeforeTicketKey.map(nbfKey -> new KeyMultiValue(nbfKey, Dates.toString(notBefore)));
   }

   private Optional<Date> issuedAt(JWTClaimsSet claimsSet) {
      return Optional.ofNullable(claimsSet.getIssueTime());
   }

   private void verifyIssuedAtIfConfigured(Optional<Date> issuedAt) throws TicketEncodingException {
      if (issuedAt.isEmpty() && this.issuedAtIsMandatory) {
         throw new TicketEncodingException("The issued at claim is configured as mandatory, but is not present in the JWT.");
      }
   }

   private Optional<KeyMultiValue> issuedAtTicketEntryIfConfigured(Date issuedAt) {
      return this.issuedAtTicketKey.map(iatKey -> new KeyMultiValue(iatKey, Dates.toString(issuedAt)));
   }

   private Optional<String> issuer(JWTClaimsSet claimsSet) {
      return Optional.ofNullable(claimsSet.getIssuer());
   }

   private void verifyIssuerIfConfigured(Optional<String> issuer) throws TicketEncodingException {
      if (issuer.isPresent()) {
         if (!this.allowedIssuers.isEmpty()) {
            TicketEncodingException.checkPresent(
               issuer.filter(iss -> this.allowedIssuers.stream().anyMatch(iss::equals)),
               "The iss (issuer) claim \"" + issuer.get() + "\" does not match any of the allowed issuers."
            );
            LOG.debug("The jwt iss claim was verified: it is contained in the configured list of allowed issuers.");
         }
      } else {
         if (this.issuerIsMandatory) {
            throw new TicketEncodingException("The issuer claim is configured as mandatory, but is not present in the JWT.");
         }

         LOG.debug("The iss (issuer) claim is not present and not configured as mandatory. Therefore it is not validated.");
      }
   }

   private Optional<KeyMultiValue> issuerTicketEntryIfConfigured(String issuer) {
      return this.issuerTicketKey.map(issKey -> new KeyMultiValue(issKey, issuer));
   }

   private Optional<List<String>> audience(JWTClaimsSet claimsSet) {
      return Optional.of(claimsSet.getAudience()).filter(audList -> !audList.isEmpty());
   }

   private void verifyAudienceIfConfigured(Optional<List<String>> audience) throws TicketEncodingException {
      if (audience.isPresent() && !audience.get().isEmpty()) {
         if (!this.allowedAudiences.isEmpty()) {
            if (CollectionUtils.intersection(audience.get(), this.allowedAudiences).isEmpty()) {
               throw new TicketEncodingException(
                  "The aud (audience) claim  \""
                     + audience.get()
                     + "\" does not contain an item from the configured list of allowed audiences "
                     + this.allowedAudiences
               );
            }

            LOG.debug(
               "The jwt aud claim was verified: at least one item on the configured list of allowed audiences is contained in the audience list of the claim."
            );
         }
      } else {
         if (this.audienceIsMandatory) {
            throw new TicketEncodingException("The audience claim is configured as mandatory, but is not present in the JWT.");
         }

         LOG.debug("The aud (audience) claim is not present and not configured as mandatory. Therefore it is not validated.");
      }
   }

   private Optional<KeyMultiValue> audienceTicketEntryIfConfigured(List<String> audience) {
      return !audience.isEmpty() ? this.audienceTicketKey.map(audKey -> new KeyMultiValue(audKey, audience)) : Optional.empty();
   }

   private Optional<String> jwtId(JWTClaimsSet claimsSet) {
      return Optional.ofNullable(claimsSet.getJWTID());
   }

   private void verifyJwtIdIfConfigured(Optional<String> jwtId) throws TicketEncodingException {
      if (jwtId.isEmpty() && this.jwtIdIsMandatory) {
         throw new TicketEncodingException("The JWT ID claim is configured as mandatory, but is not present in the JWT.");
      }
   }

   private Optional<KeyMultiValue> jwtIdTicketEntryIfConfigured(String jwtId) {
      return this.jwtIdTicketKey.map(jtiKey -> new KeyMultiValue(jtiKey, jwtId));
   }

   private void addCustomClaims(Ticket ticket, JWTClaimsSet claimsSet) throws TicketEncodingException {
      for (Entry<String, Object> entry : claimsSet.getClaims().entrySet()) {
         if (!this.isRegisteredClaim(entry.getKey())) {
            this.addCustomClaim(ticket, entry);
         }
      }
   }

   private void addCustomClaim(Ticket ticket, Entry<String, Object> entry) throws TicketEncodingException {
      LOG.debug(String.format("Adding custom claim with the following key to the ticket: '%s'.", entry.getKey()));
      if (this.isCustomJsonClaim(entry.getKey())) {
         this.addJsonClaim(ticket, entry);
      } else {
         this.addNonJsonClaim(ticket, entry);
      }
   }

   private boolean isCustomJsonClaim(String key) {
      return this.claimsStoredAsJson.contains(key);
   }

   private void addNonJsonClaim(Ticket ticket, Entry<String, Object> entry) {
      if (entry.getValue() instanceof List<?> values) {
         List<String> stringValues = values.stream().filter(Objects::nonNull).map(Object::toString).collect(Collectors.toList());
         ticket.addKeyMultiValue(new KeyMultiValue(entry.getKey(), stringValues));
      } else {
         ticket.addKeyMultiValue(new KeyMultiValue(entry.getKey(), this.valueToString(entry)));
      }
   }

   private void addJsonClaim(Ticket ticket, Entry<String, Object> entry) throws TicketEncodingException {
      String value = this.valueToString(entry);
      if (StringUtils.isEmpty(value)) {
         LOG.debug(String.format("Not adding empty value for the key '%s' to the ticket", entry.getKey()));
      } else {
         Optional<String> json;
         if ("null".equals(value)) {
            json = Optional.of("null");
         } else {
            json = JsonCoding.tryFromJsonToObject(value).flatMap(JsonCoding::tryToJson);
            if (json.isEmpty()) {
               LOG.debug(String.format("Invalid JSON for claim '%s' with value '%s'.", entry.getKey(), LogUtil.logSafeString(entry.getValue())));
               throw new TicketEncodingException(String.format("Invalid JSON for claim '%s'.", entry.getKey()));
            }
         }

         ticket.addKeyMultiValue(new KeyMultiValue(entry.getKey(), json.get()));
      }
   }

   private String valueToString(Entry<String, Object> entry) {
      Object value = entry.getValue();
      if (value instanceof String) {
         return (String)value;
      }

      LOG.debug(
         "An unexpected object type occurred in the JWT ticket. Class: "
            + (value != null ? value.getClass().getCanonicalName() : "null")
            + ", Key: "
            + entry.getKey()
            + ", Value: "
            + (value != null ? value : "null")
      );
      return value != null ? JsonCoding.toJson(value) : "null";
   }

   private Date nowPlusNotBeforeSkew() {
      return Date.from(this.timeProvider.getCurrentInstant().plusSeconds(this.validNotBeforeSkew));
   }

   @SuppressFBWarnings(justification = "generated code")
   public JwtTicketDecoderImpl.JwtTicketDecoderImplBuilder toBuilder() {
      return new JwtTicketDecoderImpl.JwtTicketDecoderImplBuilder()
         .usernameTicketKey(this.usernameTicketKey)
         .signatureVerifier(this.signatureVerifier)
         .timeProvider(this.timeProvider)
         .expirationTimeIsMandatory(this.expirationTimeIsMandatory)
         .notBeforeIsMandatory(this.notBeforeIsMandatory)
         .issuedAtIsMandatory(this.issuedAtIsMandatory)
         .issuerIsMandatory(this.issuerIsMandatory)
         .audienceIsMandatory(this.audienceIsMandatory)
         .jwtIdIsMandatory(this.jwtIdIsMandatory)
         .isSubjectMandatory(this.isSubjectMandatory)
         .validNotBeforeSkew(this.validNotBeforeSkew)
         .notBeforeTicketKey(this.notBeforeTicketKey)
         .issuedAtTicketKey(this.issuedAtTicketKey)
         .allowedIssuers(this.allowedIssuers)
         .issuerTicketKey(this.issuerTicketKey)
         .allowedAudiences(this.allowedAudiences)
         .audienceTicketKey(this.audienceTicketKey)
         .jwtIdTicketKey(this.jwtIdTicketKey)
         .decrypter(this.decrypter)
         .additionalClaimValidators(this.additionalClaimValidators)
         .claimsStoredAsJson(this.claimsStoredAsJson);
   }

   @SuppressFBWarnings(justification = "generated code")
   public JwtTicketDecoderImpl(
      Optional<String> usernameTicketKey,
      JwtSignatureVerifier signatureVerifier,
      TimeProvider timeProvider,
      boolean expirationTimeIsMandatory,
      boolean notBeforeIsMandatory,
      boolean issuedAtIsMandatory,
      boolean issuerIsMandatory,
      boolean audienceIsMandatory,
      boolean jwtIdIsMandatory,
      boolean isSubjectMandatory,
      int validNotBeforeSkew,
      Optional<String> notBeforeTicketKey,
      Optional<String> issuedAtTicketKey,
      List<String> allowedIssuers,
      Optional<String> issuerTicketKey,
      List<String> allowedAudiences,
      Optional<String> audienceTicketKey,
      Optional<String> jwtIdTicketKey,
      Optional<JwtTicketDecrypter> decrypter,
      List<ClaimsValidator> additionalClaimValidators,
      List<String> claimsStoredAsJson
   ) {
      this.usernameTicketKey = usernameTicketKey;
      this.signatureVerifier = signatureVerifier;
      this.timeProvider = timeProvider;
      this.expirationTimeIsMandatory = expirationTimeIsMandatory;
      this.notBeforeIsMandatory = notBeforeIsMandatory;
      this.issuedAtIsMandatory = issuedAtIsMandatory;
      this.issuerIsMandatory = issuerIsMandatory;
      this.audienceIsMandatory = audienceIsMandatory;
      this.jwtIdIsMandatory = jwtIdIsMandatory;
      this.isSubjectMandatory = isSubjectMandatory;
      this.validNotBeforeSkew = validNotBeforeSkew;
      this.notBeforeTicketKey = notBeforeTicketKey;
      this.issuedAtTicketKey = issuedAtTicketKey;
      this.allowedIssuers = allowedIssuers;
      this.issuerTicketKey = issuerTicketKey;
      this.allowedAudiences = allowedAudiences;
      this.audienceTicketKey = audienceTicketKey;
      this.jwtIdTicketKey = jwtIdTicketKey;
      this.decrypter = decrypter;
      this.additionalClaimValidators = additionalClaimValidators;
      this.claimsStoredAsJson = claimsStoredAsJson;
   }

   @SuppressFBWarnings(justification = "generated code")
   public JwtTicketDecoderImpl(Optional<String> usernameTicketKey, JwtSignatureVerifier signatureVerifier, TimeProvider timeProvider) {
      this.usernameTicketKey = usernameTicketKey;
      this.signatureVerifier = signatureVerifier;
      this.timeProvider = timeProvider;
   }

   @SuppressFBWarnings(justification = "generated code")
   public static class JwtTicketDecoderImplBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private Optional<String> usernameTicketKey;
      @SuppressFBWarnings(justification = "generated code")
      private JwtSignatureVerifier signatureVerifier;
      @SuppressFBWarnings(justification = "generated code")
      private TimeProvider timeProvider;
      @SuppressFBWarnings(justification = "generated code")
      private boolean expirationTimeIsMandatory;
      @SuppressFBWarnings(justification = "generated code")
      private boolean notBeforeIsMandatory;
      @SuppressFBWarnings(justification = "generated code")
      private boolean issuedAtIsMandatory;
      @SuppressFBWarnings(justification = "generated code")
      private boolean issuerIsMandatory;
      @SuppressFBWarnings(justification = "generated code")
      private boolean audienceIsMandatory;
      @SuppressFBWarnings(justification = "generated code")
      private boolean jwtIdIsMandatory;
      @SuppressFBWarnings(justification = "generated code")
      private boolean isSubjectMandatory;
      @SuppressFBWarnings(justification = "generated code")
      private int validNotBeforeSkew;
      @SuppressFBWarnings(justification = "generated code")
      private Optional<String> notBeforeTicketKey;
      @SuppressFBWarnings(justification = "generated code")
      private Optional<String> issuedAtTicketKey;
      @SuppressFBWarnings(justification = "generated code")
      private List<String> allowedIssuers;
      @SuppressFBWarnings(justification = "generated code")
      private Optional<String> issuerTicketKey;
      @SuppressFBWarnings(justification = "generated code")
      private List<String> allowedAudiences;
      @SuppressFBWarnings(justification = "generated code")
      private Optional<String> audienceTicketKey;
      @SuppressFBWarnings(justification = "generated code")
      private Optional<String> jwtIdTicketKey;
      @SuppressFBWarnings(justification = "generated code")
      private Optional<JwtTicketDecrypter> decrypter;
      @SuppressFBWarnings(justification = "generated code")
      private List<ClaimsValidator> additionalClaimValidators;
      @SuppressFBWarnings(justification = "generated code")
      private List<String> claimsStoredAsJson;

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

      @SuppressFBWarnings(justification = "generated code")
      public JwtTicketDecoderImpl.JwtTicketDecoderImplBuilder usernameTicketKey(Optional<String> usernameTicketKey) {
         this.usernameTicketKey = usernameTicketKey;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public JwtTicketDecoderImpl.JwtTicketDecoderImplBuilder signatureVerifier(JwtSignatureVerifier signatureVerifier) {
         this.signatureVerifier = signatureVerifier;
         return this;
      }

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

      @SuppressFBWarnings(justification = "generated code")
      public JwtTicketDecoderImpl.JwtTicketDecoderImplBuilder expirationTimeIsMandatory(boolean expirationTimeIsMandatory) {
         this.expirationTimeIsMandatory = expirationTimeIsMandatory;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public JwtTicketDecoderImpl.JwtTicketDecoderImplBuilder notBeforeIsMandatory(boolean notBeforeIsMandatory) {
         this.notBeforeIsMandatory = notBeforeIsMandatory;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public JwtTicketDecoderImpl.JwtTicketDecoderImplBuilder issuedAtIsMandatory(boolean issuedAtIsMandatory) {
         this.issuedAtIsMandatory = issuedAtIsMandatory;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public JwtTicketDecoderImpl.JwtTicketDecoderImplBuilder issuerIsMandatory(boolean issuerIsMandatory) {
         this.issuerIsMandatory = issuerIsMandatory;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public JwtTicketDecoderImpl.JwtTicketDecoderImplBuilder audienceIsMandatory(boolean audienceIsMandatory) {
         this.audienceIsMandatory = audienceIsMandatory;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public JwtTicketDecoderImpl.JwtTicketDecoderImplBuilder jwtIdIsMandatory(boolean jwtIdIsMandatory) {
         this.jwtIdIsMandatory = jwtIdIsMandatory;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public JwtTicketDecoderImpl.JwtTicketDecoderImplBuilder isSubjectMandatory(boolean isSubjectMandatory) {
         this.isSubjectMandatory = isSubjectMandatory;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public JwtTicketDecoderImpl.JwtTicketDecoderImplBuilder validNotBeforeSkew(int validNotBeforeSkew) {
         this.validNotBeforeSkew = validNotBeforeSkew;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public JwtTicketDecoderImpl.JwtTicketDecoderImplBuilder notBeforeTicketKey(Optional<String> notBeforeTicketKey) {
         this.notBeforeTicketKey = notBeforeTicketKey;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public JwtTicketDecoderImpl.JwtTicketDecoderImplBuilder issuedAtTicketKey(Optional<String> issuedAtTicketKey) {
         this.issuedAtTicketKey = issuedAtTicketKey;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public JwtTicketDecoderImpl.JwtTicketDecoderImplBuilder allowedIssuers(List<String> allowedIssuers) {
         this.allowedIssuers = allowedIssuers;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public JwtTicketDecoderImpl.JwtTicketDecoderImplBuilder issuerTicketKey(Optional<String> issuerTicketKey) {
         this.issuerTicketKey = issuerTicketKey;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public JwtTicketDecoderImpl.JwtTicketDecoderImplBuilder allowedAudiences(List<String> allowedAudiences) {
         this.allowedAudiences = allowedAudiences;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public JwtTicketDecoderImpl.JwtTicketDecoderImplBuilder audienceTicketKey(Optional<String> audienceTicketKey) {
         this.audienceTicketKey = audienceTicketKey;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public JwtTicketDecoderImpl.JwtTicketDecoderImplBuilder jwtIdTicketKey(Optional<String> jwtIdTicketKey) {
         this.jwtIdTicketKey = jwtIdTicketKey;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public JwtTicketDecoderImpl.JwtTicketDecoderImplBuilder decrypter(Optional<JwtTicketDecrypter> decrypter) {
         this.decrypter = decrypter;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public JwtTicketDecoderImpl.JwtTicketDecoderImplBuilder additionalClaimValidators(List<ClaimsValidator> additionalClaimValidators) {
         this.additionalClaimValidators = additionalClaimValidators;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public JwtTicketDecoderImpl.JwtTicketDecoderImplBuilder claimsStoredAsJson(List<String> claimsStoredAsJson) {
         this.claimsStoredAsJson = claimsStoredAsJson;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public JwtTicketDecoderImpl build() {
         return new JwtTicketDecoderImpl(
            this.usernameTicketKey,
            this.signatureVerifier,
            this.timeProvider,
            this.expirationTimeIsMandatory,
            this.notBeforeIsMandatory,
            this.issuedAtIsMandatory,
            this.issuerIsMandatory,
            this.audienceIsMandatory,
            this.jwtIdIsMandatory,
            this.isSubjectMandatory,
            this.validNotBeforeSkew,
            this.notBeforeTicketKey,
            this.issuedAtTicketKey,
            this.allowedIssuers,
            this.issuerTicketKey,
            this.allowedAudiences,
            this.audienceTicketKey,
            this.jwtIdTicketKey,
            this.decrypter,
            this.additionalClaimValidators,
            this.claimsStoredAsJson
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "JwtTicketDecoderImpl.JwtTicketDecoderImplBuilder(usernameTicketKey="
            + this.usernameTicketKey
            + ", signatureVerifier="
            + this.signatureVerifier
            + ", timeProvider="
            + this.timeProvider
            + ", expirationTimeIsMandatory="
            + this.expirationTimeIsMandatory
            + ", notBeforeIsMandatory="
            + this.notBeforeIsMandatory
            + ", issuedAtIsMandatory="
            + this.issuedAtIsMandatory
            + ", issuerIsMandatory="
            + this.issuerIsMandatory
            + ", audienceIsMandatory="
            + this.audienceIsMandatory
            + ", jwtIdIsMandatory="
            + this.jwtIdIsMandatory
            + ", isSubjectMandatory="
            + this.isSubjectMandatory
            + ", validNotBeforeSkew="
            + this.validNotBeforeSkew
            + ", notBeforeTicketKey="
            + this.notBeforeTicketKey
            + ", issuedAtTicketKey="
            + this.issuedAtTicketKey
            + ", allowedIssuers="
            + this.allowedIssuers
            + ", issuerTicketKey="
            + this.issuerTicketKey
            + ", allowedAudiences="
            + this.allowedAudiences
            + ", audienceTicketKey="
            + this.audienceTicketKey
            + ", jwtIdTicketKey="
            + this.jwtIdTicketKey
            + ", decrypter="
            + this.decrypter
            + ", additionalClaimValidators="
            + this.additionalClaimValidators
            + ", claimsStoredAsJson="
            + this.claimsStoredAsJson
            + ")";
      }
   }
}
