package com.airlock.iam.flow.shared.application.configuration.matrix;

import com.airlock.iam.core.misc.renderer.TokenListRenderer;
import com.airlock.iam.core.misc.tanserver.core.TanListType;
import com.airlock.iam.core.misc.tanserver.service.TanService;
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.domain.model.matrix.MatrixCheckingStepActions;
import com.airlock.iam.plugin.api.annotation.config.ConfigurationValidator;
import com.airlock.iam.plugin.api.annotation.config.NextActions;
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.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.plugin.api.exception.PropertyConstraintViolationException;
import com.airlock.iam.plugin.api.valuereader.EnumPropertyValueReader;
import com.airlock.iam.plugin.api.valuereader.PluginPropertyValueReader;
import com.airlock.iam.plugin.framework.domain.model.property.EnumPropertyTranslator;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.annotation.Nullable;
import java.util.List;
import java.util.Map;

@NextActions(MatrixCheckingStepActions.class)
public abstract class CommonMatrixCheckingStepConfig extends AbstractInteractiveFlowStepConfig {
   private static final String TYPE_INDEXED_LIST = "Indexed TAN list";
   private static final String TYPE_MATRIX_CARD = "Matrix card";
   @PluginProperty(description = "The TAN service to be used.", displayName = "TAN Service", optional = false)
   @GroupProperty(groupIdentifier = "Basic Settings")
   private TanService tanService;
   @EnumProperty(
      description = "The type of the Matrix Card/TAN list to be used. It is one of the following:<ul><li><b>Indexed TAN list:</b> A token list with an index next to each token. The tokens are queried in random order.</li><li><b>Matrix card:</b> A matrix card with the tokens organized in rows and columns. The tokens are queried in random order.</li></ul>",
      displayName = "TAN List Type",
      optional = false
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private CommonMatrixCheckingStepConfig.MatrixCardType tanListType;
   public static final String TAN_LIST_TYPE_FIELD = "tanListType";
   private static final String TAN_LIST_TYPE_DISPLAY_NAME = "TAN List Type";
   @PluginProperty(
      description = "Tells the authenticator which token list renderer has been used for producing the matrix card. This is needed for the translation of internal indices to challenge coordinates.<br/>This property is only required if \"TAN List Type\" is set to <b>Matrix card</b>.",
      displayName = "Token List Renderer",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private TokenListRenderer tokenListRenderer;
   private static final String TOKEN_LIST_RENDERER_FIELD = "tokenListRenderer";
   private static final String TOKEN_LIST_RENDERER_DISPLAY_NAME = "Token List Renderer";
   @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 = "MATRIX",
      maxLength = 23,
      optional = true
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private String authenticationMethodId;
   @IntegerProperty(
      description = "If indexed token lists (see configuration property \"TAN List Type\") are used, this property defines the lowest index. Usually the start index is zero or one (default).<br/>If the \"TAN List Type\" is not \"Indexed TAN list\", this property is ignored.",
      defaultValue = 1,
      optional = true,
      exampleValues = {0, 1},
      minValue = 0
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private int startIndex;
   @IntegerProperty(
      description = "The number of milliseconds a response is valid for. If a response is provided\ncorrectly but after its expiration, the step will fail.\n<p>If not set, this feature is disabled, i.e. challenges never expire (this is the default).</p>",
      optional = true,
      minValue = 1,
      maxValue = 86400000,
      exampleValues = {60000, 600000},
      displayName = "Challenge Validity [ms]"
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private Integer responseValidityMillis;
   @IntegerProperty(
      description = "<p>The number of times a wrong response can be sent before the flow is aborted.\nIf set to zero (the default), only one attempt is possible.</p>\n<p>The purpose of this settings is usability. The failed attempts counter is always increased upon\nreceiving a wrong response and the user is locked when the global failed attempts limit is exceeded.</p>",
      displayName = "Max Retries",
      minValue = 0,
      defaultValue = 0,
      optional = true,
      exampleValues = {0, 3, 5}
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private int maxRetries;
   private static final String MAX_RETRIES_DISPLAY_NAME = "Max Retries";
   @BooleanProperty(
      description = "If \"Max Retries\" is set to a value bigger than 0, this property specifies if a new challenge is generated for the retry.",
      optional = true,
      defaultValue = "true"
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private boolean newChallengeOnRetry;
   @BooleanProperty(
      description = "<p>If enabled, any pending challenge that is abandoned will be counted as an unanswered challenge. After too many unanswered challenges (see the \"Max Unanswered Challenges\" property), further attempts will always fail. This prevents an attacker from being able to \"wait\" for a specific challenge that has been leaked.</p><p><b>Important</b>: This feature requires the properties 'Col Challenge Open Since' and 'Col Unanswered Challenges' on the Token List Persister to be configured, otherwise it will not work properly.</p>",
      displayName = "Count Unanswered Challenges",
      defaultValue = "true",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private boolean countUnansweredChallenges;
   private static final String COUNT_UNANSWERED_CHALLENGES_DISPLAY_NAME = "Count Unanswered Challenges";
   @IntegerProperty(
      description = "When \"Count Unanswered Challenges\" is enabled, this property sets the timeout for unanswered challenges. When an unanswered challenge times out, the unanswered challenges counter is reset. Make sure to also configure the properties 'Col Challenge Open Since' and 'Col Unanswered Challenges' on the Token List Persister, otherwise this feature will not work properly.",
      exampleValues = {1, 2, 24, 48},
      defaultValue = 12,
      displayName = "Unanswered Challenge Timeout [in Hours]",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private int unansweredChallengeTimeout;
   @IntegerProperty(
      description = "When \"Count Unanswered Challenges\" is enabled, this property sets the maximum number of unanswered challenges. Once this limit is exceeded, the step will always fail.",
      displayName = "Max Unanswered Challenges",
      exampleValues = {1, 5, 10},
      defaultValue = 3,
      optional = true
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private int maxUnansweredChallenges;
   private static final String MAX_UNANSWERED_CHALLENGES_DISPLAY_NAME = "Max Unanswered Challenges";

   protected CommonMatrixCheckingStepConfig(
      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,
      TanService tanService,
      CommonMatrixCheckingStepConfig.MatrixCardType tanListType,
      TokenListRenderer tokenListRenderer,
      String authenticationMethodId,
      int startIndex,
      Integer responseValidityMillis,
      int maxRetries,
      boolean newChallengeOnRetry,
      boolean countUnansweredChallenges,
      int unansweredChallengeTimeout,
      int maxUnansweredChallenges
   ) {
      super(
         skipCondition,
         preCondition,
         requiresActivation,
         tagsOnSuccess,
         stepId,
         interactiveGotoTargets,
         dynamicStepActivations,
         onFailureGotos,
         customResponseAttributes,
         customFailureResponseAttributes
      );
      this.tanService = tanService;
      this.tanListType = tanListType;
      this.tokenListRenderer = tokenListRenderer;
      this.authenticationMethodId = authenticationMethodId;
      this.startIndex = startIndex;
      this.responseValidityMillis = responseValidityMillis;
      this.maxRetries = maxRetries;
      this.newChallengeOnRetry = newChallengeOnRetry;
      this.countUnansweredChallenges = countUnansweredChallenges;
      this.unansweredChallengeTimeout = unansweredChallengeTimeout;
      this.maxUnansweredChallenges = maxUnansweredChallenges;
   }

   @ConfigurationValidator
   static void assureTokenListRendererForMatrixCards(
      PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context, Environment validationEnvironment
   ) throws ConfigurationException {
      CommonMatrixCheckingStepConfig.MatrixCardType tanListType = (CommonMatrixCheckingStepConfig.MatrixCardType)EnumPropertyValueReader.effectiveConfigEnum(
         config, "tanListType", env, context
      );
      if (isMatrixCardAndTokenListRendererIsNotConfigured(config, env, context, tanListType)) {
         throw new PropertyConstraintViolationException(
            "tokenListRenderer",
            config,
            env,
            context,
            "If the TAN List Type is \""
               + CommonMatrixCheckingStepConfig.MatrixCardType.MATRIX_CARD.getDisplayValue()
               + "\", a Token List Renderer must be configured."
         );
      }
   }

   private static boolean isMatrixCardAndTokenListRendererIsNotConfigured(
      PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context, CommonMatrixCheckingStepConfig.MatrixCardType listType
   ) throws ConfigurationException {
      return CommonMatrixCheckingStepConfig.MatrixCardType.MATRIX_CARD == listType
         && PluginPropertyValueReader.effectivePluginPropertyValue(config, "tokenListRenderer", env, context) == null;
   }

   public TanService getTanService() {
      return this.tanService;
   }

   public CommonMatrixCheckingStepConfig.MatrixCardType getTanListType() {
      return this.tanListType;
   }

   @Nullable
   public TokenListRenderer getTokenListRenderer() {
      return this.tokenListRenderer;
   }

   public String getAuthenticationMethodId() {
      return this.authenticationMethodId;
   }

   public int getStartIndex() {
      return this.startIndex;
   }

   @Nullable
   public Integer getResponseValidityMillis() {
      return this.responseValidityMillis;
   }

   public int getMaxRetries() {
      return this.maxRetries;
   }

   public boolean isNewChallengeOnRetry() {
      return this.newChallengeOnRetry;
   }

   public boolean isCountUnansweredChallenges() {
      return this.countUnansweredChallenges;
   }

   public int getUnansweredChallengeTimeout() {
      return this.unansweredChallengeTimeout;
   }

   public int getMaxUnansweredChallenges() {
      return this.maxUnansweredChallenges;
   }

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

   public enum MatrixCardType implements EnumPropertyTranslator {
      INDEXED_LIST("Indexed TAN list", TanListType.INDEXED_LIST),
      MATRIX_CARD("Matrix card", TanListType.MATRIX_CARD);

      private final String displayValue;
      private final TanListType tanChallengeAuthenticatorType;

      public String getDisplayValue() {
         return this.displayValue;
      }

      public TanListType getTanChallengeAuthenticatorType() {
         return this.tanChallengeAuthenticatorType;
      }

      @SuppressFBWarnings(justification = "generated code")
      MatrixCardType(final String displayValue, final TanListType tanChallengeAuthenticatorType) {
         this.displayValue = displayValue;
         this.tanChallengeAuthenticatorType = tanChallengeAuthenticatorType;
      }
   }
}
