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

import com.airlock.iam.authentication.application.configuration.processor.DefaultAuthenticationProcessorsConfig;
import com.airlock.iam.authentication.application.configuration.processor.TemporaryLockingProcessorConfig;
import com.airlock.iam.authentication.application.configuration.processor.UserEnumerationProtectionProcessorConfig;
import com.airlock.iam.core.misc.authen.UsernameTransformer;
import com.airlock.iam.flow.api.application.configuration.flowprocessor.FlowProcessorConfig;
import com.airlock.iam.flow.api.application.configuration.flowprocessor.FlowProcessorsConfig;
import com.airlock.iam.flow.api.application.configuration.sequence.FlowStepSequence;
import com.airlock.iam.flow.application.configuration.validation.FlowStepIdConfigValidators;
import com.airlock.iam.flow.shared.application.configuration.HttpParameterSpec;
import com.airlock.iam.flow.types.api.application.configuration.step.StepConfigForAuthenticationFlows;
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.PluginListProperty;
import com.airlock.iam.plugin.api.annotation.property.PluginProperty;
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.PropertyConstraintViolationException;
import com.airlock.iam.plugin.api.valuereader.BooleanPropertyValueReader;
import com.airlock.iam.plugin.framework.application.service.builder.plugin.PluginPropertyBuilderInitializer;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

