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

import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.common.api.application.configuration.claims.ClaimsValidatorConfig;
import com.airlock.iam.flow.api.application.configuration.redflag.RedFlagConfig;
import com.airlock.iam.oauth2.application.configuration.accountlinking.AccountLinkingSelfServiceConfig;
import com.airlock.iam.oauth2.application.configuration.accountregistration.AccountRegistrationConfig;
import com.airlock.iam.oauth2.application.configuration.client.AbstractOAuth2RemoteResourceConfig;
import com.airlock.iam.oauth2.application.configuration.client.OAuth2ClientResourceRequest;
import com.airlock.iam.oauth2.application.configuration.client.OAuth2NoPostLogoutRedirectUriConfig;
import com.airlock.iam.oauth2.application.configuration.client.OAuth2PostLogoutRedirectUriConfig;
import com.airlock.iam.oauth2.application.configuration.client.OAuth2RedirectUriConfig;
import com.airlock.iam.oauth2.application.configuration.client.OAuth2RequestBuilderConfig;
import com.airlock.iam.oauth2.application.configuration.logging.OAuth2LoggingSettings;
import com.airlock.iam.oauth2.domain.model.client.AudienceClaimValidationMethod;
import com.airlock.iam.oauth2.domain.model.client.OpenIdConnectClaim;
import com.airlock.iam.oauth2.domain.model.client.communication.OpenIDConnectIdTokenSignatureValidator;
import com.airlock.iam.oauth2.domain.model.codegrant.PromptValue;
import com.airlock.iam.plugin.api.annotation.config.ConfigurationValidator;
import com.airlock.iam.plugin.api.annotation.property.BooleanProperty;
import com.airlock.iam.plugin.api.annotation.property.EnumProperty;
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.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.ConfigurationException;
import com.airlock.iam.plugin.api.exception.PropertyConfigurationException;
import com.airlock.iam.plugin.api.exception.PropertyConstraintViolationException;
import com.airlock.iam.plugin.api.valuereader.EnumPropertyValueReader;
import com.airlock.iam.plugin.api.valuereader.PluginListPropertyValueReader;
import com.airlock.iam.plugin.api.valuereader.StringListPropertyValueReader;
import com.airlock.iam.plugin.api.valuereader.StringPropertyValueReader;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.nimbusds.oauth2.sdk.util.StringUtils;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.net.URI;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.collections4.CollectionUtils;

