package com.airlock.iam.common.application.configuration.jwt;

import com.airlock.iam.base.misc.time.RealTimeProvider;
import com.airlock.iam.common.domain.service.jwt.JwtTicketEncoderDecoderSettingsValidator;
import com.airlock.iam.common.domain.service.jwt.JwtTicketEncoderImpl;
import com.airlock.iam.common.domain.service.jwt.encryption.JwtTicketEncrypter;
import com.airlock.iam.common.domain.service.jwt.signature.JwtSigner;
import com.airlock.iam.core.domain.service.jwt.JwtTicketEncoder;
import com.airlock.iam.core.misc.util.ticket.Ticket;
import com.airlock.iam.core.misc.util.ticket.TicketEncodingException;
import com.airlock.iam.plugin.api.annotation.config.ConfigurationValidator;
import com.airlock.iam.plugin.api.annotation.config.Groups;
import com.airlock.iam.plugin.api.annotation.config.Plugin;
import com.airlock.iam.plugin.api.annotation.lifecycle.AfterConfigActivation;
import com.airlock.iam.plugin.api.annotation.property.BooleanProperty;
import com.airlock.iam.plugin.api.annotation.property.GroupProperty;
import com.airlock.iam.plugin.api.annotation.property.IntegerProperty;
import com.airlock.iam.plugin.api.annotation.property.PluginProperty;
import com.airlock.iam.plugin.api.annotation.property.StringListProperty;
import com.airlock.iam.plugin.api.annotation.property.StringProperty;
import com.airlock.iam.plugin.api.config.ConfigurationContext;
import com.airlock.iam.plugin.api.config.ConfigurationEnvironment;
import com.airlock.iam.plugin.api.config.Environment;
import com.airlock.iam.plugin.api.config.PluginConfiguration;
import com.airlock.iam.plugin.api.exception.PropertyConstraintViolationException;
import com.airlock.iam.plugin.api.exception.PropertyTypeMismatchException;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.annotation.Nullable;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

