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

import com.airlock.iam.base.api.domain.model.exception.FailureException;
import com.airlock.iam.core.application.configuration.airlock2fa.Airlock2FAGlobalSettings;
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.Airlock2FAPayloadEncryptionSettings;
import com.airlock.iam.factor.infrastructure.rest.airlock2fa.futurae.transfer.ExtraInfo;
import com.airlock.iam.factor.infrastructure.rest.airlock2fa.futurae.transfer.ExtraInfoEncryptionImpl;
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.license.PluginLicenseRestriction;
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.PropertyConfigurationException;
import com.airlock.iam.plugin.api.exception.PropertyConstraintViolationException;
import com.airlock.iam.plugin.api.valuereader.BooleanPropertyValueReader;
import com.airlock.iam.plugin.api.valuereader.StringListPropertyValueReader;
import com.airlock.iam.plugin.api.valuereader.StringPropertyValueReader;
import com.airlock.iam.plugin.framework.domain.service.validation.DurationParser;
import com.airlock.iam.plugin.framework.domain.service.validation.DurationValidator;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.time.Duration;
import java.util.List;
import java.util.Optional;

@Plugin(displayName = "Airlock 2FA Settings", description = "Global settings related to Airlock 2FA.")
@PluginLicenseRestriction(moduleAffiliation = "Airlock2FA")
@Groups(groupSequence = {"Basic Settings", "User Enrollment Settings", "Advanced Settings"}, openGroups = {"Basic Settings", "User Enrollment Settings"})
public final class Airlock2FASettings implements Airlock2FAGlobalSettings {
   public static final String AIRLOCK_2FA_AUTH_METHOD_ID = "AIRLOCK_2FA";
   public static final String AIRLOCK_2FA_SETTINGS_DISPLAY_NAME = "Airlock 2FA Settings";
   static final String USER_ENROLLMENT_SETTINGS = "User Enrollment Settings";
   public static final String ONLINE_QR_CODE_DISPLAY_NAME = "Online QR Code";
   public static final String ONE_TOUCH_DISPLAY_NAME = "One-Touch";
   public static final String PASSCODE_DISPLAY_NAME = "Passcode";
   public static final String OFFLINE_QR_CODE_DISPLAY_NAME = "Offline QR Code";
   public static final String MOBILE_ONLY_DISPLAY_NAME = "Mobile Only";
   public static final String TRUSTED_SESSION_BINDING_FOR_ACTIVATION_DISPLAY_NAME = "Trusted Session Binding for Activation";
   public static final String TRUSTED_SESSION_BINDING_FOR_RECOVERY_DISPLAY_NAME = "Trusted Session Binding for Recovery";
   @PluginProperty(description = "Configures the repository to store Airlock 2FA data.", optional = false)
   @GroupProperty(groupIdentifier = "Basic Settings")
   private Airlock2FARepositoryConfig repository;
   @PluginProperty(description = "Configures access to Futurae servers.", optional = false)
   @GroupProperty(groupIdentifier = "Basic Settings")
   private FuturaeServerConfig futuraeServer;
   @PluginProperty(
      description = "An optional display name that is displayed in the Airlock 2FA/Futurae mobile application.\nAs an example, it could be the IAM username or a configured context data item such as the user's email address.\nNote that currently the display name will <em>not</em> be updated if the underlying user data changed after the user being enrolled.\n<p><b>Privacy warning:</b>\nThe display name, when configured, will be stored on Futurae's servers.\nSensitive data should therefore not be used as display name.</p>",
      defaultValue = Airlock2FANoDisplayNameProviderConfig.class,
      optional = true
   )
   @GroupProperty(groupIdentifier = "User Enrollment Settings")
   private Airlock2FAUserAccountDisplayNameProviderConfig accountDisplayNameProvider;
   @EnumProperty(
      displayName = "Trusted Session Binding for Activation",
      description = "<p>If enabled, activation codes (from a letter or on-screen QR code) can only be used as part of an authenticated session\nwith Airlock IAM. If enabled, activation codes are only accepted together with a \"Trusted Session Binding Token\".\nThis short-lived token can only be retrieved from an Airlock IAM flow and must be sent to the Futurae server\ntogether with the activation code.</p>\n<p>This feature ensures that only the intended user can activate a 2FA app/device with a given activation code.\nThe standard Airlock 2FA app does not support this feature, a custom mobile app built using the\nFuturae SDK is required.</p>\n<p>Airlock IAM supports three modes for Trusted Session Binding:</p>\n<ul>\n<li><b>Never</b>: Trusted Session Binding is disabled.</li>\n<li><b>Only with Letter</b>: Trusted Session Binding is only enabled for activation letters.</li>\n<li><b>Always</b>: Trusted Session Binding is enabled both for activation letters and on-screen activation.\nFor on-screen activation, Trusted Session Binding does not provide additional security because the activation code\nis already bound to an authenticated IAM session, but it could simplify the implementation of the activation process in\nthe mobile app.</li>\n</ul>",
      defaultValue = "OFF",
      optional = true
   )
   @GroupProperty(groupIdentifier = "User Enrollment Settings")
   private Airlock2FAFlowBindingMode trustedSessionBindingForActivation;
   @BooleanProperty(
      displayName = "Trusted Session Binding for Recovery",
      description = "If enabled, \"Trusted Session Binding for Recovery\" will be enabled on newly activated Airlock 2FA devices. This means that these devices can only be recovered from a backup as part of an authenticated session with Airlock IAM. Already activated devices are not retrospectively affected by this setting.",
      defaultValue = "false",
      optional = true
   )
   @GroupProperty(groupIdentifier = "User Enrollment Settings")
   private boolean trustedSessionBindingForRecovery;
   @IntegerProperty(
      description = "The amount of time a Trusted Session Binding Token for device activation and recovery is valid in seconds. This setting only has an effect if at least one of the two following conditions is met:<ul><li>Trusted Session Binding for Activation is set to \"Only with Letter\" or \"Always\".</li><li>Trusted Session Binding for Recovery is enabled.</li></ul><p>The duration should not be larger than the \"Session Idle Timeout\" in the Loginapp, to avoid session timeouts when polling the IAM status.</p>",
      displayName = "Binding Token Validity [s]",
      optional = true,
      defaultValue = 120,
      minValue = 1,
      maxValue = 600,
      exampleValues = {30, 60, 120}
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private int trustedSessionBindingValidity;
   @BooleanProperty(
      displayName = "Lock User on Fraud",
      description = "If enabled, the user is locked with reason <code>LockReason.FraudReportedByUser</code> when reporting a\npossible fraudulent authentication attempt via the Airlock 2FA app.\nThis is done by rejecting the authentication attempt and then confirming that the attempt was not initiated by the user (in the app dialog).\n<p>Self-unlock is possible when locked by this option.</p>",
      optional = true,
      defaultValue = "false"
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private boolean lockUserOnFraud;
   public static final String LOCK_USER_ON_FRAUD_DISPLAY_NAME = "Lock User on Fraud";
   @BooleanProperty(
      description = "<p>If enabled, Futurae users that have the bypass mode enabled will be allowed to authenticate /\nperform approval with IAM. Otherwise, any authentication or approval attempts for users with the bypass\nmode enabled will result in a failure.</p>\n<p><i>Warning:</i> Enabling bypass mode effectively disables all Airlock 2FA second factor checks.\nBypass mode should <i>not</i> be used in production environments.</p>",
      optional = true,
      defaultValue = "false"
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private boolean allowFuturaeBypassMode;
   @StringProperty(
      description = "The symmetric key to encrypt the authentication/transaction payloads. If left empty, the payloads are not encrypted.\n<p>\nThe encryption of payloads in requests to the Futurae API prevents that intermediate infrastructure such as\na reverse proxy is able to read or alter the confidential data therein.\nThe encryption key can be obtained from the Futurae Admin Dashboard.\n</p>",
      optional = true,
      isSensitive = true,
      displayName = "Payload Encryption Key"
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private String payloadEncryptionKey;
   public static final String PAYLOAD_ENCRYPTION_KEY_DISPLAY_NAME = "Payload Encryption Key";
   private static final String PAYLOAD_ENCRYPTION_KEY_FIELD_NAME = "payloadEncryptionKey";
   @BooleanProperty(
      description = "When a 'Payload Encryption Key' is configured, 'Unencrypted Payload for Hardware Tokens' should be enabled. This setting only applies when a hardware token is used for authentication or approval via Offline QR Code. <ul><li>If the option is enabled, the payload is transmitted in plain text (unencrypted).</li><li>If the option is disabled, hardware tokens will not be available for authentication or approval via Offline QR Code, since Futurae does not support payload encryption for hardware tokens.</li></ul>For security reasons, this exception must be explicitly configured.<p><b>Note:</b> Enabling this property without configuring a 'Payload Encryption Key' has no effect, as the payload is transmitted unencrypted by default.</p>",
      optional = true,
      defaultValue = "false",
      displayName = "Unencrypted Payload for Hardware Tokens"
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private boolean unencryptedPayloadForHWTokens;
   public static final String UNENCRYPTED_PAYLOAD_FOR_HARDWARE_TOKENS_DISPLAY_NAME = "Unencrypted Payload for Hardware Tokens";
   private static final String UNENCRYPTED_PAYLOAD_FOR_HARDWARE_TOKENS_FIELD_NAME = "unencryptedPayloadForHWTokens";
   @StringProperty(
      description = "<p>If configured, a cooldown period is enabled during which a newly registered device cannot be used for certain operations.</p><p>By default, all Airlock 2FA steps are configured so that devices may not be used during the \"Cooldown Period\". Exceptions can be configured directly on the step by de-activating the \"Respect Cooldown Period\" property.</p><p>The duration must be specified in the format \"2d 4h 10m 5s\" (any part can be omitted).</p>",
      exampleValues = {"10m", "12h", "2d"},
      optional = true,
      displayName = "Cooldown Period"
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private String cooldownPeriod;
   private static final String COOLDOWN_PERIOD_FIELD = "cooldownPeriod";
   public static final String COOLDOWN_PERIOD_DISPLAY_NAME = "Cooldown Period";
   public static final String RESPECT_COOLDOWN_DISPLAY_NAME = "Respect Cooldown Period";

   public Optional<String> getPayloadEncryptionKey() {
      return Optional.ofNullable(this.payloadEncryptionKey);
   }

   public boolean isUnencryptedPayloadForHWTokens() {
      return this.unencryptedPayloadForHWTokens;
   }

   public static void validateFactors(PluginConfiguration config, String factorsProperty, ConfigurationEnvironment env, ConfigurationContext context) throws ConfigurationException {
      List<String> factors = StringListPropertyValueReader.effectiveStringListConfigValue(config, factorsProperty, env, context);

      try {
         new Airlock2FAFactorPriority(factors.stream().map(Airlock2FAFactor::ofDisplayName).toList());
      } catch (FailureException e) {
         throw new PropertyConstraintViolationException(factorsProperty, config, env, context, e.getMessage());
      }
   }

   @ConfigurationValidator(localOnly = true)
   public static void validateEncryptionKey(PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context, Environment valenv) throws ConfigurationException {
      String key = StringPropertyValueReader.effectiveStringConfigValue(config, "payloadEncryptionKey", env, context);
      if (key != null) {
         try {
            new ExtraInfoEncryptionImpl(key).encrypt(List.of(ExtraInfo.extraInfo("key", "value").orElseThrow()));
         } catch (Exception e) {
            throw new PropertyConstraintViolationException(
               "payloadEncryptionKey",
               config,
               env,
               context,
               "Invalid key. Please provide the Base64 encoded 256 Bit key obtained from the Futurae Admin Dashboard."
            );
         }
      }
   }

   @ConfigurationValidator(localOnly = true)
   public static void validateCooldownDuration(
      PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context, Environment validationEnvironment
   ) throws ConfigurationException {
      DurationValidator.validateDurationFieldIfSet(config, "cooldownPeriod", env, context);
   }

   @ConfigurationValidator(localOnly = true)
   public static void validatePayloadEncryption(
      PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context, Environment validationEnvironment
   ) throws ConfigurationException {
      boolean unencryptedPayloadForHWTokens = BooleanPropertyValueReader.effectiveBooleanPropertyValue(config, "unencryptedPayloadForHWTokens", env, context);
      String key = StringPropertyValueReader.effectiveStringConfigValue(config, "payloadEncryptionKey", env, context);
      if (unencryptedPayloadForHWTokens && key == null) {
         throw new PropertyConfigurationException(
            config,
            env,
            context,
            "Property '%s' must be defined if '%s' is set.".formatted("Payload Encryption Key", "Unencrypted Payload for Hardware Tokens"),
            new String[]{"payloadEncryptionKey", "unencryptedPayloadForHWTokens"}
         );
      }
   }

   public Optional<Duration> getCooldownDuration() {
      return this.cooldownPeriod == null ? Optional.empty() : Optional.of(new DurationParser(this.cooldownPeriod).getDuration());
   }

   public Duration flowBindingTokenValidityDuration() {
      return Duration.ofSeconds(this.trustedSessionBindingValidity);
   }

   public Airlock2FARepositoryConfig getRepository() {
      return this.repository;
   }

   public FuturaeServerConfig getFuturaeServer() {
      return this.futuraeServer;
   }

   public Airlock2FAUserAccountDisplayNameProviderConfig getAccountDisplayNameProvider() {
      return this.accountDisplayNameProvider;
   }

   public Airlock2FAFlowBindingMode getTrustedSessionBindingForActivation() {
      return this.trustedSessionBindingForActivation;
   }

   public boolean isTrustedSessionBindingForRecovery() {
      return this.trustedSessionBindingForRecovery;
   }

   public boolean isLockUserOnFraud() {
      return this.lockUserOnFraud;
   }

   public boolean isAllowFuturaeBypassMode() {
      return this.allowFuturaeBypassMode;
   }

   public Airlock2FAPayloadEncryptionSettings getPayloadEncryptionSettings() {
      return new Airlock2FAPayloadEncryptionSettings(this.getPayloadEncryptionKey().isPresent(), this.unencryptedPayloadForHWTokens);
   }

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

   @SuppressFBWarnings(justification = "generated code")
   public Airlock2FASettings(
      Airlock2FARepositoryConfig repository,
      FuturaeServerConfig futuraeServer,
      Airlock2FAUserAccountDisplayNameProviderConfig accountDisplayNameProvider,
      Airlock2FAFlowBindingMode trustedSessionBindingForActivation,
      boolean trustedSessionBindingForRecovery,
      int trustedSessionBindingValidity,
      boolean lockUserOnFraud,
      boolean allowFuturaeBypassMode,
      String payloadEncryptionKey,
      boolean unencryptedPayloadForHWTokens,
      String cooldownPeriod
   ) {
      this.repository = repository;
      this.futuraeServer = futuraeServer;
      this.accountDisplayNameProvider = accountDisplayNameProvider;
      this.trustedSessionBindingForActivation = trustedSessionBindingForActivation;
      this.trustedSessionBindingForRecovery = trustedSessionBindingForRecovery;
      this.trustedSessionBindingValidity = trustedSessionBindingValidity;
      this.lockUserOnFraud = lockUserOnFraud;
      this.allowFuturaeBypassMode = allowFuturaeBypassMode;
      this.payloadEncryptionKey = payloadEncryptionKey;
      this.unencryptedPayloadForHWTokens = unencryptedPayloadForHWTokens;
      this.cooldownPeriod = cooldownPeriod;
   }

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

   @SuppressFBWarnings(justification = "generated code")
   public static class Airlock2FASettingsBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private Airlock2FARepositoryConfig repository;
      @SuppressFBWarnings(justification = "generated code")
      private FuturaeServerConfig futuraeServer;
      @SuppressFBWarnings(justification = "generated code")
      private Airlock2FAUserAccountDisplayNameProviderConfig accountDisplayNameProvider;
      @SuppressFBWarnings(justification = "generated code")
      private Airlock2FAFlowBindingMode trustedSessionBindingForActivation;
      @SuppressFBWarnings(justification = "generated code")
      private boolean trustedSessionBindingForRecovery;
      @SuppressFBWarnings(justification = "generated code")
      private int trustedSessionBindingValidity;
      @SuppressFBWarnings(justification = "generated code")
      private boolean lockUserOnFraud;
      @SuppressFBWarnings(justification = "generated code")
      private boolean allowFuturaeBypassMode;
      @SuppressFBWarnings(justification = "generated code")
      private String payloadEncryptionKey;
      @SuppressFBWarnings(justification = "generated code")
      private boolean unencryptedPayloadForHWTokens;
      @SuppressFBWarnings(justification = "generated code")
      private String cooldownPeriod;

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

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FASettings.Airlock2FASettingsBuilder repository(Airlock2FARepositoryConfig repository) {
         this.repository = repository;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FASettings.Airlock2FASettingsBuilder futuraeServer(FuturaeServerConfig futuraeServer) {
         this.futuraeServer = futuraeServer;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FASettings.Airlock2FASettingsBuilder accountDisplayNameProvider(Airlock2FAUserAccountDisplayNameProviderConfig accountDisplayNameProvider) {
         this.accountDisplayNameProvider = accountDisplayNameProvider;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FASettings.Airlock2FASettingsBuilder trustedSessionBindingForActivation(Airlock2FAFlowBindingMode trustedSessionBindingForActivation) {
         this.trustedSessionBindingForActivation = trustedSessionBindingForActivation;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FASettings.Airlock2FASettingsBuilder trustedSessionBindingForRecovery(boolean trustedSessionBindingForRecovery) {
         this.trustedSessionBindingForRecovery = trustedSessionBindingForRecovery;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FASettings.Airlock2FASettingsBuilder trustedSessionBindingValidity(int trustedSessionBindingValidity) {
         this.trustedSessionBindingValidity = trustedSessionBindingValidity;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FASettings.Airlock2FASettingsBuilder lockUserOnFraud(boolean lockUserOnFraud) {
         this.lockUserOnFraud = lockUserOnFraud;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FASettings.Airlock2FASettingsBuilder allowFuturaeBypassMode(boolean allowFuturaeBypassMode) {
         this.allowFuturaeBypassMode = allowFuturaeBypassMode;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FASettings.Airlock2FASettingsBuilder payloadEncryptionKey(String payloadEncryptionKey) {
         this.payloadEncryptionKey = payloadEncryptionKey;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FASettings.Airlock2FASettingsBuilder unencryptedPayloadForHWTokens(boolean unencryptedPayloadForHWTokens) {
         this.unencryptedPayloadForHWTokens = unencryptedPayloadForHWTokens;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FASettings.Airlock2FASettingsBuilder cooldownPeriod(String cooldownPeriod) {
         this.cooldownPeriod = cooldownPeriod;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FASettings build() {
         return new Airlock2FASettings(
            this.repository,
            this.futuraeServer,
            this.accountDisplayNameProvider,
            this.trustedSessionBindingForActivation,
            this.trustedSessionBindingForRecovery,
            this.trustedSessionBindingValidity,
            this.lockUserOnFraud,
            this.allowFuturaeBypassMode,
            this.payloadEncryptionKey,
            this.unencryptedPayloadForHWTokens,
            this.cooldownPeriod
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "Airlock2FASettings.Airlock2FASettingsBuilder(repository="
            + this.repository
            + ", futuraeServer="
            + this.futuraeServer
            + ", accountDisplayNameProvider="
            + this.accountDisplayNameProvider
            + ", trustedSessionBindingForActivation="
            + this.trustedSessionBindingForActivation
            + ", trustedSessionBindingForRecovery="
            + this.trustedSessionBindingForRecovery
            + ", trustedSessionBindingValidity="
            + this.trustedSessionBindingValidity
            + ", lockUserOnFraud="
            + this.lockUserOnFraud
            + ", allowFuturaeBypassMode="
            + this.allowFuturaeBypassMode
            + ", payloadEncryptionKey="
            + this.payloadEncryptionKey
            + ", unencryptedPayloadForHWTokens="
            + this.unencryptedPayloadForHWTokens
            + ", cooldownPeriod="
            + this.cooldownPeriod
            + ")";
      }
   }
}
