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

import com.airlock.iam.common.api.domain.model.authmethod.AuthenticationMethod;
import com.airlock.iam.common.api.domain.service.email.EmailService;
import com.airlock.iam.core.application.configuration.contextdata.StringContextDataItemNameConfig;
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.types.api.application.configuration.step.StepConfigForPublicSelfServiceFlows;
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.publicselfservice.domain.model.step.EmailIdentityVerificationStep;
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 email 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 an email address.</li>  <li>It implements stealth mode: if a user does not exist or cannot do a public self-service 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 = "Email Identity Verification Step"
)
@Groups(openGroups = "Basic Settings", groupSequence = {"Basic Settings", "Tags/Guards", "Flow Control", "Advanced Settings"})
@NextActions(EmailIdentityVerificationStep.EmailOtpStepAction.class)
public final class EmailIdentityVerificationStepConfig extends AbstractInteractiveFlowStepConfig implements StepConfigForPublicSelfServiceFlows {
   public static final String USERNAME_KEY = "USERNAME";
   public static final String TOKEN_KEY = "TOKEN";
   public static final String CURRENT_DATE_KEY = "Now";
   @PluginProperty(description = "The context data field that contains the email address of the user.", displayName = "Email Item", optional = false)
   @GroupProperty(groupIdentifier = "Basic Settings")
   private StringContextDataItemNameConfig emailContextData;
   @PluginProperty(description = "Email service for sending emails.", displayName = "Email Service", optional = false)
   @GroupProperty(groupIdentifier = "Basic Settings")
   private EmailService emailService;
   @PluginProperty(
      description = "Secret string generator to create the OTP. Make sure that the code is long enough to prevent brute-force attacks (by restarting the flow multiple times).",
      displayName = "OTP Generator",
      defaultValue = ExtendedStringGenerator.class,
      optional = true
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private SecretStringGenerator otpGenerator;
   @StringProperty(
      description = "The resource key for the email subject. The replacement variables described for \"Body Resource Key\" are also available here.",
      optional = true,
      defaultValue = "public-self-service.email.otp.subject"
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private String subjectResourceKey;
   @StringProperty(
      description = "<p>The resource key for the email message body.</p><p>The following syntax can be used to include data in the template:<ul><li><tt>${TOKEN}</tt> to include the generated OTP.</li><li><tt>${USERNAME}</tt> to include the name of the user (as entered to initiate the public self-service).</li><li><tt>${Now,date,format}</tt> to include the current date/time, where \"format\" is a date pattern like \"yyyy-MM-dd HH:mm:ss\".</li><li><tt>${contextDataName}</tt> to include the value of the context data field \"contextDataName\". Note that only context data of type \"string\" can be included.</li></ul></p><p>Note that unreplaced variables result in a failure and no email is sent. Therefore, only variable names should be used that are guaranteed to be available when the email verification is performed.</p>",
      optional = true,
      defaultValue = "public-self-service.email.otp.body"
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private String bodyResourceKey;
   @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 = 10,
      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.",
      optional = true,
      defaultValue = "true"
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private boolean otpCaseSensitive;
   @BooleanProperty(
      description = "<p>If enabled, the verification email will be sent as an HTML mail. Otherwise it will be sent as plain text.</p><p><strong>Security Warning</strong>: If e-mails are sent as HTML, make sure to properly escape values originating from untrusted sources (such as user input during self-registration). This can be achieved by enabling the property 'Escape Values in HTML'. ",
      displayName = "Send As HTML",
      optional = true,
      defaultValue = "false"
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private boolean sendAsHtml;
   private static final String SEND_AS_HTML_PROPERTY_NAME = "Send As HTML";
   @BooleanProperty(
      description = "<p>HTML-escape all provided values if property Send As HTML is enabled.</p><p><strong>Security Warning</strong>: If e-mails are sent as HTML, make sure to properly escape values originating from untrusted sources (such as user input during self-registration). This can be achieved by enabling the property 'Escape Values in HTML'. ",
      displayName = "Escape Values in HTML",
      defaultValue = "true",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private boolean escapeHtmlValues;
   @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 = "EMAIL",
      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 StringContextDataItemNameConfig getEmailContextData() {
      return this.emailContextData;
   }

   public EmailService getEmailService() {
      return this.emailService;
   }

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

   public String getSubjectResourceKey() {
      return this.subjectResourceKey;
   }

   public String getBodyResourceKey() {
      return this.bodyResourceKey;
   }

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

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

   public boolean isSendAsHtml() {
      return this.sendAsHtml;
   }

   public boolean isEscapeHtmlValues() {
      return this.escapeHtmlValues;
   }

   private EmailIdentityVerificationStepConfig(
      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,
      StringContextDataItemNameConfig emailContextData,
      EmailService emailService,
      SecretStringGenerator otpGenerator,
      String subjectResourceKey,
      String bodyResourceKey,
      int maxFailedAttempts,
      int otpValidity,
      boolean otpCaseSensitive,
      boolean sendAsHtml,
      boolean escapeHtmlValues,
      String authenticationMethodId
   ) {
      super(
         skipCondition,
         preCondition,
         requiresActivation,
         tagsOnSuccess,
         stepId,
         interactiveGotoTargets,
         dynamicStepActivations,
         onFailureGotos,
         customResponseAttributes,
         customFailureResponseAttributes
      );
      this.emailContextData = emailContextData;
      this.emailService = emailService;
      this.otpGenerator = otpGenerator;
      this.subjectResourceKey = subjectResourceKey;
      this.bodyResourceKey = bodyResourceKey;
      this.maxFailedAttempts = maxFailedAttempts;
      this.otpValidity = otpValidity;
      this.otpCaseSensitive = otpCaseSensitive;
      this.sendAsHtml = sendAsHtml;
      this.escapeHtmlValues = escapeHtmlValues;
   }

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

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

   @SuppressFBWarnings(justification = "generated code")
   public static class EmailIdentityVerificationStepConfigBuilder {
      @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 StringContextDataItemNameConfig emailContextData;
      @SuppressFBWarnings(justification = "generated code")
      private EmailService emailService;
      @SuppressFBWarnings(justification = "generated code")
      private SecretStringGenerator otpGenerator;
      @SuppressFBWarnings(justification = "generated code")
      private String subjectResourceKey;
      @SuppressFBWarnings(justification = "generated code")
      private String bodyResourceKey;
      @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 boolean sendAsHtml;
      @SuppressFBWarnings(justification = "generated code")
      private boolean escapeHtmlValues;
      @SuppressFBWarnings(justification = "generated code")
      private String authenticationMethodId;

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

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

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

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

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

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

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

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

      @SuppressFBWarnings(justification = "generated code")
      public EmailIdentityVerificationStepConfig.EmailIdentityVerificationStepConfigBuilder 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 EmailIdentityVerificationStepConfig.EmailIdentityVerificationStepConfigBuilder clearInteractiveGotoTargets() {
         if (this.interactiveGotoTargets != null) {
            this.interactiveGotoTargets.clear();
         }

         return this;
      }

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

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

      @SuppressFBWarnings(justification = "generated code")
      public EmailIdentityVerificationStepConfig.EmailIdentityVerificationStepConfigBuilder 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 EmailIdentityVerificationStepConfig.EmailIdentityVerificationStepConfigBuilder clearDynamicStepActivations() {
         if (this.dynamicStepActivations != null) {
            this.dynamicStepActivations.clear();
         }

         return this;
      }

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

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

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

      @SuppressFBWarnings(justification = "generated code")
      public EmailIdentityVerificationStepConfig.EmailIdentityVerificationStepConfigBuilder emailContextData(StringContextDataItemNameConfig emailContextData) {
         this.emailContextData = emailContextData;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public EmailIdentityVerificationStepConfig.EmailIdentityVerificationStepConfigBuilder emailService(EmailService emailService) {
         this.emailService = emailService;
         return this;
      }

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

      @SuppressFBWarnings(justification = "generated code")
      public EmailIdentityVerificationStepConfig.EmailIdentityVerificationStepConfigBuilder subjectResourceKey(String subjectResourceKey) {
         this.subjectResourceKey = subjectResourceKey;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public EmailIdentityVerificationStepConfig.EmailIdentityVerificationStepConfigBuilder bodyResourceKey(String bodyResourceKey) {
         this.bodyResourceKey = bodyResourceKey;
         return this;
      }

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

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

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

      @SuppressFBWarnings(justification = "generated code")
      public EmailIdentityVerificationStepConfig.EmailIdentityVerificationStepConfigBuilder sendAsHtml(boolean sendAsHtml) {
         this.sendAsHtml = sendAsHtml;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public EmailIdentityVerificationStepConfig.EmailIdentityVerificationStepConfigBuilder escapeHtmlValues(boolean escapeHtmlValues) {
         this.escapeHtmlValues = escapeHtmlValues;
         return this;
      }

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

      @SuppressFBWarnings(justification = "generated code")
      public EmailIdentityVerificationStepConfig 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 EmailIdentityVerificationStepConfig(
            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.emailContextData,
            this.emailService,
            this.otpGenerator,
            this.subjectResourceKey,
            this.bodyResourceKey,
            this.maxFailedAttempts,
            this.otpValidity,
            this.otpCaseSensitive,
            this.sendAsHtml,
            this.escapeHtmlValues,
            this.authenticationMethodId
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "EmailIdentityVerificationStepConfig.EmailIdentityVerificationStepConfigBuilder(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
            + ", emailContextData="
            + this.emailContextData
            + ", emailService="
            + this.emailService
            + ", otpGenerator="
            + this.otpGenerator
            + ", subjectResourceKey="
            + this.subjectResourceKey
            + ", bodyResourceKey="
            + this.bodyResourceKey
            + ", maxFailedAttempts="
            + this.maxFailedAttempts
            + ", otpValidity="
            + this.otpValidity
            + ", otpCaseSensitive="
            + this.otpCaseSensitive
            + ", sendAsHtml="
            + this.sendAsHtml
            + ", escapeHtmlValues="
            + this.escapeHtmlValues
            + ", authenticationMethodId="
            + this.authenticationMethodId
            + ")";
      }
   }
}
