package com.airlock.iam.publicselfservice.application.configuration.steps;

import com.airlock.iam.common.api.domain.model.authmethod.AuthenticationMethod;
import com.airlock.iam.common.application.configuration.mtan.BasicMtanSettings;
import com.airlock.iam.core.misc.authen.SecretStringGenerator;
import com.airlock.iam.core.misc.impl.authen.AuthenticationMethodFactory;
import com.airlock.iam.core.misc.impl.authen.ExtendedStringGenerator;
import com.airlock.iam.flow.api.application.configuration.dynamicactivation.DynamicStepActivationConfig;
import com.airlock.iam.flow.api.application.configuration.selection.FlowConditionConfig;
import com.airlock.iam.flow.api.application.configuration.step.AbstractInteractiveFlowStepConfig;
import com.airlock.iam.flow.api.application.configuration.step.InteractiveGotoTargetConfig;
import com.airlock.iam.flow.api.application.configuration.step.StepIdConfig;
import com.airlock.iam.flow.api.application.configuration.tag.TagConfig;
import com.airlock.iam.flow.api.application.configuration.valueprovider.ValueMapProviderConfig;
import com.airlock.iam.flow.shared.api.application.configuration.message.MtanMessageProviderConfig;
import com.airlock.iam.flow.types.api.application.configuration.step.StepConfigForPublicSelfServiceFlows;
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.NextActions;
import com.airlock.iam.plugin.api.annotation.config.Plugin;
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.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.publicselfservice.domain.model.step.MtanIdentityVerificationStep;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.annotation.Nullable;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;