@Plugin(
   description = "<p>Configures the JWT (JSON Web Token) ticket encoder.</p><p>The <em>encoder</em> protects the integrity of the ticket by a MAC or signature according to the configured signer settings. It also has the ability to (optionally) encrypt the ticket if encrypter settings are configured.</p>",
   displayName = "JWT Ticket Encoder"
)
@Groups(groupSequence = {"Claim Settings", "Security Settings"}, openGroups = {"Claim Settings", "Security Settings"})
public class JwtTicketEncoderSettings implements JwtTicketEncoder {
   public static final String CLAIM_SETTINGS = "Claim Settings";
   @StringProperty(
      description = "This defines the ticket value to be set as the subject field (<code>sub</code>) of the JWT. The corresponding key-value pair from the ticket is not added as a separate JWT claim.",
      optional = false,
      suggestedValues = "username"
   )
   @GroupProperty(groupIdentifier = "Claim Settings")
   private String usernameTicketKey;
   @StringProperty(
      description = "The value for the issuer field (<code>iss</code>) in the JWT. If this value is set, the ticket must not contain a value for the key <code>iss</code>.",
      optional = true,
      exampleValues = "Airlock IAM"
   )
   @GroupProperty(groupIdentifier = "Claim Settings")
   private String issuer;
   @StringListProperty(
      description = "The values for the audience field (<code>aud</code>) in the JWT. If a value is set, the ticket must not contain a value for the key <code>aud</code>. <b>Note:</b> If a single value is configured, it will be sent as string. If multiple values are configured an array is sent. This behaviour cannot be changed by specifying the <code>aud</code>  claim in claimsStoredAsArray.",
      optional = true,
      exampleElements = {"consumingApplicationA", "consumingApplicationB"}
   )
   @GroupProperty(groupIdentifier = "Claim Settings")
   private List<String> audience;
   @IntegerProperty(
      description = "<p>Expiration time in seconds after which the JWT must not be accepted. If configured, a potentially defined expiration time in the ticket is overwritten. When left empty, the ticket's expiration date - if present - is unchanged. </p><p>Note: For security reasons, a short expiration time is preferable.</p>",
      displayName = "Expiration Time [s]",
      optional = true,
      minValue = 0,
      exampleValues = {5, 60, 3600}
   )
   @GroupProperty(groupIdentifier = "Claim Settings")
   private Integer expirationTime;
   @IntegerProperty(
      description = "When generating a JWT, a JWT 'nbf' (not before) claim is added. This claim identifies the time before which the JWT must not be accepted for processing. To determine the 'nbf' in the JWT, the number of seconds configured in this property are subtracted from the JWT issue time. The motivation to set a time in the past is to avoid clock synchronization problems with the JWT receiver. ",
      displayName = "Valid Not Before Skew [s]",
      optional = true,
      defaultValue = 5,
      minValue = 0,
      exampleValues = {5, 60, 3600}
   )
   @GroupProperty(groupIdentifier = "Claim Settings")
   private int validNotBeforeSkew;
   @BooleanProperty(
      description = "The \"jti\" (JWT ID) claim provides a unique identifier for the JWT. This claim can be used to prevent the JWT from being replayed. A new JWT ID is applied only, when no ticket ID is set and no custom jti claim is defined. ",
      displayName = "Enforce JWT ID",
      optional = true,
      defaultValue = "true"
   )
   @GroupProperty(groupIdentifier = "Claim Settings")
   private boolean enforceJwtId;
   @StringListProperty(
      description = "The keys of ticket fields that should be stored as array in the JWT. If such a key does not exist in the ticket an empty array is written into the JWT. <b>Note:</b> It is not allowed to specify registered claims here. Registered claims are always propagated as specified in RFC 7519. The <code>aud</code> claim will be sent as string if it is a single value and as array otherwise.",
      optional = true,
      exampleElements = "roles"
   )
   @GroupProperty(groupIdentifier = "Claim Settings")
   private List<String> claimsStoredAsArray;
   public static final String CLAIMS_STORED_AS_ARRAY_PROPERTY_NAME = "claimsStoredAsArray";
   @StringListProperty(
      description = "The keys of ticket fields that should be stored as JSON in the JWT. If such a key does not exist in the ticket, the corresponding key is not written into the JWT. The claim name is always the ticket key. If the JSON is invalid, an exception is thrown. <br><b>Note:</b> It is not allowed to specify registered claims here. Registered claims are always propagated as specified in RFC 7519. The <code>aud</code> claim will be sent as string if it is a single value and as array otherwise.",
      displayName = "Claims Stored As JSON",
      optional = true,
      unique = true,
      exampleElements = "orgs"
   )
   @GroupProperty(groupIdentifier = "Claim Settings")
   private List<String> claimsStoredAsJson;
   public static final String CLAIMS_STORED_AS_JSON_PROPERTY_NAME = "claimsStoredAsJson";
   @PluginProperty(description = "The settings that are used for signing the JWT.", optional = false)
   @GroupProperty(groupIdentifier = "Security Settings")
   private JwtSigner signer;
   @PluginProperty(
      description = "The settings that are used for encrypting the JWT. If no plugin is configured the JWT will be sent unencrypted.",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Security Settings")
   private JwtTicketEncrypter encrypter;
   private JwtTicketEncoderImpl ticketEncoder;

   public JwtTicketEncoderSettings(
      String usernameTicketKey,
      String issuer,
      List<String> audience,
      Integer expirationTime,
      int validNotBeforeSkew,
      boolean enforceJwtId,
      List<String> claimsStoredAsArray,
      List<String> claimsStoredAsJson,
      JwtSigner signer,
      JwtTicketEncrypter encrypter
   ) {
      this.usernameTicketKey = usernameTicketKey;
      this.issuer = issuer;
      this.audience = audience;
      this.expirationTime = expirationTime;
      this.validNotBeforeSkew = validNotBeforeSkew;
      this.enforceJwtId = enforceJwtId;
      this.claimsStoredAsArray = claimsStoredAsArray;
      this.claimsStoredAsJson = claimsStoredAsJson;
      this.signer = signer;
      this.encrypter = encrypter;
   }

   @ConfigurationValidator(localOnly = true)
   public static void ensureClaimsStoredAsArrayContainsNoRegisteredClaims(
      PluginConfiguration pc, ConfigurationEnvironment env, ConfigurationContext context, Environment valenv
   ) throws PropertyConstraintViolationException, PropertyTypeMismatchException {
      JwtTicketEncoderDecoderSettingsValidator.ensureListDoesNotContainRegisteredClaims(pc, env, context, "claimsStoredAsArray");
   }

   @ConfigurationValidator(localOnly = true)
   public static void ensureClaimsStoredAsJsonContainsNoRegisteredClaims(
      PluginConfiguration pc, ConfigurationEnvironment env, ConfigurationContext context, Environment valenv
   ) throws PropertyConstraintViolationException, PropertyTypeMismatchException {
      JwtTicketEncoderDecoderSettingsValidator.ensureListDoesNotContainRegisteredClaims(pc, env, context, "claimsStoredAsJson");
   }

   @ConfigurationValidator(localOnly = true)
   public static void ensureNoClaimsAreConfiguredAsBothArrayAndJsonClaims(
      PluginConfiguration pc, ConfigurationEnvironment env, ConfigurationContext context, Environment valenv
   ) throws PropertyConstraintViolationException, PropertyTypeMismatchException {
      JwtTicketEncoderDecoderSettingsValidator.ensureNoClaimsAreConfiguredAsBothArrayAndJsonClaims(
         pc, env, context, "claimsStoredAsArray", "claimsStoredAsJson"
      );
   }

   @AfterConfigActivation
   void afterConfigActivation() {
      this.ticketEncoder = JwtTicketEncoderImpl.builder(this.usernameTicketKey, this.signer, new RealTimeProvider())
         .expirationTime(this.expirationTime)
         .validNotBeforeSkew(this.validNotBeforeSkew)
         .enforceJwtId(this.enforceJwtId)
         .issuer(Optional.ofNullable(this.issuer))
         .audience(this.audience)
         .claimsStoredAsArray(this.claimsStoredAsArray)
         .claimsStoredAsJson(this.claimsStoredAsJson)
         .encrypter(Optional.ofNullable(this.encrypter))
         .build();
   }

   @Override
   public String encode(Ticket ticket, @Nullable String urlEncodingScheme) throws TicketEncodingException {
      return Objects.requireNonNull(this.ticketEncoder, "AfterConfigActivation has not been called").encode(ticket, urlEncodingScheme);
   }

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

   @SuppressFBWarnings(justification = "generated code")
   public JwtTicketEncoderSettings() {
   }

   @SuppressFBWarnings(justification = "generated code")
   public static class JwtTicketEncoderSettingsBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private String usernameTicketKey;
      @SuppressFBWarnings(justification = "generated code")
      private String issuer;
      @SuppressFBWarnings(justification = "generated code")
      private List<String> audience;
      @SuppressFBWarnings(justification = "generated code")
      private Integer expirationTime;
      @SuppressFBWarnings(justification = "generated code")
      private int validNotBeforeSkew;
      @SuppressFBWarnings(justification = "generated code")
      private boolean enforceJwtId;
      @SuppressFBWarnings(justification = "generated code")
      private List<String> claimsStoredAsArray;
      @SuppressFBWarnings(justification = "generated code")
      private List<String> claimsStoredAsJson;
      @SuppressFBWarnings(justification = "generated code")
      private JwtSigner signer;
      @SuppressFBWarnings(justification = "generated code")
      private JwtTicketEncrypter encrypter;

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

      @SuppressFBWarnings(justification = "generated code")
      public JwtTicketEncoderSettings.JwtTicketEncoderSettingsBuilder usernameTicketKey(String usernameTicketKey) {
         this.usernameTicketKey = usernameTicketKey;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public JwtTicketEncoderSettings.JwtTicketEncoderSettingsBuilder issuer(String issuer) {
         this.issuer = issuer;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public JwtTicketEncoderSettings.JwtTicketEncoderSettingsBuilder audience(List<String> audience) {
         this.audience = audience;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public JwtTicketEncoderSettings.JwtTicketEncoderSettingsBuilder expirationTime(Integer expirationTime) {
         this.expirationTime = expirationTime;
         return this;
      }

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

      @SuppressFBWarnings(justification = "generated code")
      public JwtTicketEncoderSettings.JwtTicketEncoderSettingsBuilder enforceJwtId(boolean enforceJwtId) {
         this.enforceJwtId = enforceJwtId;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public JwtTicketEncoderSettings.JwtTicketEncoderSettingsBuilder claimsStoredAsArray(List<String> claimsStoredAsArray) {
         this.claimsStoredAsArray = claimsStoredAsArray;
         return this;
      }

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

      @SuppressFBWarnings(justification = "generated code")
      public JwtTicketEncoderSettings.JwtTicketEncoderSettingsBuilder signer(JwtSigner signer) {
         this.signer = signer;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public JwtTicketEncoderSettings.JwtTicketEncoderSettingsBuilder encrypter(JwtTicketEncrypter encrypter) {
         this.encrypter = encrypter;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public JwtTicketEncoderSettings build() {
         return new JwtTicketEncoderSettings(
            this.usernameTicketKey,
            this.issuer,
            this.audience,
            this.expirationTime,
            this.validNotBeforeSkew,
            this.enforceJwtId,
            this.claimsStoredAsArray,
            this.claimsStoredAsJson,
            this.signer,
            this.encrypter
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "JwtTicketEncoderSettings.JwtTicketEncoderSettingsBuilder(usernameTicketKey="
            + this.usernameTicketKey
            + ", issuer="
            + this.issuer
            + ", audience="
            + this.audience
            + ", expirationTime="
            + this.expirationTime
            + ", validNotBeforeSkew="
            + this.validNotBeforeSkew
            + ", enforceJwtId="
            + this.enforceJwtId
            + ", claimsStoredAsArray="
            + this.claimsStoredAsArray
            + ", claimsStoredAsJson="
            + this.claimsStoredAsJson
            + ", signer="
            + this.signer
            + ", encrypter="
            + this.encrypter
            + ")";
      }
   }
}