public abstract class OpenIDConnectSsoFlowClientSettingsBase extends OAuth2SsoFlowClientSettingsBase {
   static final String LOGOUT = "Logout";
   static final String ID_TOKEN = "ID Token";
   static final String CUSTOM_AUDIENCE_CLAIM = "Custom Audience Claim";
   @StringListProperty(
      description = "Scopes to request from the authorization endpoint. The scope 'openid' is always requested.<p>Scopes may only contain the following characters: <code>0-9, A-Z, a-z, !, #, $, %, &, ', (, ), *, +, ',', -, ., /, :, ;, &lt;, &gt;, =, ?, @, [, ], ^, _, `, {, }, |, ~</code></p>",
      validationRegex = "[\\x21\\x23-\\x5B\\x5D-\\x7E]+",
      exampleElements = "userinfo",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Authorization Request")
   private List<String> scopesToRequest;
   @PluginListProperty(
      description = "Allows requesting specific claims using the <em>claims</em> parameter as specified by the <a target=\"_blank\" href=\"https://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter\">OpenID Connect Core Specification</a>. The claims can be requested from the UserInfo Endpoint and/or in the ID Token. <b>Note</b> that support for the claims parameter by authorization servers is optional. <p>If the client wants to validate claims (e.g. returned in the ID Token), \"Additional Claim Validators\" can be configured. <b>Note</b> that the acr claim is validated separately. Refer to the property description of \"Validate ACR Claim\".</p>",
      displayName = "Claims To Request",
      minElements = 1,
      optional = true
   )
   @GroupProperty(groupIdentifier = "Authorization Request")
   private List<ClaimRequestConfig> claimsToRequest;
   private static final String CLAIMS_TO_REQUEST_FIELD = "claimsToRequest";
   private static final String CLAIMS_TO_REQUEST_DISPLAY_NAME = "Claims To Request";
   @StringListProperty(
      description = "Authentication Context Class Reference (acr) values to request in order of preference from the authorization server (AS) using the <em>acr_values</em> parameter. The configured values are requested as voluntary claim as specified by the <a target=\"_blank\" href=\"https://openid.net/specs/openid-connect-core-1_0.html#acrSemantics\">OpenID Connect Core Specification</a>. If the AS supports this claim, the performed acr is included in the ID Token as 'acr' claim and validated according to \"Validate ACR Claim\". <b>Note</b> that the AS is not obligated to return the acr claim.<p>If the client wants to enforce a certain acr (e.g. to enforce strong authentication), \"Additional Claim Validators\" can be configured to validate if the requested acr is contained in the ID Token. Otherwise the authorization fails.</p>",
      displayName = "ACR Values Claim",
      exampleElements = {"loa-1", "loa-2"},
      minElements = 1,
      optional = true
   )
   @GroupProperty(groupIdentifier = "Authorization Request")
   private List<String> acrValuesClaim;
   private static final String ACR_VALUES_FIELD = "acrValuesClaim";
   private static final String ACR_VALUES_DISPLAY_NAME = "ACR Values Claim";
   @BooleanProperty(
      description = "If enabled, a nonce is included in the authorization request and verified against the received ID Token. The nonce is used to associate a client session with an ID Token, and to mitigate replay attacks.",
      defaultValue = "true",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Authorization Request")
   private boolean includeNonce;
   @BooleanProperty(
      description = "Whether or not the Loginapp should specifically request the currently used language for the user interaction at the authorization server.",
      optional = true,
      defaultValue = "false"
   )
   @GroupProperty(groupIdentifier = "Authorization Request")
   private boolean includeLanguageParameter;
   @IntegerProperty(
      description = "Maximum age in seconds of a preexisting authenticated session at the authorization server. Forces re-authentication if the authenticated session is older than the specified value. This parameter is only included if the value is greater than 0. To always force authentication, use of the prompt parameter is recommended.",
      displayName = "Max Authentication Age [s]",
      exampleValues = {900, 3600},
      defaultValue = 0,
      minValue = 0,
      optional = true
   )
   @GroupProperty(groupIdentifier = "Authorization Request")
   private int maxAuthenticationAge;
   @StringListProperty(
      description = "Prompt parameter value(s) sent during the OpenID Connect Authorization Code Flow. Used to either suppress or force user interaction at the authorization endpoint. If left empty, the prompt parameter will be omitted.<p>Available options:</p><li>none: Suppress all user interaction at the authorization server.</li><li>login: Force login at the authorization server.</li><li>consent: Force user consent at the authorization server.</li></ul><li>select_account: Force the authorization server to prompt the end-user to select a user account.</li></ul><p><b>Note: </b> If the setting does not match the authorization server policy the authorization process may fail. For example, if user interaction is suppressed but login is required.</p>",
      optional = true,
      displayName = "Send Prompt Parameter",
      unique = true,
      allowedValues = {"none", "login", "consent", "select_account"}
   )
   @GroupProperty(groupIdentifier = "Authorization Request")
   private List<String> prompt;
   private static final String PROMPT_FIELD = "prompt";
   @PluginListProperty(
      description = "Custom authorization request parameters to be sent to the authorization server (AS). This can be used to define otherwise not configurable parameters such as <code>login_hint</code> or <code>display</code> as well as non-standard parameters.<p><b>Note:</b> If a parameter is set both using the standard functionality and custom authorization request parameter, the standard parameter will take precedence.</p>",
      displayName = "Custom Request Parameters",
      minElements = 1,
      optional = true
   )
   @GroupProperty(groupIdentifier = "Authorization Request")
   private List<OpenIdConnectAuthorizationRequestParameterConfig> customAuthorizationRequestParameters;
   private static final String CUSTOM_AUTHORIZATION_REQUEST_PARAMETERS_PROPERTY_NAME = "customAuthorizationRequestParameters";
   @StringProperty(
      description = "Issuer ('iss') claim expected during ID token validation. The issuer ('iss') value is a case-sensitive URL using the https scheme that contains scheme, host, and optionally, port number and path components and no query or fragment components.<br>If not set, the issuer URI in the ID Token is only compared to the host name of the authorization endpoint.",
      optional = true,
      displayName = "Expected Issuer",
      exampleValues = "urn:windows:liveid"
   )
   @GroupProperty(groupIdentifier = "ID Token")
   private String customIssuerClaim;
   @EnumProperty(
      description = "Audience claim validation method to use for the ID token validation.<p> Options:\t<ul>\t<li>STANDARD: Standard audience claim validation as defined in the OpenID Connect Core specification.</li>\t<li>REDIRECT_URL: Compatibility option. The audience claim must be a valid URL and the host must match the host of the redirect URL used by Airlock IAM.</li>\t<li>CUSTOM: Compatibility option. The audience claim must exactly match a custom value supplied by 'Custom Audience Claim'.</li> </ul></p>",
      optional = true,
      defaultValue = "STANDARD"
   )
   @GroupProperty(groupIdentifier = "ID Token")
   private AudienceClaimValidationMethod audienceClaimValidationMethod;
   private static final String AUDIENCE_CLAIM_VALIDATION_METHOD_FIELD = "audienceClaimValidationMethod";
   @StringProperty(
      description = "Custom audience ('aud') claim to use for ID token validation. Some OpenID Connect providers may not adhere to the standard completely and successful ID token validation may therefore require to accept a custom audience ('aud') claim.",
      displayName = "Custom Audience Claim",
      optional = true,
      exampleValues = "custom_audience"
   )
   @GroupProperty(groupIdentifier = "ID Token")
   private String customAudienceClaim;
   private static final String CUSTOM_AUDIENCE_CLAIM_FIELD = "customAudienceClaim";
   @BooleanProperty(
      description = "If enabled, acr values being requested by \"ACR Values Claim\" or \"Claims To Request\" are validated to be contained in the received ID Token.<br/>Validation fails if no acr claim is present or the contained value does not match one of the requested acr values.<br/>Validation succeeds if no acr values are requested or the value of the acr claim in the ID Token matches one of the requested acr values.<p>Disable to not validate the acr claim. In addition, it is possible to define custom acr validation using \"Additional Claim Validators\".</p>",
      defaultValue = "true",
      displayName = "Validate ACR Claim",
      optional = true
   )
   @GroupProperty(groupIdentifier = "ID Token")
   private boolean enableAcrValidation;
   private static final String ACR_VALIDATION_DISPLAY_NAME = "Validate ACR Claim";
   @PluginListProperty(
      description = "List of additional claim validators. Allows to validate the presence and value of specific claims. This can be useful to e.g. validate ID Token claims that were requested in an authorization request. See the configuration of \"Claims To Request\" and \"ACR Values Claim\".<p>If any of the validators fail, the ID Token is rejected and authorization fails.</p>",
      displayName = "Additional Claim Validators",
      optional = true
   )
   @GroupProperty(groupIdentifier = "ID Token")
   private List<ClaimsValidatorConfig> additionalClaimValidators;
   private static final String ADDITIONAL_CLAIM_VALIDATORS_DISPLAY_NAME = "Additional Claim Validators";
   @PluginListProperty(
      description = "List of remote resources that are contained in the OpenID Connect ID token. These resources are included into the OAuth 2.0 credential described in Resource Requests.",
      displayName = "ID Token Resources",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Resource Mappings")
   private List<AbstractOAuth2RemoteResourceConfig> idTokenResources;
   private static final String ID_TOKEN_RESOURCES_FIELD = "idTokenResources";
   @PluginProperty(
      description = "The URL to which the AS should redirect after a logout has been performed. <p>It is recommended to define this property to ensure a seamless application flow. If not configured, the AS defines how to respond to the logout request.</p>",
      displayName = "Post Logout Redirect URL",
      optional = true,
      defaultValue = OAuth2NoPostLogoutRedirectUriConfig.class
   )
   @GroupProperty(groupIdentifier = "Logout")
   private OAuth2PostLogoutRedirectUriConfig postLogoutRedirectUrl;
   protected static final String POST_LOGOUT_REDIRECT_URL_DISPLAY_NAME = "Post Logout Redirect URL";
   protected static final String POST_LOGOUT_REDIRECT_URL_PROPERTY = "postLogoutRedirectUrl";

   protected OpenIDConnectSsoFlowClientSettingsBase(
      String clientId,
      String clientSecret,
      OAuth2RequestBuilderConfig accessTokenRequestMethod,
      OAuth2LoggingSettings loggingSettings,
      boolean enableAccountLinking,
      String autoLinkExistingUsersContextDataField,
      AccountRegistrationConfig accountRegistrationConfig,
      OAuth2ProviderIdentifierConfig providerId,
      OAuth2RedirectUriConfig clientRedirectURI,
      List<OAuth2ClientResourceRequest> resourceRequests,
      AccountLinkingSelfServiceConfig accountLinkingSelfService,
      RedFlagConfig missingAccountLinkRedFlag,
      List<ClaimRequestConfig> claimsToRequest,
      List<String> acrValuesClaim,
      boolean includeNonce,
      boolean includeLanguageParameter,
      int maxAuthenticationAge,
      List<String> prompt,
      String customIssuerClaim,
      AudienceClaimValidationMethod audienceClaimValidationMethod,
      String customAudienceClaim,
      boolean enableAcrValidation,
      List<ClaimsValidatorConfig> additionalClaimValidators,
      List<String> scopesToRequest,
      List<AbstractOAuth2RemoteResourceConfig> idTokenResources,
      OAuth2PostLogoutRedirectUriConfig postLogoutRedirectUrl,
      List<OpenIdConnectAuthorizationRequestParameterConfig> customAuthorizationRequestParameters
   ) {
      super(
         clientId,
         clientSecret,
         accessTokenRequestMethod,
         loggingSettings,
         enableAccountLinking,
         autoLinkExistingUsersContextDataField,
         accountRegistrationConfig,
         providerId,
         clientRedirectURI,
         resourceRequests,
         accountLinkingSelfService,
         missingAccountLinkRedFlag
      );
      this.claimsToRequest = claimsToRequest;
      this.acrValuesClaim = acrValuesClaim;
      this.includeNonce = includeNonce;
      this.includeLanguageParameter = includeLanguageParameter;
      this.maxAuthenticationAge = maxAuthenticationAge;
      this.prompt = prompt;
      this.customIssuerClaim = customIssuerClaim;
      this.audienceClaimValidationMethod = audienceClaimValidationMethod;
      this.customAudienceClaim = customAudienceClaim;
      this.enableAcrValidation = enableAcrValidation;
      this.additionalClaimValidators = additionalClaimValidators;
      this.scopesToRequest = scopesToRequest;
      this.idTokenResources = idTokenResources;
      this.postLogoutRedirectUrl = postLogoutRedirectUrl;
      this.customAuthorizationRequestParameters = customAuthorizationRequestParameters;
   }

   public abstract String getAuthorizationEndpointURL();

   public abstract String getTokenEndpointURL();

   public abstract OpenIDConnectIdTokenSignatureValidator getSignatureValidator();

   public abstract URI getEndSessionEndpoint();

   public abstract boolean supportsEndSessionEndpoint();

   public List<String> getPrompt() {
      return this.prompt;
   }

   public List<String> getScopesToRequest() {
      return this.scopesToRequest;
   }

   public boolean isIncludeLanguageParameter() {
      return this.includeLanguageParameter;
   }

   public int getMaxAuthenticationAge() {
      return this.maxAuthenticationAge;
   }

   public List<String> getAcrValuesClaim() {
      return this.acrValuesClaim;
   }

   public List<ClaimRequestConfig> getClaimsToRequest() {
      return this.claimsToRequest;
   }

   public boolean isIncludeNonce() {
      return this.includeNonce;
   }

   public List<OpenIdConnectAuthorizationRequestParameterConfig> getCustomAuthorizationRequestParameters() {
      return this.customAuthorizationRequestParameters;
   }

   public String getCustomIssuerClaim() {
      return this.customIssuerClaim;
   }

   public String getCustomAudienceClaim() {
      return this.customAudienceClaim;
   }

   public AudienceClaimValidationMethod getAudienceClaimValidationMethod() {
      return this.audienceClaimValidationMethod;
   }

   public boolean isEnableAcrValidation() {
      return this.enableAcrValidation;
   }

   public List<AbstractOAuth2RemoteResourceConfig> getIdTokenResources() {
      return this.idTokenResources;
   }

   public List<ClaimsValidatorConfig> getAdditionalClaimValidators() {
      return this.additionalClaimValidators;
   }

   public OAuth2PostLogoutRedirectUriConfig getPostLogoutRedirectUrl() {
      return this.postLogoutRedirectUrl;
   }

   @ConfigurationValidator
   static void checkExactlyOneUsernameResource(
      PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context, Environment validationEnvironment
   ) throws ConfigurationException {
      OAuth2ResourcesConfigValidator.builder()
         .config(config)
         .env(env)
         .context(context)
         .resourceRequestsField("resourceRequests")
         .containedResourcesField("containedResources")
         .idTokenResourcesField("idTokenResources")
         .build()
         .checkExactlyOneUsernameResourceRequestOrIdTokenResource();
   }

   @ConfigurationValidator
   static void validateCustomAudClaimSet(
      PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context, Environment validationEnvironment
   ) throws ConfigurationException {
      AudienceClaimValidationMethod audClaimValidationMethod = (AudienceClaimValidationMethod)EnumPropertyValueReader.effectiveConfigEnum(
         config, "audienceClaimValidationMethod", env, context
      );
      if (AudienceClaimValidationMethod.CUSTOM == audClaimValidationMethod) {
         String customClaim = StringPropertyValueReader.effectiveStringConfigValue(config, "customAudienceClaim", env, context);
         if (StringUtils.isBlank(customClaim)) {
            throw new PropertyConstraintViolationException(
               "customAudienceClaim", config, env, context, "In order to use the custom audience claim validation method a custom claim has to be set."
            );
         }
      }
   }

   @ConfigurationValidator
   static void validateClaims(PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context, Environment validationEnvironment) throws ConfigurationException {
      List<PluginConfiguration> claimRequestConfig = PluginListPropertyValueReader.effectivePluginListPropertyValue(config, "claimsToRequest", env, context);
      if (!CollectionUtils.isEmpty(claimRequestConfig)) {
         List<String> acrValues = StringListPropertyValueReader.effectiveStringListConfigValue(config, "acrValuesClaim", env, context);
         Multimap<String, String> uniqueClaims = HashMultimap.create();

         for (PluginConfiguration claimRequest : claimRequestConfig) {
            String claim = StringPropertyValueReader.effectiveStringConfigValue(claimRequest, "claim", env, context);
            checkIfClaimsUnique(config, env, context, uniqueClaims, claim, claimRequest);
            if (CollectionUtils.isNotEmpty(acrValues)) {
               checkIfOnlyOneAcrRequestMethod(config, env, context, claim);
            }
         }
      }
   }

   @ConfigurationValidator
   static void validateCustomRequestParams(
      PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context, Environment validationEnvironment
   ) throws ConfigurationException {
      List<PluginConfiguration> requestParameters = PluginListPropertyValueReader.effectivePluginListPropertyValue(
         config, "customAuthorizationRequestParameters", env, context
      );
      if (!CollectionUtils.isEmpty(requestParameters)) {
         Set<String> requestParams = new HashSet<>();

         for (PluginConfiguration requestParameter : requestParameters) {
            String name = StringPropertyValueReader.effectiveStringConfigValue(requestParameter, "parameterName", env, context);
            if (name != null && !requestParams.add(name)) {
               throw new PropertyConstraintViolationException(
                  "parameterName",
                  requestParameter,
                  env,
                  context,
                  "There is more than one \"Request Parameter\" with name " + LogUtil.quote(name) + " configured. All names must be distinct."
               );
            }
         }
      }
   }

   @ConfigurationValidator
   static void validatePrompt(PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context, Environment validationEnvironment) throws PropertyConfigurationException {
      List<String> promptValues = StringListPropertyValueReader.effectiveStringListConfigValue(config, "prompt", env, context);
      List<String> invalidPromptValues = promptValues.stream().filter(p -> !isValidPrompt(p)).collect(Collectors.toList());
      if (!invalidPromptValues.isEmpty()) {
         throw new PropertyConstraintViolationException("prompt", config, env, context, "Invalid prompt values: " + String.join(", ", invalidPromptValues));
      }

      if (promptValues.contains(PromptValue.NONE.getParamValue()) && promptValues.size() > 1) {
         throw new PropertyConstraintViolationException("prompt", config, env, context, "The prompt 'none' can not be combined with other prompt values.");
      }
   }

   private static boolean isValidPrompt(String promptValue) {
      return Stream.of(PromptValue.values()).filter(PromptValue::isSupported).anyMatch(p -> p.getParamValue().equals(promptValue));
   }

   private static void checkIfOnlyOneAcrRequestMethod(PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context, String claim) throws PropertyConfigurationException {
      if (OpenIdConnectClaim.ACR.getClaimName().equals(claim)) {
         throw new PropertyConstraintViolationException(
            "acrValuesClaim",
            config,
            env,
            context,
            "It is not allowed to request acr values using "
               + LogUtil.quote("ACR Values Claim")
               + " and additionally request the acr claim using "
               + LogUtil.quote("Claims To Request")
         );
      }
   }

   private static void checkIfClaimsUnique(
      PluginConfiguration config,
      ConfigurationEnvironment env,
      ConfigurationContext context,
      Multimap<String, String> uniqueClaims,
      String claim,
      PluginConfiguration claimConfig
   ) throws PropertyConstraintViolationException {
      boolean isUnique = uniqueClaims.put(claimConfig.getPluginClassName(), claim);
      if (!isUnique) {
         throw new PropertyConstraintViolationException(
            "claimsToRequest",
            config,
            env,
            context,
            "It is not allowed to request the " + claimConfig.getPluginClassDisplayName() + " " + LogUtil.quote(claim) + " multiple times."
         );
      }
   }

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