package com.airlock.iam.oauth2.application.configuration.token;

import com.airlock.iam.common.domain.model.jwt.SignatureVerificationInformation;
import com.airlock.iam.oauth2.api.application.configuration.claims.CustomClaimConfig;
import com.airlock.iam.oauth2.application.configuration.claims.DistributedClaimConfig;
import com.airlock.iam.oauth2.application.configuration.validation.OAuth2ClaimsUniquenessValidator;
import com.airlock.iam.oauth2.domain.model.claims.DistributedClaim;
import com.airlock.iam.oauth2.domain.model.claims.DistributedClaims;
import com.airlock.iam.oauth2.domain.model.claims.ScopeFormat;
import com.airlock.iam.oauth2.domain.model.settings.JwtAccessTokenFormatSettings;
import com.airlock.iam.oauth2.domain.service.token.OAuth2JwtAccessTokenGenerator;
import com.airlock.iam.oauth2.domain.service.token.OAuth2TokenGenerator;
import com.airlock.iam.plugin.api.annotation.config.ConfigurationValidator;
import com.airlock.iam.plugin.api.annotation.config.Plugin;
import com.airlock.iam.plugin.api.annotation.license.PluginLicenseRestriction;
import com.airlock.iam.plugin.api.annotation.property.BooleanProperty;
import com.airlock.iam.plugin.api.annotation.property.IntegerProperty;
import com.airlock.iam.plugin.api.annotation.property.PluginListProperty;
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.PropertyConfigurationException;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.time.Duration;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

@Plugin(
   description = "JWT Access Tokens allow to add metadata to the token. This is useful when clients send Access Tokens to third parties (e.g. resource servers). These third parties can read metadata directly from the token (e.g. validity time), without making any requests (e.g. Token Introspection) to the authorization server.</p><b>Security Warning:</b> Using this feature has a major security drawback: Revoked / invalidated tokens might still be considered valid by third parties.</p>Third parties <b>must</b> validate the JWT according to current security best practices (signature validation, validation of <a target=\"_blank\" href=\"https://tools.ietf.org/html/rfc7519#section-4.1\">Registered Claims</a>, etc.).</p>The JWT always includes the following claims:<ul><li>iat - Issue Time</li><li>nbf - Not Valid Before</li><li>exp - Expiration Time (claim is not included if token has infinite validity)</li><li>jti - JWT ID (random value)</li><li>random - A random value defined through \"OAuth 2.0 Token Generator Settings\" for the token entropy</li><li>scope - A JSON array defining the scope of the access token</li></ul>",
   displayName = "JWT Access Token Format"
)
@PluginLicenseRestriction(moduleAffiliation = "OAuthServer")
public final class JwtAccessTokenFormatConfig implements AccessTokenFormatConfig {
   @BooleanProperty(description = "If enabled, the username will be included as subject claim (<code>sub</code>).", optional = true, defaultValue = "false")
   private boolean includeSubjectClaim;
   @StringProperty(description = "The issuer claim (<code>iss</code>). If left empty the claim will not be included.", optional = true)
   private String issuer;
   @StringListProperty(
      description = "<p>The audience claim (<code>aud</code>) to include. If left empty the claim will not be included.</p>\n<p>If there is one audience, the claim is written as a string, for multiple values as an array.</p>",
      optional = true,
      exampleElements = {"https://airlock.com/auth-login/", "https://host1:443/path"}
   )
   private List<String> audience;
   @IntegerProperty(
      description = "The skew that will be subtracted from the token creation date to define the not-before claim (<code>nbf</code>).",
      displayName = "Not Valid Before Skew [s]",
      exampleValues = {0, 5, 10},
      defaultValue = 5,
      optional = true
   )
   private int notValidBeforeSkew;
   @BooleanProperty(
      description = "When enabled, scopes are written as space-separated string claim (as required by <a href=\"https://www.rfc-editor.org/rfc/rfc9068#name-authorization-claims\" target=\"_blank\">RFC 9086</a>). Otherwise, the <code>scope</code> claim will be issued as a string array, even if it only contains a single value.",
      optional = true,
      defaultValue = "true"
   )
   private boolean scopesAsSpaceSeparatedString;
   @PluginListProperty(
      description = "<p>Custom claims to include in the JWT.</p>\n<p>Multiple claims with the same name can be configured if each has a claim condition which ensures that only\none of them will be included at runtime.</p>\nThe following claims are automatically set by Airlock IAM and therefore will be ignored if defined as custom claim.\n<ul>\n<li><code>iss</code></li>\n<li><code>aud</code></li>\n<li><code>exp</code></li>\n<li><code>nbf</code></li>\n<li><code>iat</code></li>\n<li><code>jti</code></li>\n<li><code>random</code></li>\n<li><code>scope</code></li>\n</ul>\n<p>Note: When \"Persist Claims\" is disabled, custom claims are collected when the Access Token is requested by\nan OAuth 2.0 client and not when the Access Token is issued. Therefore the values of the custom claims may\nchange between issue and request time.<p>",
      minElements = 1,
      optional = true
   )
   private List<CustomClaimConfig> customClaims;
   public static final String CUSTOM_CLAIMS_PROPERTY = "customClaims";
   @PluginListProperty(
      description = "<p><a href=\"https://openid.net/specs/openid-connect-core-1_0.html#AggregatedDistributedClaims\" target=\"_blank\">Distributed Claims</a> to add to the JWT.</p><p>These claims allow providing a URL to a 3rd party claims provider in the response where additional claims may be obtained.</p>",
      optional = true,
      minElements = 1
   )
   private List<DistributedClaimConfig> distributedClaims;
   private static final String DISTRIBUTED_CLAIMS_PROPERTY = "distributedClaims";
   @PluginProperty(
      description = "The signature of the Access Token.<p><b>Security Warning:</b> The signature must be verified by the consumer of the JWT before the content is interpreted. When using \"JWT Access Token No Signature\", the consumer <b>must not</b> trust the content of the JWT and therefore not use it as authenticated data.</p><p><b>Security Warning:</b> Verifying the signature and validity of the self-contained JWT is not sufficient to validate the access token. The access token might have been revoked and thus consumers must verify the validity of the access token (e.g. using Token Introspection) before being used for access control.</p>",
      optional = false
   )
   private JwtAccessTokenSignature signature;
   public static final String SIGNATURE_PROPERTY = "signature";

