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

import com.airlock.iam.authentication.domain.model.airlock2fa.Airlock2FAUserFactorAuthenticationStep;
import com.airlock.iam.core.misc.authen.SecretStringGenerator;
import com.airlock.iam.core.misc.impl.authen.Alphabet;
import com.airlock.iam.core.misc.impl.authen.ExtendedStringGenerator;
import com.airlock.iam.core.misc.impl.authen.StringGeneratorHelper;
import com.airlock.iam.core.misc.impl.authen.StringGeneratorRuntimeException;
import com.airlock.iam.factor.application.configuration.airlock2fa.Airlock2FASettings;
import com.airlock.iam.factor.application.configuration.airlock2fa.validation.Airlock2FAShortLivedQrCodeConfigValidator;
import com.airlock.iam.factor.domain.model.airlock2fa.Airlock2FAFactor;
import com.airlock.iam.factor.domain.model.airlock2fa.Airlock2FAFactorPriority;
import com.airlock.iam.factor.domain.model.airlock2fa.Airlock2FAShortLivedOnlineQrCodeSettings;
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.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.Airlock2FAMessageProviderConfig;
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.license.PluginLicenseRestriction;
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.IntegerProperty;
import com.airlock.iam.plugin.api.annotation.property.PluginListProperty;
import com.airlock.iam.plugin.api.annotation.property.PluginMapProperty;
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.InitializeException;
import com.airlock.iam.plugin.api.exception.PropertyConstraintViolationException;
import com.airlock.iam.plugin.api.exception.PropertyTypeMismatchException;
import com.airlock.iam.plugin.api.valuereader.BooleanPropertyValueReader;
import com.airlock.iam.plugin.api.valuereader.PluginMapPropertyValueReader;
import com.airlock.iam.plugin.api.valuereader.PluginPropertyValueReader;
import com.airlock.iam.plugin.api.valuereader.StringPropertyValueReader;
import com.airlock.iam.plugin.framework.application.service.builder.plugin.PluginPropertyBuilderInitializer;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.annotation.Nullable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Map.Entry;