@Plugin(
   description = "<p>Public self-service flow step that verifies the user identity by sending an SMS with an OTP that the user has to enter correctly for the flow to continue.</p><p>This is an <em>identity verification</em> step that differs from a general \"factor check\" step in the following ways: <ul><li>It doesn't fail with non-existing users or users without a phone number.</li>  <li>It never presents a phone number selection, but automatically selects the last used number.</li>  <li>It implements stealth mode: if a user does not exist or cannot do public self-services for whatever reason, no error is returned, but any OTP entered is rejected, so that the step can never be completed successfully.</li></ul>",
   displayName = "SMS Identity Verification Step"
)
@Groups(openGroups = "Basic Settings", groupSequence = {"Basic Settings", "Tags/Guards", "Flow Control", "Advanced Settings"})
@NextActions(MtanIdentityVerificationStep.MtanOtpStepAction.class)
public final class SmsIdentityVerificationStepConfig extends AbstractInteractiveFlowStepConfig implements StepConfigForPublicSelfServiceFlows {
   @PluginProperty(description = "General settings for SMS/mTAN handling.", optional = false, displayName = "mTAN Settings")
   @GroupProperty(groupIdentifier = "Basic Settings")
   private BasicMtanSettings mtanSettings;
   public static final String BASIC_SETTINGS_PROPERTY_NAME = "mtanSettings";
   @PluginProperty(description = "Creates the message sent in the verification SMS.", optional = false)
   @GroupProperty(groupIdentifier = "Basic Settings")
   private MtanMessageProviderConfig messageProvider;
   @PluginProperty(
      description = "The string generator plugin to generate the one-time password (OTP) token.",
      displayName = "OTP Generator",
      optional = true,
      defaultValue = ExtendedStringGenerator.class
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private SecretStringGenerator otpGenerator;
   @IntegerProperty(
      description = "Number of allowed failed attempts before the flow is aborted.",
      exampleValues = {0, 3, 5},
      minValue = 0,
      defaultValue = 1,
      optional = true
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private int maxFailedAttempts;
   @IntegerProperty(
      description = "Determines how long the OTP is valid (in seconds).",
      displayName = "OTP Validity [s]",
      exampleValues = {60, 300, 600},
      minValue = 1,
      defaultValue = 300,
      optional = true
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private int otpValidity;
   @BooleanProperty(
      description = "If enabled, the case of characters is considered when matching the entered OTP against the generated one.",
      displayName = "Case-Sensitive OTP Check",
      optional = true,
      defaultValue = "true"
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private boolean otpCaseSensitive;
   @StringProperty(
      description = "The identifier of the authentication method for this step. Since the authentication method is also the identifier for failed login attempts for this step, distinct identifiers must be chosen if multiple instances of the same step type are used to check different credentials (e.g., two password step instances check two different passwords).<p>Example: a system tracks two passwords per user. Password 1 is used in flow A, password 2 in flow B. These two steps must have distinct 'Authentication Method Ids', e.g. PASSWORD1 and PASSWORD2.</p>If only one identifier (e.g. \"PASSWORD\") is used, this may allow brute force attacks on the password as follows. Assuming an attacker knows the user's password 1, they get an unlimited number of attempts on flow B, as the 'PASSWORD' counter can repeatedly be reset to 0 by performing a successful login on flow A.<p>To prevent such attacks, use two different counters by setting the authentication methods, for example, to PASSWORD1 and PASSWORD2, respectively.</p>",
      displayName = "Authentication Method ID",
      defaultValue = "MTAN",
      maxLength = 23,
      optional = true
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private String authenticationMethodId;

   public Duration getOtpValidty() {
      return Duration.ofSeconds(this.otpValidity);
   }

   public AuthenticationMethod getAuthenticationMethodId() {
      return AuthenticationMethodFactory.authMethodFor(this.authenticationMethodId);
   }

   public BasicMtanSettings getMtanSettings() {
      return this.mtanSettings;
   }

   public MtanMessageProviderConfig getMessageProvider() {
      return this.messageProvider;
   }

   public SecretStringGenerator getOtpGenerator() {
      return this.otpGenerator;
   }

   public int getMaxFailedAttempts() {
      return this.maxFailedAttempts;
   }

   public boolean isOtpCaseSensitive() {
      return this.otpCaseSensitive;
   }

   private SmsIdentityVerificationStepConfig(
      FlowConditionConfig skipCondition,
      FlowConditionConfig preCondition,
      boolean requiresActivation,
      List<TagConfig> tagsOnSuccess,
      @Nullable StepIdConfig stepId,
      List<InteractiveGotoTargetConfig> interactiveGotoTargets,
      List<DynamicStepActivationConfig> dynamicStepActivations,
      Map<String, StepIdConfig> onFailureGotos,
      List<ValueMapProviderConfig> customResponseAttributes,
      List<ValueMapProviderConfig> customFailureResponseAttributes,
      BasicMtanSettings mtanSettings,
      MtanMessageProviderConfig messageProvider,
      SecretStringGenerator otpGenerator,
      int maxFailedAttempts,
      int otpValidity,
      boolean otpCaseSensitive,
      String authenticationMethodId
   ) {
      super(
         skipCondition,
         preCondition,
         requiresActivation,
         tagsOnSuccess,
         stepId,
         interactiveGotoTargets,
         dynamicStepActivations,
         onFailureGotos,
         customResponseAttributes,
         customFailureResponseAttributes
      );
      this.mtanSettings = mtanSettings;
      this.messageProvider = messageProvider;
      this.otpGenerator = otpGenerator;
      this.maxFailedAttempts = maxFailedAttempts;
      this.otpValidity = otpValidity;
      this.otpCaseSensitive = otpCaseSensitive;
      this.authenticationMethodId = authenticationMethodId;
   }

   @ConfigurationValidator
   static void validateOnlyOneTokenAllowed(
      PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context, Environment validationEnvironment
   ) throws ConfigurationException {
      new SmsIdentityVerificationStepConfigValidator(config, context, env).validate();
   }

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

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

   @SuppressFBWarnings(justification = "generated code")
   public static class SmsIdentityVerificationStepConfigBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private FlowConditionConfig skipCondition;
      @SuppressFBWarnings(justification = "generated code")
      private FlowConditionConfig preCondition;
      @SuppressFBWarnings(justification = "generated code")
      private boolean requiresActivation;
      @SuppressFBWarnings(justification = "generated code")
      private List<TagConfig> tagsOnSuccess;
      @SuppressFBWarnings(justification = "generated code")
      private StepIdConfig stepId;
      @SuppressFBWarnings(justification = "generated code")
      private ArrayList<InteractiveGotoTargetConfig> interactiveGotoTargets;
      @SuppressFBWarnings(justification = "generated code")
      private ArrayList<DynamicStepActivationConfig> dynamicStepActivations;
      @SuppressFBWarnings(justification = "generated code")
      private Map<String, StepIdConfig> onFailureGotos;
      @SuppressFBWarnings(justification = "generated code")
      private List<ValueMapProviderConfig> customResponseAttributes;
      @SuppressFBWarnings(justification = "generated code")
      private List<ValueMapProviderConfig> customFailureResponseAttributes;
      @SuppressFBWarnings(justification = "generated code")
      private BasicMtanSettings mtanSettings;
      @SuppressFBWarnings(justification = "generated code")
      private MtanMessageProviderConfig messageProvider;
      @SuppressFBWarnings(justification = "generated code")
      private SecretStringGenerator otpGenerator;
      @SuppressFBWarnings(justification = "generated code")
      private int maxFailedAttempts;
      @SuppressFBWarnings(justification = "generated code")
      private int otpValidity;
      @SuppressFBWarnings(justification = "generated code")
      private boolean otpCaseSensitive;
      @SuppressFBWarnings(justification = "generated code")
      private String authenticationMethodId;

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

      @SuppressFBWarnings(justification = "generated code")
      public SmsIdentityVerificationStepConfig.SmsIdentityVerificationStepConfigBuilder skipCondition(FlowConditionConfig skipCondition) {
         this.skipCondition = skipCondition;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SmsIdentityVerificationStepConfig.SmsIdentityVerificationStepConfigBuilder preCondition(FlowConditionConfig preCondition) {
         this.preCondition = preCondition;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SmsIdentityVerificationStepConfig.SmsIdentityVerificationStepConfigBuilder requiresActivation(boolean requiresActivation) {
         this.requiresActivation = requiresActivation;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SmsIdentityVerificationStepConfig.SmsIdentityVerificationStepConfigBuilder tagsOnSuccess(List<TagConfig> tagsOnSuccess) {
         this.tagsOnSuccess = tagsOnSuccess;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SmsIdentityVerificationStepConfig.SmsIdentityVerificationStepConfigBuilder stepId(@Nullable StepIdConfig stepId) {
         this.stepId = stepId;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SmsIdentityVerificationStepConfig.SmsIdentityVerificationStepConfigBuilder interactiveGotoTarget(InteractiveGotoTargetConfig interactiveGotoTarget) {
         if (this.interactiveGotoTargets == null) {
            this.interactiveGotoTargets = new ArrayList<>();
         }

         this.interactiveGotoTargets.add(interactiveGotoTarget);
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SmsIdentityVerificationStepConfig.SmsIdentityVerificationStepConfigBuilder interactiveGotoTargets(
         Collection<? extends InteractiveGotoTargetConfig> interactiveGotoTargets
      ) {
         if (interactiveGotoTargets == null) {
            throw new NullPointerException("interactiveGotoTargets cannot be null");
         }

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

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

      @SuppressFBWarnings(justification = "generated code")
      public SmsIdentityVerificationStepConfig.SmsIdentityVerificationStepConfigBuilder clearInteractiveGotoTargets() {
         if (this.interactiveGotoTargets != null) {
            this.interactiveGotoTargets.clear();
         }

         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SmsIdentityVerificationStepConfig.SmsIdentityVerificationStepConfigBuilder dynamicStepActivation(DynamicStepActivationConfig dynamicStepActivation) {
         if (this.dynamicStepActivations == null) {
            this.dynamicStepActivations = new ArrayList<>();
         }

         this.dynamicStepActivations.add(dynamicStepActivation);
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SmsIdentityVerificationStepConfig.SmsIdentityVerificationStepConfigBuilder dynamicStepActivations(
         Collection<? extends DynamicStepActivationConfig> dynamicStepActivations
      ) {
         if (dynamicStepActivations == null) {
            throw new NullPointerException("dynamicStepActivations cannot be null");
         }

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

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

      @SuppressFBWarnings(justification = "generated code")
      public SmsIdentityVerificationStepConfig.SmsIdentityVerificationStepConfigBuilder clearDynamicStepActivations() {
         if (this.dynamicStepActivations != null) {
            this.dynamicStepActivations.clear();
         }

         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SmsIdentityVerificationStepConfig.SmsIdentityVerificationStepConfigBuilder onFailureGotos(Map<String, StepIdConfig> onFailureGotos) {
         this.onFailureGotos = onFailureGotos;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SmsIdentityVerificationStepConfig.SmsIdentityVerificationStepConfigBuilder customResponseAttributes(
         List<ValueMapProviderConfig> customResponseAttributes
      ) {
         this.customResponseAttributes = customResponseAttributes;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SmsIdentityVerificationStepConfig.SmsIdentityVerificationStepConfigBuilder customFailureResponseAttributes(
         List<ValueMapProviderConfig> customFailureResponseAttributes
      ) {
         this.customFailureResponseAttributes = customFailureResponseAttributes;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SmsIdentityVerificationStepConfig.SmsIdentityVerificationStepConfigBuilder mtanSettings(BasicMtanSettings mtanSettings) {
         this.mtanSettings = mtanSettings;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SmsIdentityVerificationStepConfig.SmsIdentityVerificationStepConfigBuilder messageProvider(MtanMessageProviderConfig messageProvider) {
         this.messageProvider = messageProvider;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SmsIdentityVerificationStepConfig.SmsIdentityVerificationStepConfigBuilder otpGenerator(SecretStringGenerator otpGenerator) {
         this.otpGenerator = otpGenerator;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SmsIdentityVerificationStepConfig.SmsIdentityVerificationStepConfigBuilder maxFailedAttempts(int maxFailedAttempts) {
         this.maxFailedAttempts = maxFailedAttempts;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SmsIdentityVerificationStepConfig.SmsIdentityVerificationStepConfigBuilder otpValidity(int otpValidity) {
         this.otpValidity = otpValidity;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SmsIdentityVerificationStepConfig.SmsIdentityVerificationStepConfigBuilder otpCaseSensitive(boolean otpCaseSensitive) {
         this.otpCaseSensitive = otpCaseSensitive;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SmsIdentityVerificationStepConfig.SmsIdentityVerificationStepConfigBuilder authenticationMethodId(String authenticationMethodId) {
         this.authenticationMethodId = authenticationMethodId;
         return this;
      }

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

         return new SmsIdentityVerificationStepConfig(
            this.skipCondition,
            this.preCondition,
            this.requiresActivation,
            this.tagsOnSuccess,
            this.stepId,
            interactiveGotoTargets,
            switch (this.dynamicStepActivations == null ? 0 : this.dynamicStepActivations.size()) {
               case 0 -> Collections.emptyList();
               case 1 -> Collections.singletonList(this.dynamicStepActivations.get(0));
               default -> Collections.unmodifiableList(new ArrayList<>(this.dynamicStepActivations));
            },
            this.onFailureGotos,
            this.customResponseAttributes,
            this.customFailureResponseAttributes,
            this.mtanSettings,
            this.messageProvider,
            this.otpGenerator,
            this.maxFailedAttempts,
            this.otpValidity,
            this.otpCaseSensitive,
            this.authenticationMethodId
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "SmsIdentityVerificationStepConfig.SmsIdentityVerificationStepConfigBuilder(skipCondition="
            + this.skipCondition
            + ", preCondition="
            + this.preCondition
            + ", requiresActivation="
            + this.requiresActivation
            + ", tagsOnSuccess="
            + this.tagsOnSuccess
            + ", stepId="
            + this.stepId
            + ", interactiveGotoTargets="
            + this.interactiveGotoTargets
            + ", dynamicStepActivations="
            + this.dynamicStepActivations
            + ", onFailureGotos="
            + this.onFailureGotos
            + ", customResponseAttributes="
            + this.customResponseAttributes
            + ", customFailureResponseAttributes="
            + this.customFailureResponseAttributes
            + ", mtanSettings="
            + this.mtanSettings
            + ", messageProvider="
            + this.messageProvider
            + ", otpGenerator="
            + this.otpGenerator
            + ", maxFailedAttempts="
            + this.maxFailedAttempts
            + ", otpValidity="
            + this.otpValidity
            + ", otpCaseSensitive="
            + this.otpCaseSensitive
            + ", authenticationMethodId="
            + this.authenticationMethodId
            + ")";
      }
   }
}
