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

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.client.OpenIdConnectClaim;
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.PluginListProperty;
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.ConfigurationException;
import com.airlock.iam.plugin.api.exception.PropertyConfigurationException;
import com.airlock.iam.plugin.api.exception.PropertyConstraintViolationException;
import com.airlock.iam.plugin.api.exception.PropertyTypeMismatchException;
import com.airlock.iam.plugin.api.valuereader.PluginListPropertyValueReader;
import com.airlock.iam.plugin.api.valuereader.PluginPropertyValueReader;
import com.airlock.iam.plugin.api.valuereader.StringPropertyValueReader;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;

@Plugin(
   description = "<p>Configuration plugin for the OpenID Connect ID Token Claims.</p><p>The 'iss' (issuer) claim is always filled from the issuer configured in the top level configuration.</p>",
   displayName = "OIDC ID Token Claims"
)
@PluginLicenseRestriction(moduleAffiliation = "OAuthServer")
public class OpenIDConnectClaimsConfiguration {
   @PluginListProperty(
      description = "<p>List of custom claims added to the OpenID Connect ID Token as additional claims.</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>auth_time</li>\n<li>nonce</li>\n<li>acr</li>\n</ul>\n<p>Note: When \"Persist Claims\" is disabled, custom claims are collected when the ID Token is requested by\nan OpenID Connect client and not when the ID Token is issued. Therefore the values of the custom claims may\nchange between issue and request time.<p>",
      optional = true
   )
   private List<CustomClaimConfig> customClaims;
   public static final String CUSTOM_CLAIMS_FIELD = "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 ID Token.</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";

   @ConfigurationValidator
   static void checkUniqueClaimIds(PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context, Environment validationEnvironment) throws ConfigurationException {
      List<PluginConfiguration> customClaims = PluginListPropertyValueReader.effectivePluginListPropertyValue(config, "customClaims", env, context);
      if (!customClaims.isEmpty()) {
         Set<String> reservedIdentifiers = new HashSet<>();
         Set<String> reservedIdentifiersWithCondition = new HashSet<>();

         for (OpenIdConnectClaim claim : OpenIdConnectClaim.values()) {
            reservedIdentifiers.add(claim.getClaimName());
         }

         List<PluginConfiguration> distributedClaims = PluginListPropertyValueReader.effectivePluginListPropertyValue(config, "distributedClaims", env, context);
         if (!distributedClaims.isEmpty()) {
            reservedIdentifiers.addAll(List.of("_claim_sources", "_claim_names"));
         }

         for (PluginConfiguration claimConfig : customClaims) {
            String claimId = StringPropertyValueReader.effectiveStringConfigValue(claimConfig, "claimName", env, context);
            if (StringUtils.isBlank(claimId)) {
               return;
            }

            if (!isConditionalClaim(claimConfig, env, context) || reservedIdentifiers.contains(claimId)) {
               if (reservedIdentifiers.contains(claimId) || reservedIdentifiersWithCondition.contains(claimId)) {
                  throw new PropertyConstraintViolationException(
                     "claimName",
                     claimConfig,
                     env,
                     context,
                     "The claim name of this OpenID Connect ID Token Claim is already used by a standard claim or another custom claim. Please choose a unique name."
                  );
               }

               reservedIdentifiers.add(claimId);
            } else {
               reservedIdentifiersWithCondition.add(claimId);
            }
         }
      }
   }

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

   private static boolean isConditionalClaim(PluginConfiguration customClaim, ConfigurationEnvironment env, ConfigurationContext context) throws PropertyTypeMismatchException {
      return PluginPropertyValueReader.effectivePluginPropertyValue(customClaim, "claimCondition", env, context) != null;
   }

   public List<CustomClaimConfig> getCustomClaims() {
      return this.customClaims;
   }

   public List<DistributedClaimConfig> getDistributedClaims() {
      return this.distributedClaims;
   }

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

   @SuppressFBWarnings(justification = "generated code")
   public OpenIDConnectClaimsConfiguration(List<CustomClaimConfig> customClaims, List<DistributedClaimConfig> distributedClaims) {
      this.customClaims = customClaims;
      this.distributedClaims = distributedClaims;
   }

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

   @SuppressFBWarnings(justification = "generated code")
   public static class OpenIDConnectClaimsConfigurationBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private List<CustomClaimConfig> customClaims;
      @SuppressFBWarnings(justification = "generated code")
      private List<DistributedClaimConfig> distributedClaims;

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

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

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

      @SuppressFBWarnings(justification = "generated code")
      public OpenIDConnectClaimsConfiguration build() {
         return new OpenIDConnectClaimsConfiguration(this.customClaims, this.distributedClaims);
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "OpenIDConnectClaimsConfiguration.OpenIDConnectClaimsConfigurationBuilder(customClaims="
            + this.customClaims
            + ", distributedClaims="
            + this.distributedClaims
            + ")";
      }
   }
}