   @ConfigurationValidator
   public static void validateUniqueClaimNames(
      PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context, Environment validationEnvironment
   ) throws PropertyConfigurationException {
      OAuth2ClaimsUniquenessValidator.validateUniqueDistributedClaimNames(config, env, context, "distributedClaims");
      OAuth2ClaimsUniquenessValidator.validateUniqueCustomClaimNames(config, env, context, "customClaims", "distributedClaims");
   }

   @Override
   public Optional<SignatureVerificationInformation> signatureVerificationInformation() {
      return this.signature.getOptionalSignatureVerificationInformation();
   }

   @Override
   public OAuth2TokenGenerator accessTokenGenerator(int tokenRandomPartLength, OAuth2TokenGenerator randomTokenGenerator, int accessTokenExpiresIn) {
      return OAuth2JwtAccessTokenGenerator.builder()
         .signer(this.signature.getOptionalSigner().orElse(null))
         .tokenRandomPartLength(tokenRandomPartLength)
         .scopeFormat(this.scopesAsSpaceSeparatedString ? ScopeFormat.SPACE_SEPARATED_STRING : ScopeFormat.STRING_ARRAY)
         .expiresIn(Duration.ofSeconds(accessTokenExpiresIn))
         .notValidBeforeSkew(Duration.ofSeconds(this.notValidBeforeSkew))
         .build();
   }

   @Override
   public Optional<JwtAccessTokenFormatSettings> settings() {
      return Optional.of(new JwtAccessTokenFormatSettings(this.includeSubjectClaim, this.issuer, this.audience, this.distributedClaims(), this.customClaims));
   }

   private Optional<DistributedClaims> distributedClaims() {
      return this.distributedClaims.isEmpty()
         ? Optional.empty()
         : Optional.of(
            new DistributedClaims(
               this.distributedClaims.stream().map(c -> new DistributedClaim(c.getClaimNames(), c.getEndpointUrl())).collect(Collectors.toList())
            )
         );
   }

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