@Plugin(
   description = "Configuration of an Airlock 2FA authentication step for any of the factors One-Touch, Online QR Code, Passcode or Offline QR Code.<p>The identifier of the authentication method for this step is 'AIRLOCK_2FA' and is also the identifier for failed authentication attempts.</p><p>Note that for mobile-only authentication scenarios, the other authentication plugin \"Airlock 2FA Mobile Only Authentication Step\" should be used.</p>",
   displayName = "Airlock 2FA Authentication Step"
)
@PluginLicenseRestriction(moduleAffiliation = "Airlock2FA")
@Groups(
   openGroups = "Basic Settings",
   groupSequence = {"Basic Settings", "Short-Lived Online QR Code Settings", "Login ID", "Tags/Guards", "Flow Control", "Advanced Settings"}
)
@NextActions(Airlock2FAUserFactorAuthenticationStep.Airlock2FAUserFactorAuthenticationStepAction.class)
public final class Airlock2FAUserFactorAuthenticationStepConfig extends Airlock2FAAuthenticationStepConfig {
   public static final String AIRLOCK_2FA_USER_FACTOR_AUTHENTICATION_STEP_DISPLAY_NAME = "Airlock 2FA Authentication Step";
   static final String LOGIN_ID_SETTINGS = "Login ID";
   @StringListProperty(
      description = "<p>Priority list of all enabled factors. Only factors that are in this list can be used for authentication.\nThe factors are offered in the configured order.</p>\n<p>Online factors (One-Touch and Online QR Code) must come before all other factors.\nIt is recommended to include at least one offline factor.</p>\n<p>Available factors:\n  <ul>\n     <li><b>One-Touch</b>: a push message is sent to the user's mobile app, where it must be approved. This is an online factor and will\n         require device selection if the user has multiple devices.</li>\n     <li><b>Online QR Code</b>: a QR code is displayed in the browser, which has to be scanned by a mobile app and approved there. This\n         is an online factor. No prior device selection is required.</li>\n     <li><b>Passcode</b>: the device (mobile app or hardware token) generates a time-dependent code (OTP) that has to be entered manually\n         in the browser. This is an offline factor. No prior device selection is required.</li>\n     <li><b>Offline QR Code</b>: a QR code is displayed in the browser which has to be scanned by a mobile app or hardware token. The device\n         displays a code (OTP) that must be entered manually in the browser. This is an offline factor and will\n         require device selection if the user has multiple devices.</li>\n</ul></p>",
      optional = true,
      minElements = 1,
      unique = true,
      defaultValue = {"One-Touch", "Passcode", "Offline QR Code"},
      allowedValues = {"One-Touch", "Online QR Code", "Passcode", "Offline QR Code"}
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private List<String> factors;
   static final String FACTORS_PROPERTY = "factors";
   @IntegerProperty(
      description = "Defines the number of failed passcode checks that may occur before the flow is aborted. Setting this value to <var>n</var> means that the flow is aborted on the <var>n + 1</var>st failed attempt. This value must be less than \"Max Failed Logins\" in the \"Authentication Flows\" settings to be effective.",
      minValue = 0,
      defaultValue = 3,
      optional = true
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private int maxFailedPasscodeCheckAttempts;
   static final String ENFORCE_DEVICE_SELECTION_FIELD = "enforceDeviceSelection";
   @BooleanProperty(
      description = "Defines if the device has to be selected even when there is only one selectable device.",
      defaultValue = "false",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private boolean enforceDeviceSelection;
   static final String PUSH_TO_ALL_FIELD = "enablePushToAll";
   @BooleanProperty(
      displayName = "Enable Push-to-All",
      description = "<p>If Push-to-All is enabled for One-Touch, device selection is never required for One-Touch. Push notifications are sent to all of a user's devices and authentication can be approved on any of the devices.</p><p>The combination of Push-to-All and \"Cooldown Period\" can result in push notifications being sent to devices that are currently still in cooldown. However, those devices can not be used for successfully completing the authentication.</p><p>The combination of Push-to-All and \"Lock User on Fraud\" could have undesired effects, because users might report fraud in legitimate use-cases.</p>",
      defaultValue = "false",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private boolean enablePushToAll;
   @PluginProperty(
      description = "<p>Creates the message that will be displayed on the user's device when using One-Touch.\nIf no message provider is configured, only a title with the fixed translation key\n\"airlock2fa.one-touch.authentication-title\" or its fallback value \"Login\" is used.</p>\n<p>Using a custom Message Provider could prevent authentication with a smartwatch:\nBecause additional information is included, the app forces the user to scroll\nthrough the message (which might not be supported by the watch).</p>",
      displayName = "One-Touch Message Provider",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private Airlock2FAMessageProviderConfig messageProvider;
   public static final String MESSAGE_PROVIDER_FIELD = "messageProvider";
   @PluginProperty(
      description = "\t<p>Creates the message that will be displayed on the user's device when using Online QR Code or Offline QR Code factors.\n\tIf no message provider is configured, the default title of Futurae will be shown (without any additional information items).</p>\n\t<p>Note that the Login ID cannot be included because it is only available in the One-Touch Message Provider.</p>\n\t<p>Also, because of technical limitations, the title of Offline QR Codes is always the default title from Futurae, the configuration is ignored.</p>\n",
      displayName = "QR Code Message Provider",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private Airlock2FAMessageProviderConfig qrCodeMessageProvider;
   @BooleanProperty(
      displayName = "Enable Short-Lived Online QR Codes",
      description = "<p>Whether to enable short-lived Online QR Codes. Unlike regular Online QR Codes, these are refreshed regularly, allowing for shorter individual validities.</p>\n<p>Shorter validities enhance security, since forwarding a QR code to victims and tricking them to scan the QR code becomes more difficult if the available time window is small.</p>",
      defaultValue = "false",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Short-Lived Online QR Code Settings")
   private boolean enableShortLivedOnlineQrCodes;
   @IntegerProperty(
      displayName = "QR Code Validity [s]",
      description = "<p>The maximum amount of time in seconds for which an Online QR Code is valid after it is first displayed to\nthe end user (ignoring latency). This duration includes the time defined for the validity overlap.\nIt only limits the time for scanning the QR code, not for the confirmation or approval afterwards.</p>\n<p><strong> Security Notice: The validity duration represents the attack window. Choosing a small validity makes\nattacks more difficult, in cases where an attacker attempts to forward a QR code to a victim for scanning.</strong></p>\n<p>This setting is only active if short-lived Online QR Codes are enabled.</p>",
      optional = true,
      defaultValue = 10,
      minValue = 3
   )
   @GroupProperty(groupIdentifier = "Short-Lived Online QR Code Settings")
   private int shortLivedQrCodeValidity;
   @IntegerProperty(
      displayName = "QR Code Validity Overlap [s]",
      description = "<p>Defines the duration in seconds during which the previously displayed QR Code is still valid after being\nreplaced by the next QR code in sequence.</p>\n<p>This provides time for pending requests to complete and ensures that a valid QR code is displayed at every\nmoment in time, provided that there are no network or performance issues.</p>\n<p>The validity overlap must meet the following criteria:</p>\n<ul>\n<li>It must be smaller than half the overall validity of the QR code.</li>\n<li>It must be larger than the Loginapp UI polling interval (1s) plus the network latency (IAM backend &rarr;\nLoginapp UI plus Mobile Device &rarr; Futurae Backend).<br/>\nNote that the polling interval may differ for custom user interfaces.</li>\n</ul>\n<p>This setting is only active if short-lived Online QR Codes are enabled.</p>",
      optional = true,
      defaultValue = 3,
      minValue = 2
   )
   @GroupProperty(groupIdentifier = "Short-Lived Online QR Code Settings")
   private int shortLivedQrCodeValidityOverlap;
   @IntegerProperty(
      displayName = "Session Timeout [s]",
      description = "<p>Maximum duration in seconds during which short-lived Online QR Codes are displayed until a session timeout occurs.</p>\n<p>This setting is used exclusively for short-lived Online QR Codes. It has no effect if short-lived Online QR Codes are disabled.</p>",
      optional = true,
      defaultValue = 60,
      minValue = 30,
      maxValue = 120,
      exampleValues = {30, 60, 90}
   )
   @GroupProperty(groupIdentifier = "Short-Lived Online QR Code Settings")
   private int shortLivedSessionTimeout;
   @BooleanProperty(
      description = "<p>If enabled, a random ID is generated and shown to the user during One-Touch authentication.</p>\n<p>The ID is generated according to the pattern configured below.\n</p>\n<p>The ID is shown on the Airlock 2FA device and on the login page, allowing the user to correlate the session.</p>\n<p>The \"One-Touch Message Provider\" property must be configured for the Login ID to be displayed on the device.\nThe message provider can use the Login ID by configuring a dedicated value provider.</p>\n<p>If the multi-numbered challenge feature is enabled on the Futurae service, \"Generate One-Touch Login ID\" should be disabled. In that case, the Login ID does not provide any security\nenhancement but severely impacts usability.</p>",
      defaultValue = "true",
      optional = true,
      displayName = "Generate One-Touch Login ID"
   )
   @GroupProperty(groupIdentifier = "Login ID")
   private boolean generateLoginId;
   public static final String GENERATE_LOGIN_ID_FIELD = "generateLoginId";
   public static final String GENERATE_LOGIN_ID_DISPLAY_NAME = "Generate One-Touch Login ID";
   @StringProperty(
      description = "If enabled through the Generate One-Touch Login ID property, an ID is generated and shown\nto the user during One-Touch authentication according to the pattern defined in this property.\n<p><b>Pattern syntax:</b><br/>\n<code>pattern = fix_part | random_part [fix_part | random_part]*</code><br/>\n<code>random_part = {alphabet_name:number_of_characters}</code><br/>\n<code>fix_part = any_string_without_'{'</code>\n</p>\n<p>The <code>alphabet_name</code> refers either to a built-in alphabet (see below)\nor to a custom alphabet defined in the separate <b>Alphabets</b> property below.\n</p>\n<p><b>Examples:</b><br/>\n<code>{digits:6}</code> &rarr; 482913<br/>\n<code>OTP-{digits:4}</code> &rarr; OTP-4821<br/>\n<code>{HEX:8}</code> (with HEX defined in the custom Alphabets property below) &rarr; A9F03C1B\n</p>\n<p>Built-in and ready-to-use alphabets are:</p>\n<ul>\n<li>\"<code>digits</code>\" all decimal digits (i.e. the characters 0123456790)</li>\n<li>\"<code>lower26</code>\" standard alphabet with 26 lowercase letters (i.e. the characters abcdefghijklmnopqrstuvwxyz)</li>\n<li>\"<code>upper26</code>\" standard alphabet with 26 uppercase letters (i.e. the characters ABCDEFGHIJKLMNOPQRSTUVWXYZ)</li>\n<li>\"<code>alpha52</code>\" standard alphabet with 26 upper- and 26 lowercase letters (i.e. the characters ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz)</li>\n<li>\"<code>distinct</code>\" distinct standard characters: digits, upper- and lowercase letter without the hard to distinguish '0,O,1,l,I'\n (i.e. the characters 23456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ)</li>\n<li>\"<code>DISTINCT</code>\" distinct standard characters (with uppercase letters): digits and uppercase letter without the hard to distinguish '0,O,1,I'\n (i.e. the characters 23456789ABCDEFGHJKLMNPQRSTUVWXYZ)</li>\n<li>\"<code>extended</code>\" contains most of the characters visible on a computer keyboard without the hard to distinguish '0,O,1,l,I'\n (i.e. the characters +-.,:;$<>()[]{}%&!?/*@#=_23456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ)<br/>\n<b>NOTE:</b> Characters in this pattern do not pass the input filter for tokens (OTP, SMS, and alike).\n Choose a different pattern for tokens or relax the corresponding pattern (in the Loginapp's security settings). Characters may be blocked by a WAF deny rule.</li>\n</ul><p><b>Using custom alphabets:</b><br/>\n1. Define an alphabet in the <b>Alphabets</b> property below.<br/>\n2. Reference it in the pattern using <code>{alphabet_name:number_of_characters}</code> where the <alphabet_name> is the key of the alphabet in the Alphabets property.\n</p>\n<p><b>NOTE:</b> A pattern that results in a very long Login ID may negatively impact usability.\nIt may also cause issues when generating the push message, as push messages are limited in length.\nTo avoid rejection by Futurae during authentication, do not use special characters (e.g. from the 'extended' alphabet).</p>\n",
      optional = true,
      defaultValue = "{digits:6}",
      exampleValues = {"{digits:6}", "{DISTINCT:4}", "OTP-{digits:4}"},
      displayName = "Pattern"
   )
   @GroupProperty(groupIdentifier = "Login ID")
   private String loginIdPattern;
   private static final String LOGIN_ID_PATTERN_FIELD = "loginIdPattern";
   @PluginMapProperty(
      description = "A map of custom alphabets that can be referenced in the <b>Pattern</b> property.\n<p><b>How it works:</b><br/>\nThe map key defines the <code>alphabet_name:number_of_characters</code>.<br/>\nThe plugin (alphabet) defines the characters used for sampling during random generation.<br/>\nThe alphabet can then be referenced in the Pattern property above using <code>{alphabet_name:number_of_characters}</code>.\n</p>\n<p><b>Example configuration:</b><br/>\nKey (alphabet_name): <code>HEX</code><br/>\nPlugin: Alphabet with the following characters <code>0123456789ABCDEF</code>\n</p>\n<p><b>Example usage in the Pattern property above:</b><br/>\n<code>{HEX:8}</code> &rarr; A9F03C1B<br/>\n<code>OTP-{HEX:6}</code> &rarr; OTP-4F9A2C\n</p>\n",
      optional = true,
      displayName = "Alphabets"
   )
   @GroupProperty(groupIdentifier = "Login ID")
   private Map<String, Alphabet> loginIdAlphabets;
   private static final String LOGIN_ID_ALPHABETS_FIELD = "loginIdAlphabets";
   private SecretStringGenerator loginIdGenerator;
   @PluginListProperty(
      description = "Additional success tags to be granted if the step is completed using One-Touch.",
      displayName = "Tags On Successful One-Touch",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Tags/Guards")
   private List<TagConfig> tagsOnSuccessfulOneTouch;
   @PluginListProperty(
      description = "Additional success tags to be granted if the step is completed using online QR Code.",
      displayName = "Tags On Successful Online QR Code",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Tags/Guards")
   private List<TagConfig> tagsOnSuccessfulOnlineQrCode;
   @PluginListProperty(
      description = "Additional success tags to be granted if the step is completed using passcode.",
      displayName = "Tags On Successful Passcode Check",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Tags/Guards")
   private List<TagConfig> tagsOnSuccessfulPasscodeCheck;
   @PluginListProperty(
      description = "Additional success tags to be granted if the step is completed using Offline QR Code.",
      displayName = "Tags On Successful Offline QR Code",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Tags/Guards")
   private List<TagConfig> tagsOnSuccessfulOfflineQrCode;
   @PluginListProperty(
      description = "Additional success tags to be granted if the step is completed using bypass.",
      displayName = "Tags On Successful Bypass",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private List<TagConfig> tagsOnSuccessfulBypass;

   public Airlock2FAFactorPriority getFactorPriority() {
      return new Airlock2FAFactorPriority(this.factors.stream().map(Airlock2FAFactor::ofDisplayName).toList());
   }

   private Airlock2FAUserFactorAuthenticationStepConfig(
      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,
      Airlock2FASettings airlock2faSettings,
      List<String> factors,
      boolean generateLoginId,
      String loginIdPattern,
      Map<String, Alphabet> loginIdAlphabets,
      int maxFailedPasscodeCheckAttempts,
      boolean enforceDeviceSelection,
      boolean enablePushToAll,
      Airlock2FAMessageProviderConfig messageProvider,
      Airlock2FAMessageProviderConfig qrCodeMessageProvider,
      List<TagConfig> tagsOnSuccessfulOneTouch,
      List<TagConfig> tagsOnSuccessfulOnlineQrCode,
      List<TagConfig> tagsOnSuccessfulPasscodeCheck,
      List<TagConfig> tagsOnSuccessfulOfflineQrCode,
      List<TagConfig> tagsOnSuccessfulBypass,
      boolean respectCooldownPeriod,
      boolean enableShortLivedOnlineQrCodes,
      int shortLivedQrCodeValidity,
      int shortLivedQrCodeValidityOverlap,
      int shortLivedSessionTimeout
   ) {
      super(
         skipCondition,
         preCondition,
         requiresActivation,
         tagsOnSuccess,
         stepId,
         interactiveGotoTargets,
         dynamicStepActivations,
         onFailureGotos,
         customResponseAttributes,
         customFailureResponseAttributes,
         airlock2faSettings,
         respectCooldownPeriod
      );
      this.factors = factors;
      this.maxFailedPasscodeCheckAttempts = maxFailedPasscodeCheckAttempts;
      this.enforceDeviceSelection = enforceDeviceSelection;
      this.enablePushToAll = enablePushToAll;
      this.messageProvider = messageProvider;
      this.generateLoginId = generateLoginId;
      this.loginIdPattern = loginIdPattern;
      this.loginIdAlphabets = loginIdAlphabets;
      this.qrCodeMessageProvider = qrCodeMessageProvider;
      this.tagsOnSuccessfulOneTouch = tagsOnSuccessfulOneTouch;
      this.tagsOnSuccessfulOnlineQrCode = tagsOnSuccessfulOnlineQrCode;
      this.tagsOnSuccessfulPasscodeCheck = tagsOnSuccessfulPasscodeCheck;
      this.tagsOnSuccessfulOfflineQrCode = tagsOnSuccessfulOfflineQrCode;
      this.tagsOnSuccessfulBypass = tagsOnSuccessfulBypass;
      this.enableShortLivedOnlineQrCodes = enableShortLivedOnlineQrCodes;
      this.shortLivedQrCodeValidity = shortLivedQrCodeValidity;
      this.shortLivedQrCodeValidityOverlap = shortLivedQrCodeValidityOverlap;
      this.shortLivedSessionTimeout = shortLivedSessionTimeout;
   }

   @ConfigurationValidator
   public static void requireMessageProviderIfGenerateLoginId(
      PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context, Environment valEnv
   ) throws ConfigurationException {
      boolean generateLoginId = BooleanPropertyValueReader.effectiveBooleanPropertyValue(config, "generateLoginId", env, context);
      PluginConfiguration messageProviderConfig = PluginPropertyValueReader.effectivePluginPropertyValue(config, "messageProvider", env, context);
      if (generateLoginId && messageProviderConfig == null) {
         throw new PropertyConstraintViolationException(
            "messageProvider",
            config,
            env,
            context,
            "If a Login ID is used, a One-Touch Message Provider must be configured for the Login ID to be displayed on the device. The message provider can use the Login ID by configuring a dedicated value provider."
         );
      }
   }

   @ConfigurationValidator
   public static void requirePushToAllAndEnforceDeviceSelectionNotBothEnabled(
      PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context, Environment valEnv
   ) throws ConfigurationException {
      boolean pushToAll = BooleanPropertyValueReader.effectiveBooleanPropertyValue(config, "enablePushToAll", env, context);
      boolean enforceDeviceSelection = BooleanPropertyValueReader.effectiveBooleanPropertyValue(config, "enforceDeviceSelection", env, context);
      if (pushToAll && enforceDeviceSelection) {
         throw new PropertyConstraintViolationException(
            "enforceDeviceSelection", config, env, context, "Push-to-All and Enforce Device Selection cannot both be enabled."
         );
      }
   }

   @ConfigurationValidator
   public static void validateFactors(PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context, Environment valenv) throws ConfigurationException {
      Airlock2FASettings.validateFactors(config, "factors", env, context);
   }

   @ConfigurationValidator(localOnly = true)
   public static void validateShortLivedQrCodeSettings(PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext ctx, Environment vEnv) throws ConfigurationException {
      Airlock2FAShortLivedQrCodeConfigValidator.runShortLivedSettingsValidation("shortLivedSessionTimeout", Optional.of("factors"), config, env, ctx);
   }

   @ConfigurationValidator
   public static void validateLoginIdAlphabets(PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context, Environment valEnv) throws ConfigurationException {
      StringGeneratorHelper.validateAlphabets(config, env, context, "loginIdAlphabets");
   }

   @ConfigurationValidator
   public static void validateLoginIdPattern(PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context, Environment valEnv) throws ConfigurationException {
      String loginIdPattern = StringPropertyValueReader.effectiveStringConfigValue(config, "loginIdPattern", env, context);
      Map<String, Alphabet> loginIdAlphabets = getLoginIdAlphabets(config, env, context);

      try {
         createExtendedStringGenerator(loginIdPattern, loginIdAlphabets);
      } catch (InitializeException | StringGeneratorRuntimeException e) {
         throw new PropertyConstraintViolationException("loginIdPattern", config, env, context, e.getMessage());
      }
   }

   private static Map<String, Alphabet> getLoginIdAlphabets(PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context) throws PropertyTypeMismatchException {
      Map<String, PluginConfiguration> val = PluginMapPropertyValueReader.effectivePluginMapPropertyValue(config, "loginIdAlphabets", env, context);
      Map<String, Alphabet> alphabets = new HashMap<>();

      for (Entry<String, PluginConfiguration> entry : val.entrySet()) {
         alphabets.put(entry.getKey(), new Alphabet(StringPropertyValueReader.effectiveStringConfigValue(entry.getValue(), "characters", env, context)));
      }

      return alphabets;
   }

   @AfterConfigActivation
   void afterConfigActivation() throws InitializeException {
      try {
         this.loginIdGenerator = createExtendedStringGenerator(this.loginIdPattern, this.loginIdAlphabets);
      } catch (StringGeneratorRuntimeException e) {
         throw new InitializeException(e.getMessage());
      }
   }

   private static ExtendedStringGenerator createExtendedStringGenerator(String loginIdPattern, Map<String, Alphabet> loginIdAlphabets) throws InitializeException, StringGeneratorRuntimeException {
      return ((ExtendedStringGenerator.ExtendedStringGeneratorBuilder)PluginPropertyBuilderInitializer.builder(
            ExtendedStringGenerator.ExtendedStringGeneratorBuilder.class
         ))
         .pattern(loginIdPattern)
         .alphabets(loginIdAlphabets)
         .build();
   }

   public int getMaxFailedPasscodeCheckAttempts() {
      return this.maxFailedPasscodeCheckAttempts;
   }

   public boolean isEnforceDeviceSelection() {
      return this.enforceDeviceSelection;
   }

   public boolean isEnablePushToAll() {
      return this.enablePushToAll;
   }

   @Nullable
   public Airlock2FAMessageProviderConfig getMessageProvider() {
      return this.messageProvider;
   }

   @Nullable
   public Airlock2FAMessageProviderConfig getQrCodeMessageProvider() {
      return this.qrCodeMessageProvider;
   }

   public boolean isGenerateLoginId() {
      return this.generateLoginId;
   }

   public SecretStringGenerator getLoginIdGenerator() {
      return this.loginIdGenerator;
   }

   public List<TagConfig> getTagsOnSuccessfulOneTouch() {
      return this.tagsOnSuccessfulOneTouch;
   }

   public List<TagConfig> getTagsOnSuccessfulOnlineQrCode() {
      return this.tagsOnSuccessfulOnlineQrCode;
   }

   public List<TagConfig> getTagsOnSuccessfulPasscodeCheck() {
      return this.tagsOnSuccessfulPasscodeCheck;
   }

   public List<TagConfig> getTagsOnSuccessfulOfflineQrCode() {
      return this.tagsOnSuccessfulOfflineQrCode;
   }

   public List<TagConfig> getTagsOnSuccessfulBypass() {
      return this.tagsOnSuccessfulBypass;
   }

   public Optional<Airlock2FAShortLivedOnlineQrCodeSettings> getShortLivedOnlineQrCodeSettings() {
      return this.enableShortLivedOnlineQrCodes
         ? Optional.of(
            Airlock2FAShortLivedOnlineQrCodeSettings.shortLivedOnlineQrCodeSettings(
               this.shortLivedQrCodeValidity, this.shortLivedQrCodeValidityOverlap, this.shortLivedSessionTimeout
            )
         )
         : Optional.empty();
   }

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

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

   @SuppressFBWarnings(justification = "generated code")
   public static class Airlock2FAUserFactorAuthenticationStepConfigBuilder {
      @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 List<InteractiveGotoTargetConfig> interactiveGotoTargets;
      @SuppressFBWarnings(justification = "generated code")
      private List<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 Airlock2FASettings airlock2faSettings;
      @SuppressFBWarnings(justification = "generated code")
      private List<String> factors;
      @SuppressFBWarnings(justification = "generated code")
      private boolean generateLoginId;
      @SuppressFBWarnings(justification = "generated code")
      private String loginIdPattern;
      @SuppressFBWarnings(justification = "generated code")
      private Map<String, Alphabet> loginIdAlphabets;
      @SuppressFBWarnings(justification = "generated code")
      private int maxFailedPasscodeCheckAttempts;
      @SuppressFBWarnings(justification = "generated code")
      private boolean enforceDeviceSelection;
      @SuppressFBWarnings(justification = "generated code")
      private boolean enablePushToAll;
      @SuppressFBWarnings(justification = "generated code")
      private Airlock2FAMessageProviderConfig messageProvider;
      @SuppressFBWarnings(justification = "generated code")
      private Airlock2FAMessageProviderConfig qrCodeMessageProvider;
      @SuppressFBWarnings(justification = "generated code")
      private List<TagConfig> tagsOnSuccessfulOneTouch;
      @SuppressFBWarnings(justification = "generated code")
      private List<TagConfig> tagsOnSuccessfulOnlineQrCode;
      @SuppressFBWarnings(justification = "generated code")
      private List<TagConfig> tagsOnSuccessfulPasscodeCheck;
      @SuppressFBWarnings(justification = "generated code")
      private List<TagConfig> tagsOnSuccessfulOfflineQrCode;
      @SuppressFBWarnings(justification = "generated code")
      private List<TagConfig> tagsOnSuccessfulBypass;
      @SuppressFBWarnings(justification = "generated code")
      private boolean respectCooldownPeriod;
      @SuppressFBWarnings(justification = "generated code")
      private boolean enableShortLivedOnlineQrCodes;
      @SuppressFBWarnings(justification = "generated code")
      private int shortLivedQrCodeValidity;
      @SuppressFBWarnings(justification = "generated code")
      private int shortLivedQrCodeValidityOverlap;
      @SuppressFBWarnings(justification = "generated code")
      private int shortLivedSessionTimeout;

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

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

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

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

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

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

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FAUserFactorAuthenticationStepConfig.Airlock2FAUserFactorAuthenticationStepConfigBuilder interactiveGotoTargets(
         List<InteractiveGotoTargetConfig> interactiveGotoTargets
      ) {
         this.interactiveGotoTargets = interactiveGotoTargets;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FAUserFactorAuthenticationStepConfig.Airlock2FAUserFactorAuthenticationStepConfigBuilder dynamicStepActivations(
         List<DynamicStepActivationConfig> dynamicStepActivations
      ) {
         this.dynamicStepActivations = dynamicStepActivations;
         return this;
      }

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

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

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

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FAUserFactorAuthenticationStepConfig.Airlock2FAUserFactorAuthenticationStepConfigBuilder airlock2faSettings(
         Airlock2FASettings airlock2faSettings
      ) {
         this.airlock2faSettings = airlock2faSettings;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FAUserFactorAuthenticationStepConfig.Airlock2FAUserFactorAuthenticationStepConfigBuilder factors(List<String> factors) {
         this.factors = factors;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FAUserFactorAuthenticationStepConfig.Airlock2FAUserFactorAuthenticationStepConfigBuilder generateLoginId(boolean generateLoginId) {
         this.generateLoginId = generateLoginId;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FAUserFactorAuthenticationStepConfig.Airlock2FAUserFactorAuthenticationStepConfigBuilder loginIdPattern(String loginIdPattern) {
         this.loginIdPattern = loginIdPattern;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FAUserFactorAuthenticationStepConfig.Airlock2FAUserFactorAuthenticationStepConfigBuilder loginIdAlphabets(
         Map<String, Alphabet> loginIdAlphabets
      ) {
         this.loginIdAlphabets = loginIdAlphabets;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FAUserFactorAuthenticationStepConfig.Airlock2FAUserFactorAuthenticationStepConfigBuilder maxFailedPasscodeCheckAttempts(
         int maxFailedPasscodeCheckAttempts
      ) {
         this.maxFailedPasscodeCheckAttempts = maxFailedPasscodeCheckAttempts;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FAUserFactorAuthenticationStepConfig.Airlock2FAUserFactorAuthenticationStepConfigBuilder enforceDeviceSelection(
         boolean enforceDeviceSelection
      ) {
         this.enforceDeviceSelection = enforceDeviceSelection;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FAUserFactorAuthenticationStepConfig.Airlock2FAUserFactorAuthenticationStepConfigBuilder enablePushToAll(boolean enablePushToAll) {
         this.enablePushToAll = enablePushToAll;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FAUserFactorAuthenticationStepConfig.Airlock2FAUserFactorAuthenticationStepConfigBuilder messageProvider(
         Airlock2FAMessageProviderConfig messageProvider
      ) {
         this.messageProvider = messageProvider;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FAUserFactorAuthenticationStepConfig.Airlock2FAUserFactorAuthenticationStepConfigBuilder qrCodeMessageProvider(
         Airlock2FAMessageProviderConfig qrCodeMessageProvider
      ) {
         this.qrCodeMessageProvider = qrCodeMessageProvider;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FAUserFactorAuthenticationStepConfig.Airlock2FAUserFactorAuthenticationStepConfigBuilder tagsOnSuccessfulOneTouch(
         List<TagConfig> tagsOnSuccessfulOneTouch
      ) {
         this.tagsOnSuccessfulOneTouch = tagsOnSuccessfulOneTouch;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FAUserFactorAuthenticationStepConfig.Airlock2FAUserFactorAuthenticationStepConfigBuilder tagsOnSuccessfulOnlineQrCode(
         List<TagConfig> tagsOnSuccessfulOnlineQrCode
      ) {
         this.tagsOnSuccessfulOnlineQrCode = tagsOnSuccessfulOnlineQrCode;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FAUserFactorAuthenticationStepConfig.Airlock2FAUserFactorAuthenticationStepConfigBuilder tagsOnSuccessfulPasscodeCheck(
         List<TagConfig> tagsOnSuccessfulPasscodeCheck
      ) {
         this.tagsOnSuccessfulPasscodeCheck = tagsOnSuccessfulPasscodeCheck;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FAUserFactorAuthenticationStepConfig.Airlock2FAUserFactorAuthenticationStepConfigBuilder tagsOnSuccessfulOfflineQrCode(
         List<TagConfig> tagsOnSuccessfulOfflineQrCode
      ) {
         this.tagsOnSuccessfulOfflineQrCode = tagsOnSuccessfulOfflineQrCode;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FAUserFactorAuthenticationStepConfig.Airlock2FAUserFactorAuthenticationStepConfigBuilder tagsOnSuccessfulBypass(
         List<TagConfig> tagsOnSuccessfulBypass
      ) {
         this.tagsOnSuccessfulBypass = tagsOnSuccessfulBypass;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FAUserFactorAuthenticationStepConfig.Airlock2FAUserFactorAuthenticationStepConfigBuilder respectCooldownPeriod(
         boolean respectCooldownPeriod
      ) {
         this.respectCooldownPeriod = respectCooldownPeriod;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FAUserFactorAuthenticationStepConfig.Airlock2FAUserFactorAuthenticationStepConfigBuilder enableShortLivedOnlineQrCodes(
         boolean enableShortLivedOnlineQrCodes
      ) {
         this.enableShortLivedOnlineQrCodes = enableShortLivedOnlineQrCodes;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FAUserFactorAuthenticationStepConfig.Airlock2FAUserFactorAuthenticationStepConfigBuilder shortLivedQrCodeValidity(
         int shortLivedQrCodeValidity
      ) {
         this.shortLivedQrCodeValidity = shortLivedQrCodeValidity;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FAUserFactorAuthenticationStepConfig.Airlock2FAUserFactorAuthenticationStepConfigBuilder shortLivedQrCodeValidityOverlap(
         int shortLivedQrCodeValidityOverlap
      ) {
         this.shortLivedQrCodeValidityOverlap = shortLivedQrCodeValidityOverlap;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FAUserFactorAuthenticationStepConfig.Airlock2FAUserFactorAuthenticationStepConfigBuilder shortLivedSessionTimeout(
         int shortLivedSessionTimeout
      ) {
         this.shortLivedSessionTimeout = shortLivedSessionTimeout;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FAUserFactorAuthenticationStepConfig build() {
         return new Airlock2FAUserFactorAuthenticationStepConfig(
            this.skipCondition,
            this.preCondition,
            this.requiresActivation,
            this.tagsOnSuccess,
            this.stepId,
            this.interactiveGotoTargets,
            this.dynamicStepActivations,
            this.onFailureGotos,
            this.customResponseAttributes,
            this.customFailureResponseAttributes,
            this.airlock2faSettings,
            this.factors,
            this.generateLoginId,
            this.loginIdPattern,
            this.loginIdAlphabets,
            this.maxFailedPasscodeCheckAttempts,
            this.enforceDeviceSelection,
            this.enablePushToAll,
            this.messageProvider,
            this.qrCodeMessageProvider,
            this.tagsOnSuccessfulOneTouch,
            this.tagsOnSuccessfulOnlineQrCode,
            this.tagsOnSuccessfulPasscodeCheck,
            this.tagsOnSuccessfulOfflineQrCode,
            this.tagsOnSuccessfulBypass,
            this.respectCooldownPeriod,
            this.enableShortLivedOnlineQrCodes,
            this.shortLivedQrCodeValidity,
            this.shortLivedQrCodeValidityOverlap,
            this.shortLivedSessionTimeout
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "Airlock2FAUserFactorAuthenticationStepConfig.Airlock2FAUserFactorAuthenticationStepConfigBuilder(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
            + ", airlock2faSettings="
            + this.airlock2faSettings
            + ", factors="
            + this.factors
            + ", generateLoginId="
            + this.generateLoginId
            + ", loginIdPattern="
            + this.loginIdPattern
            + ", loginIdAlphabets="
            + this.loginIdAlphabets
            + ", maxFailedPasscodeCheckAttempts="
            + this.maxFailedPasscodeCheckAttempts
            + ", enforceDeviceSelection="
            + this.enforceDeviceSelection
            + ", enablePushToAll="
            + this.enablePushToAll
            + ", messageProvider="
            + this.messageProvider
            + ", qrCodeMessageProvider="
            + this.qrCodeMessageProvider
            + ", tagsOnSuccessfulOneTouch="
            + this.tagsOnSuccessfulOneTouch
            + ", tagsOnSuccessfulOnlineQrCode="
            + this.tagsOnSuccessfulOnlineQrCode
            + ", tagsOnSuccessfulPasscodeCheck="
            + this.tagsOnSuccessfulPasscodeCheck
            + ", tagsOnSuccessfulOfflineQrCode="
            + this.tagsOnSuccessfulOfflineQrCode
            + ", tagsOnSuccessfulBypass="
            + this.tagsOnSuccessfulBypass
            + ", respectCooldownPeriod="
            + this.respectCooldownPeriod
            + ", enableShortLivedOnlineQrCodes="
            + this.enableShortLivedOnlineQrCodes
            + ", shortLivedQrCodeValidity="
            + this.shortLivedQrCodeValidity
            + ", shortLivedQrCodeValidityOverlap="
            + this.shortLivedQrCodeValidityOverlap
            + ", shortLivedSessionTimeout="
            + this.shortLivedSessionTimeout
            + ")";
      }
   }
}