@Plugin(description = "Configuration for an authentication flow.", displayName = "Authentication Flow")
@Groups(groupSequence = {"Basic Settings", "Security Settings", "Advanced Settings"}, openGroups = {"Basic Settings", "Security Settings"})
public class AuthenticationFlowConfig implements AuthenticationFlowConfigSpec {
   @PluginListProperty(description = "Steps of the flow.", optional = false, minElements = 1)
   @GroupProperty(groupIdentifier = "Basic Settings")
   private List<StepConfigForAuthenticationFlows> steps;
   @PluginProperty(
      description = "Processors get notified about the various stages of the flow and offer hooks to plug in custom logic. These processors realize the entire authentication logic such as incrementing failed login counters or checking of user validity. <p>The configured processors are extended with the following processors (if not already present):<ol><li>User Enumeration Protection Processor (only if \"Prevent User Enumeration\" enabled)</li><li>Temporary Locking Processor (only if \"Enable Temporary Locking\" enabled)</li></ol></p>",
      optional = true,
      defaultValue = DefaultAuthenticationProcessorsConfig.class
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private FlowProcessorsConfig processors;
   @BooleanProperty(
      description = "<p>If enabled, user enumeration is prevented by not revealing what went wrong in a user identifying step (\"Stealth Mode\"). In particular, all failures because of wrong password or not existing, locked or invalid user are answered with the same generic error code <code>AUTHENTICATION_FAILED</code>. Furthermore, the sessions of the user will be terminated on IAM and on the Airlock Gateway (WAF).</p><p>Note that this feature is not compatible with Temporary Locking. It is recommended to configure a \"Fixed Response Duration\" for failed responses to prevent user enumeration timing attacks.</p><p><b>Important note:</b> This feature only protects against user enumeration if the identifying step identifies the user <em>and at the same time checks a credential</em>, e.g. in case of \"Password Authentication\" or \"Device Token Authentication\". If the \"User Identifying Step\" is used, this feature does <em>not</em> protect against user enumeration.</p><p>If enabled, a \"User Enumeration Protection Processor\" is automatically added to the list of flow processors.</p>",
      optional = true,
      displayName = "Prevent User Enumeration",
      defaultValue = "false"
   )
   @GroupProperty(groupIdentifier = "Security Settings")
   private boolean preventUserEnumeration;
   private static final String PREVENT_USER_ENUMERATION_FIELD = "preventUserEnumeration";
   private static final String PREVENT_USER_ENUMERATION_DISPLAY_NAME = "Prevent User Enumeration";
   @BooleanProperty(
      description = "<p>Enables Temporary Locking for this flow.</p><p><b>Note:</b> This is only effective, if temporary locking is also enabled in the \"Target Applications and Authentication\" plugin.</p><p>If enabled, a \"Temporary Locking Processor\" is automatically added to the list of flow processors.</p><p><b>Note:</b> Disabling and re-enabling this feature does not reset temporary locks.</p>",
      optional = true,
      defaultValue = "true",
      displayName = "Enable Temporary Locking"
   )
   @GroupProperty(groupIdentifier = "Security Settings")
   private boolean temporaryLockingActive;
   private static final String TEMP_LOCKING_ACTIVE_FIELD = "temporaryLockingActive";
   @BooleanProperty(
      description = "<p>If enabled, for any step result that caused an increase in the number of failed attempts, the remaining number of attempts for that factor is returned with the step result.</p><p>This feature is not combinable with username enumeration protection.</p>",
      defaultValue = "false",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Security Settings")
   private boolean addRemainingAttemptsInfo;
   private static final String ADD_REMAINING_ATTEMPTS_INFO_FIELD = "addRemainingAttemptsInfo";
   @PluginListProperty(
      displayName = "Username Transformers",
      description = "Username transformers may transform the provided username into the single unique user ID required for the flow.<br />The transformation of a username takes place in the first step before the user is loaded. Note that username transformers have no effect on the propagated username value. Transformers can be chained, i.e. a first transformer could normalize the original name, where the next transformer looks up the normalized name in a database for potential transformation matches.<br />In contrary to the above description of chaining, a transformer can also signal that it already found the final user ID and the chain must stop after it.<br/>For further details please refer to the documentation of the username transformer plugins.",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private List<UsernameTransformer> usernameTransformers;
   @PluginListProperty(
      description = "<p>Whitelist of additional attributes (e.g. headers or REST attributes) for the check password\nauthentication REST call (<tt>/&lt;loginapp-uri&gt;/rest/public/authentication/password/check/</tt>).</p>\n<p>Attributes with matching names and valid values are made available to the flow.</p>",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private List<HttpParameterSpec> additionalAttributes;
   @BooleanProperty(
      description = "<p>If enabled, this flow does not consider persistency, i.e. users don't have to exist locally in order to be authenticated. This is typically used with SSO tickets or external authentication using OAuth or SAML.</p><p>Persistency-less flows are very limited in their capabilities, in particular:<ul><li>Password checks and second factor authentication are not possible.</li><li>The user state (locked, invalid etc.) cannot be verified.</li><li>Identity propagation is limited to the information received from external systems.</li></ul></p><p>Note that configuration validation support is limited. It is essential to test such a flow extensively to ensure it behaves correctly in all situations.</p><p>It is recommended to use the \"Default Persistency-less Authentication Processors\" when using a persistency-less flow.</p>",
      displayName = "Persistency-less",
      defaultValue = "false",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Security Settings")
   private boolean persistencyless;
   private List<FlowProcessorConfig> inititalizedProcessors;

   public AuthenticationFlowConfig(
      List<StepConfigForAuthenticationFlows> steps,
      FlowProcessorsConfig processors,
      boolean preventUserEnumeration,
      boolean temporaryLockingActive,
      boolean addRemainingAttemptsInfo,
      List<UsernameTransformer> usernameTransformers,
      List<HttpParameterSpec> additionalAttributes,
      boolean persistencyless
   ) {
      this.steps = steps;
      this.processors = processors;
      this.preventUserEnumeration = preventUserEnumeration;
      this.temporaryLockingActive = temporaryLockingActive;
      this.addRemainingAttemptsInfo = addRemainingAttemptsInfo;
      this.usernameTransformers = usernameTransformers;
      this.additionalAttributes = additionalAttributes;
      this.persistencyless = persistencyless;
   }

   @Override
   public FlowStepSequence getSteps() {
      return ((FlowStepSequence.FlowStepSequenceBuilder)PluginPropertyBuilderInitializer.pluginBuilder(FlowStepSequence.FlowStepSequenceBuilder.class))
         .steps(this.steps)
         .build();
   }

   @AfterConfigActivation
   void initializeProcessors() {
      List<FlowProcessorConfig> procs = new ArrayList<>(this.processors.getProcessors());
      if (this.preventUserEnumeration) {
         addIfMissing(procs, UserEnumerationProtectionProcessorConfig.USER_ENUMERATION_PROTECTION_PROCESSOR_CONFIG);
      }

      if (this.temporaryLockingActive) {
         addIfMissing(procs, TemporaryLockingProcessorConfig.TEMPORARY_LOCKING_PROCESSOR_CONFIG);
      }

      this.inititalizedProcessors = Collections.unmodifiableList(procs);
   }

   @Override
   public List<FlowProcessorConfig> getProcessors() {
      return this.inititalizedProcessors;
   }

   @Override
   public boolean requiresUserPersistency() {
      return !this.persistencyless;
   }

   private static void addIfMissing(List<FlowProcessorConfig> processors, FlowProcessorConfig processorToAdd) {
      Class<? extends FlowProcessorConfig> type = (Class<? extends FlowProcessorConfig>)processorToAdd.getClass();
      boolean alreadyConfigured = processors.stream().anyMatch(proc -> proc.getClass().equals(type));
      if (!alreadyConfigured) {
         processors.add(processorToAdd);
      }
   }

   @ConfigurationValidator
   static void validateSecuritySettings(
      PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context, Environment validationEnvironment
   ) throws ConfigurationException {
      boolean userEnumerationProtectionEnabled = BooleanPropertyValueReader.effectiveBooleanPropertyValue(config, "preventUserEnumeration", env, context);
      boolean temporaryLockingEnabled = BooleanPropertyValueReader.effectiveBooleanPropertyValue(config, "temporaryLockingActive", env, context);
      if (userEnumerationProtectionEnabled && temporaryLockingEnabled) {
         throw new PropertyConstraintViolationException(
            "preventUserEnumeration", config, env, context, "User Enumeration Protection and Temporary Locking cannot be used at the same time."
         );
      }

      boolean addInfoEnabled = BooleanPropertyValueReader.effectiveBooleanPropertyValue(config, "addRemainingAttemptsInfo", env, context);
      if (userEnumerationProtectionEnabled && addInfoEnabled) {
         throw new PropertyConstraintViolationException(
            "preventUserEnumeration", config, env, context, "User Enumeration Protection and Add Remaining Attempts Info cannot be used at the same time."
         );
      }
   }

   @ConfigurationValidator
   static void validateFlowConstraints(
      PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context, Environment validationEnvironment
   ) throws ConfigurationException {
      new FlowStepIdConfigValidators(config, env, context, false, config).validateFlowConstraints();
   }

   @Override
   public boolean isPreventUserEnumeration() {
      return this.preventUserEnumeration;
   }

   @Override
   public boolean isTemporaryLockingActive() {
      return this.temporaryLockingActive;
   }

   @Override
   public boolean isAddRemainingAttemptsInfo() {
      return this.addRemainingAttemptsInfo;
   }

   @Override
   public boolean isPersistencyless() {
      return this.persistencyless;
   }

   @Override
   public List<FlowProcessorConfig> getInititalizedProcessors() {
      return this.inititalizedProcessors;
   }

   @Override
   public List<HttpParameterSpec> getAdditionalAttributes() {
      return this.additionalAttributes;
   }

   @Override
   public List<UsernameTransformer> getUsernameTransformers() {
      return this.usernameTransformers;
   }

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

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

   @SuppressFBWarnings(justification = "generated code")
   public static class AuthenticationFlowConfigBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private ArrayList<StepConfigForAuthenticationFlows> steps;
      @SuppressFBWarnings(justification = "generated code")
      private FlowProcessorsConfig processors;
      @SuppressFBWarnings(justification = "generated code")
      private boolean preventUserEnumeration;
      @SuppressFBWarnings(justification = "generated code")
      private boolean temporaryLockingActive;
      @SuppressFBWarnings(justification = "generated code")
      private boolean addRemainingAttemptsInfo;
      @SuppressFBWarnings(justification = "generated code")
      private ArrayList<UsernameTransformer> usernameTransformers;
      @SuppressFBWarnings(justification = "generated code")
      private ArrayList<HttpParameterSpec> additionalAttributes;
      @SuppressFBWarnings(justification = "generated code")
      private boolean persistencyless;

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

      @SuppressFBWarnings(justification = "generated code")
      public AuthenticationFlowConfig.AuthenticationFlowConfigBuilder step(StepConfigForAuthenticationFlows step) {
         if (this.steps == null) {
            this.steps = new ArrayList<>();
         }

         this.steps.add(step);
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public AuthenticationFlowConfig.AuthenticationFlowConfigBuilder steps(Collection<? extends StepConfigForAuthenticationFlows> steps) {
         if (steps == null) {
            throw new NullPointerException("steps cannot be null");
         }

         if (this.steps == null) {
            this.steps = new ArrayList<>();
         }

         this.steps.addAll(steps);
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public AuthenticationFlowConfig.AuthenticationFlowConfigBuilder clearSteps() {
         if (this.steps != null) {
            this.steps.clear();
         }

         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public AuthenticationFlowConfig.AuthenticationFlowConfigBuilder processors(FlowProcessorsConfig processors) {
         this.processors = processors;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public AuthenticationFlowConfig.AuthenticationFlowConfigBuilder preventUserEnumeration(boolean preventUserEnumeration) {
         this.preventUserEnumeration = preventUserEnumeration;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public AuthenticationFlowConfig.AuthenticationFlowConfigBuilder temporaryLockingActive(boolean temporaryLockingActive) {
         this.temporaryLockingActive = temporaryLockingActive;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public AuthenticationFlowConfig.AuthenticationFlowConfigBuilder addRemainingAttemptsInfo(boolean addRemainingAttemptsInfo) {
         this.addRemainingAttemptsInfo = addRemainingAttemptsInfo;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public AuthenticationFlowConfig.AuthenticationFlowConfigBuilder usernameTransformer(UsernameTransformer usernameTransformer) {
         if (this.usernameTransformers == null) {
            this.usernameTransformers = new ArrayList<>();
         }

         this.usernameTransformers.add(usernameTransformer);
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public AuthenticationFlowConfig.AuthenticationFlowConfigBuilder usernameTransformers(Collection<? extends UsernameTransformer> usernameTransformers) {
         if (usernameTransformers == null) {
            throw new NullPointerException("usernameTransformers cannot be null");
         }

         if (this.usernameTransformers == null) {
            this.usernameTransformers = new ArrayList<>();
         }

         this.usernameTransformers.addAll(usernameTransformers);
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public AuthenticationFlowConfig.AuthenticationFlowConfigBuilder clearUsernameTransformers() {
         if (this.usernameTransformers != null) {
            this.usernameTransformers.clear();
         }

         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public AuthenticationFlowConfig.AuthenticationFlowConfigBuilder additionalAttribute(HttpParameterSpec additionalAttribute) {
         if (this.additionalAttributes == null) {
            this.additionalAttributes = new ArrayList<>();
         }

         this.additionalAttributes.add(additionalAttribute);
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public AuthenticationFlowConfig.AuthenticationFlowConfigBuilder additionalAttributes(Collection<? extends HttpParameterSpec> additionalAttributes) {
         if (additionalAttributes == null) {
            throw new NullPointerException("additionalAttributes cannot be null");
         }

         if (this.additionalAttributes == null) {
            this.additionalAttributes = new ArrayList<>();
         }

         this.additionalAttributes.addAll(additionalAttributes);
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public AuthenticationFlowConfig.AuthenticationFlowConfigBuilder clearAdditionalAttributes() {
         if (this.additionalAttributes != null) {
            this.additionalAttributes.clear();
         }

         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public AuthenticationFlowConfig.AuthenticationFlowConfigBuilder persistencyless(boolean persistencyless) {
         this.persistencyless = persistencyless;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public AuthenticationFlowConfig build() {
         List steps = switch (this.steps == null ? 0 : this.steps.size()) {
            case 0 -> Collections.emptyList();
            case 1 -> Collections.singletonList(this.steps.get(0));
            default -> Collections.unmodifiableList(new ArrayList<>(this.steps));
         };

         List usernameTransformers = switch (this.usernameTransformers == null ? 0 : this.usernameTransformers.size()) {
            case 0 -> Collections.emptyList();
            case 1 -> Collections.singletonList(this.usernameTransformers.get(0));
            default -> Collections.unmodifiableList(new ArrayList<>(this.usernameTransformers));
         };

         return new AuthenticationFlowConfig(
            steps,
            this.processors,
            this.preventUserEnumeration,
            this.temporaryLockingActive,
            this.addRemainingAttemptsInfo,
            usernameTransformers,
            switch (this.additionalAttributes == null ? 0 : this.additionalAttributes.size()) {
               case 0 -> Collections.emptyList();
               case 1 -> Collections.singletonList(this.additionalAttributes.get(0));
               default -> Collections.unmodifiableList(new ArrayList<>(this.additionalAttributes));
            },
            this.persistencyless
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "AuthenticationFlowConfig.AuthenticationFlowConfigBuilder(steps="
            + this.steps
            + ", processors="
            + this.processors
            + ", preventUserEnumeration="
            + this.preventUserEnumeration
            + ", temporaryLockingActive="
            + this.temporaryLockingActive
            + ", addRemainingAttemptsInfo="
            + this.addRemainingAttemptsInfo
            + ", usernameTransformers="
            + this.usernameTransformers
            + ", additionalAttributes="
            + this.additionalAttributes
            + ", persistencyless="
            + this.persistencyless
            + ")";
      }
   }
}