   @SuppressFBWarnings(justification = "generated code")
   public JwtAccessTokenFormatConfig(
      boolean includeSubjectClaim,
      String issuer,
      List<String> audience,
      int notValidBeforeSkew,
      boolean scopesAsSpaceSeparatedString,
      List<CustomClaimConfig> customClaims,
      List<DistributedClaimConfig> distributedClaims,
      JwtAccessTokenSignature signature
   ) {
      this.includeSubjectClaim = includeSubjectClaim;
      this.issuer = issuer;
      this.audience = audience;
      this.notValidBeforeSkew = notValidBeforeSkew;
      this.scopesAsSpaceSeparatedString = scopesAsSpaceSeparatedString;
      this.customClaims = customClaims;
      this.distributedClaims = distributedClaims;
      this.signature = signature;
   }

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

   @SuppressFBWarnings(justification = "generated code")
   public static class JwtAccessTokenFormatConfigBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private boolean includeSubjectClaim;
      @SuppressFBWarnings(justification = "generated code")
      private String issuer;
      @SuppressFBWarnings(justification = "generated code")
      private List<String> audience;
      @SuppressFBWarnings(justification = "generated code")
      private int notValidBeforeSkew;
      @SuppressFBWarnings(justification = "generated code")
      private boolean scopesAsSpaceSeparatedString;
      @SuppressFBWarnings(justification = "generated code")
      private List<CustomClaimConfig> customClaims;
      @SuppressFBWarnings(justification = "generated code")
      private List<DistributedClaimConfig> distributedClaims;
      @SuppressFBWarnings(justification = "generated code")
      private JwtAccessTokenSignature signature;

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

      @SuppressFBWarnings(justification = "generated code")
      public JwtAccessTokenFormatConfig.JwtAccessTokenFormatConfigBuilder includeSubjectClaim(boolean includeSubjectClaim) {
         this.includeSubjectClaim = includeSubjectClaim;
         return this;
      }

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

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

      @SuppressFBWarnings(justification = "generated code")
      public JwtAccessTokenFormatConfig.JwtAccessTokenFormatConfigBuilder notValidBeforeSkew(int notValidBeforeSkew) {
         this.notValidBeforeSkew = notValidBeforeSkew;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public JwtAccessTokenFormatConfig.JwtAccessTokenFormatConfigBuilder scopesAsSpaceSeparatedString(boolean scopesAsSpaceSeparatedString) {
         this.scopesAsSpaceSeparatedString = scopesAsSpaceSeparatedString;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public JwtAccessTokenFormatConfig.JwtAccessTokenFormatConfigBuilder customClaims(List<CustomClaimConfig> customClaims) {
         this.customClaims = customClaims;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public JwtAccessTokenFormatConfig.JwtAccessTokenFormatConfigBuilder distributedClaims(List<DistributedClaimConfig> distributedClaims) {
         this.distributedClaims = distributedClaims;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public JwtAccessTokenFormatConfig.JwtAccessTokenFormatConfigBuilder signature(JwtAccessTokenSignature signature) {
         this.signature = signature;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public JwtAccessTokenFormatConfig build() {
         return new JwtAccessTokenFormatConfig(
            this.includeSubjectClaim,
            this.issuer,
            this.audience,
            this.notValidBeforeSkew,
            this.scopesAsSpaceSeparatedString,
            this.customClaims,
            this.distributedClaims,
            this.signature
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "JwtAccessTokenFormatConfig.JwtAccessTokenFormatConfigBuilder(includeSubjectClaim="
            + this.includeSubjectClaim
            + ", issuer="
            + this.issuer
            + ", audience="
            + this.audience
            + ", notValidBeforeSkew="
            + this.notValidBeforeSkew
            + ", scopesAsSpaceSeparatedString="
            + this.scopesAsSpaceSeparatedString
            + ", customClaims="
            + this.customClaims
            + ", distributedClaims="
            + this.distributedClaims
            + ", signature="
            + this.signature
            + ")";
      }
   }
}
