package com.airlock.iam.common.application.configuration.secretquestion;

import com.airlock.iam.common.domain.model.token.secretquestions.SecretQuestionsNormalization;
import com.airlock.iam.core.misc.persistency.token.TokenDataProvider;
import com.airlock.iam.core.misc.util.password.hash.PasswordHash;
import com.airlock.iam.core.misc.util.password.hash.SHA256Base64PasswordHash;
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.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.RegexProperty;
import com.airlock.iam.plugin.api.annotation.property.StringListProperty;
import com.airlock.iam.plugin.api.config.ConfigurationContext;
import com.airlock.iam.plugin.api.config.ConfigurationEnvironment;
import com.airlock.iam.plugin.api.config.Environment;
import com.airlock.iam.plugin.api.config.PluginConfiguration;
import com.airlock.iam.plugin.api.exception.ConfigurationException;
import com.airlock.iam.plugin.api.exception.PropertyConstraintViolationException;
import com.airlock.iam.plugin.api.valuereader.IntegerPropertyValueReader;
import com.airlock.iam.plugin.api.valuereader.StringListPropertyValueReader;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.regex.Pattern;

@Plugin(
   description = "Configures common settings for secret questions. It is recommended to use the same settings for provisioning, administration and password reset."
)
@Groups(groupSequence = {"Basic Settings", "Advanced Settings"}, openGroups = "Basic Settings")
public class BasicSecretQuestionSettings implements GeneralSecretQuestionsSettings {
   @StringListProperty(
      description = "List of resource keys of the available questions. Each key represents one question. Removing a question (resource key) from this list, causes all answers to that question to become invalid. <p>Ensure that no new question with the same key is introduced later. Any user's answer to the previous question would not match the new question.</p>",
      optional = false,
      displayName = "Question Resource Keys",
      minElements = 1,
      exampleElements = {"secretquestion.pet", "secretquestion.mother"}
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private List<String> questionResourceKeys;
   static final String QUESTION_RESOURCE_KEYS_FIELD = "questionResourceKeys";
   static final String QUESTION_RESOURCE_KEYS_DISPLAY_NAME = "Question Resource Keys";
   @IntegerProperty(
      description = "This property defines the number of questions which have to be provisioned and answered. ",
      displayName = "Number of Questions",
      optional = true,
      minValue = 1,
      exampleValues = {2, 3, 4},
      defaultValue = 2
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private Integer numberOfQuestions;
   static final String NUMBER_OF_QUESTIONS_FIELD = "numberOfQuestions";
   static final String NUMBER_OF_QUESTIONS_DISPLAY_NAME = "Number of Questions";
   @EnumProperty(
      description = "Normalization is a string-transformation applied to answers before they are persisted, and before they are verified. Therefore, an answer can be accepted even if it has minor differences to the provisioned answer. Currently, the following options exist: <ul> \t<li><b>OFF</b>:<br /> \t\tNo normalization. Provisioned and challenged answers must match exactly.</li> \t<li><b>TRIM</b>:<br /> \t\tRemoves whitespaces at the beginning and end of the answer string.</li> \t<li><b>TRIM_CASEINSENSITIVE</b>:<br /> \t\tDoes the same as <em>TRIM</em> and additionally converts all characters to lowercase.</li> \t<li><b>TRIM_CASEINSENSITIVE_NOWHITESPACE</b>:<br /> \t\tDoes the same as <em>CASEINSENSITIVE_TRIM</em> and additionally removes all whitespace.</li> \t<li><b>TRIM_CASEINSENSITIVE_NOWHITESPACE_NOSPECIALCHARS</b>:<br /> \t\tDoes the same as <em>CASEINSENSITIVE_TRIM_NOWHITESPACE</em> and additionally removes all      non-word characters (all characters except letters, digits and the underscore).</li> </ul>",
      optional = true,
      defaultValue = "TRIM_CASEINSENSITIVE"
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private SecretQuestionsNormalization normalization;
   @PluginProperty(
      description = "The provider for token data for persisting the secret answers.",
      optional = false,
      globalPluginReference = "globalTokenDataProvider"
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private TokenDataProvider tokenDataProvider;
   @PluginProperty(
      description = "This hash algorithm is used to hash the answers.<p><b>NOTE:</b> Some password hashes, such as SHA 256 Password Hash or Scrypt Password Hash, produce binary output. If one of these is used, make sure the persistence layer supports binary data in the hash field and the corresponding persistence plugins (e.g. Database User Store or Ldap Connector) are configured to treat hash values as binary values.<br/>In case the persistence layer expects a string, encode the password hash by wrapping it with an encoder. To achieve this, use the Password Hash Configuration plugin and specify the hash function (such as Scrypt Password Hash) together with the desired encoder. We recommend using the Base64 Password Hash Encoder.</p>",
      optional = true,
      defaultValue = SHA256Base64PasswordHash.class
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private PasswordHash hashFunctionPlugin;
   @IntegerProperty(description = "Defines the minimum length of an answer.", optional = true, minValue = 1, defaultValue = 2, exampleValues = {2, 5, 10})
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private int minLength;
   @IntegerProperty(description = "Defines the maximum length of an answer.", optional = true, minValue = 1, defaultValue = 100, exampleValues = {35, 100})
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private int maxLength;
   @RegexProperty(description = "Regex pattern to check the given answer (after normalization).", optional = true)
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private Pattern answerRegexPattern;
   @BooleanProperty(description = "Forbid the same answer for more than one question per user.", defaultValue = "true", optional = true)
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private boolean duplicateAnswersForbidden;
   @BooleanProperty(
      description = "<p>If enabled, answers containing special characters stored by IAM earlier than 6.3 are still accepted. This option does not have to be activated if all answers were set using IAM 6.3 or later or if all answers were set via webservices or REST.</p><p>To support legacy answers, those with special characters are additionally checked using their legacy encoding in latin1.</p>",
      optional = true,
      defaultValue = "false"
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private boolean checkUsingLatin1Encoding;

   @Override
   public Optional<Pattern> getAnswerRegexPattern() {
      return Optional.ofNullable(this.answerRegexPattern);
   }

   @Override
   public int getAllowedNumberOfAttempts() {
      return Integer.MAX_VALUE;
   }

   @Override
   public int getRequiredNumberOfProvisionedAnswers() {
      return this.numberOfQuestions;
   }

   @Override
   public boolean areDuplicateAnswersForbidden() {
      return this.duplicateAnswersForbidden;
   }

   @Override
   public List<String> getQuestionResourceKeys() {
      return this.questionResourceKeys;
   }

   @Override
   public SecretQuestionsNormalization getNormalization() {
      return this.normalization;
   }

   @Override
   public TokenDataProvider getTokenDataProvider() {
      return this.tokenDataProvider;
   }

   @Override
   public PasswordHash getHashFunctionPlugin() {
      return this.hashFunctionPlugin;
   }

   @Override
   public int getMinLength() {
      return this.minLength;
   }

   @Override
   public int getMaxLength() {
      return this.maxLength;
   }

   @Override
   public boolean isCheckUsingLatin1Encoding() {
      return this.checkUsingLatin1Encoding;
   }

   @ConfigurationValidator
   static void validateNotMoreAnswersRequiredThanQuestionsDefined(
      PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context, Environment validationEnvironment
   ) throws ConfigurationException {
      int numAvailableQuestions = StringListPropertyValueReader.effectiveStringListConfigValue(config, "questionResourceKeys", env, context).size();
      int numRequiredAnswers = IntegerPropertyValueReader.effectiveIntegerConfigValue(config, "numberOfQuestions", env, context);
      if (numRequiredAnswers > numAvailableQuestions) {
         throw new PropertyConstraintViolationException(
            "numberOfQuestions", config, env, context, "The \"Number of Questions\" must not be greater than the number of \"Question Resource Keys\"."
         );
      }
   }

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

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

   @SuppressFBWarnings(justification = "generated code")
   public BasicSecretQuestionSettings(
      List<String> questionResourceKeys,
      Integer numberOfQuestions,
      SecretQuestionsNormalization normalization,
      TokenDataProvider tokenDataProvider,
      PasswordHash hashFunctionPlugin,
      int minLength,
      int maxLength,
      Pattern answerRegexPattern,
      boolean duplicateAnswersForbidden,
      boolean checkUsingLatin1Encoding
   ) {
      this.questionResourceKeys = questionResourceKeys;
      this.numberOfQuestions = numberOfQuestions;
      this.normalization = normalization;
      this.tokenDataProvider = tokenDataProvider;
      this.hashFunctionPlugin = hashFunctionPlugin;
      this.minLength = minLength;
      this.maxLength = maxLength;
      this.answerRegexPattern = answerRegexPattern;
      this.duplicateAnswersForbidden = duplicateAnswersForbidden;
      this.checkUsingLatin1Encoding = checkUsingLatin1Encoding;
   }

   @SuppressFBWarnings(justification = "generated code")
   public static class BasicSecretQuestionSettingsBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private ArrayList<String> questionResourceKeys;
      @SuppressFBWarnings(justification = "generated code")
      private Integer numberOfQuestions;
      @SuppressFBWarnings(justification = "generated code")
      private SecretQuestionsNormalization normalization;
      @SuppressFBWarnings(justification = "generated code")
      private TokenDataProvider tokenDataProvider;
      @SuppressFBWarnings(justification = "generated code")
      private PasswordHash hashFunctionPlugin;
      @SuppressFBWarnings(justification = "generated code")
      private int minLength;
      @SuppressFBWarnings(justification = "generated code")
      private int maxLength;
      @SuppressFBWarnings(justification = "generated code")
      private Pattern answerRegexPattern;
      @SuppressFBWarnings(justification = "generated code")
      private boolean duplicateAnswersForbidden;
      @SuppressFBWarnings(justification = "generated code")
      private boolean checkUsingLatin1Encoding;

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

      @SuppressFBWarnings(justification = "generated code")
      public BasicSecretQuestionSettings.BasicSecretQuestionSettingsBuilder questionResourceKey(String questionResourceKey) {
         if (this.questionResourceKeys == null) {
            this.questionResourceKeys = new ArrayList<>();
         }

         this.questionResourceKeys.add(questionResourceKey);
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public BasicSecretQuestionSettings.BasicSecretQuestionSettingsBuilder questionResourceKeys(Collection<? extends String> questionResourceKeys) {
         if (questionResourceKeys == null) {
            throw new NullPointerException("questionResourceKeys cannot be null");
         }

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

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

      @SuppressFBWarnings(justification = "generated code")
      public BasicSecretQuestionSettings.BasicSecretQuestionSettingsBuilder clearQuestionResourceKeys() {
         if (this.questionResourceKeys != null) {
            this.questionResourceKeys.clear();
         }

         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public BasicSecretQuestionSettings.BasicSecretQuestionSettingsBuilder numberOfQuestions(Integer numberOfQuestions) {
         this.numberOfQuestions = numberOfQuestions;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public BasicSecretQuestionSettings.BasicSecretQuestionSettingsBuilder normalization(SecretQuestionsNormalization normalization) {
         this.normalization = normalization;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public BasicSecretQuestionSettings.BasicSecretQuestionSettingsBuilder tokenDataProvider(TokenDataProvider tokenDataProvider) {
         this.tokenDataProvider = tokenDataProvider;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public BasicSecretQuestionSettings.BasicSecretQuestionSettingsBuilder hashFunctionPlugin(PasswordHash hashFunctionPlugin) {
         this.hashFunctionPlugin = hashFunctionPlugin;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public BasicSecretQuestionSettings.BasicSecretQuestionSettingsBuilder minLength(int minLength) {
         this.minLength = minLength;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public BasicSecretQuestionSettings.BasicSecretQuestionSettingsBuilder maxLength(int maxLength) {
         this.maxLength = maxLength;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public BasicSecretQuestionSettings.BasicSecretQuestionSettingsBuilder answerRegexPattern(Pattern answerRegexPattern) {
         this.answerRegexPattern = answerRegexPattern;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public BasicSecretQuestionSettings.BasicSecretQuestionSettingsBuilder duplicateAnswersForbidden(boolean duplicateAnswersForbidden) {
         this.duplicateAnswersForbidden = duplicateAnswersForbidden;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public BasicSecretQuestionSettings.BasicSecretQuestionSettingsBuilder checkUsingLatin1Encoding(boolean checkUsingLatin1Encoding) {
         this.checkUsingLatin1Encoding = checkUsingLatin1Encoding;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public BasicSecretQuestionSettings build() {
         return new BasicSecretQuestionSettings(
            switch (this.questionResourceKeys == null ? 0 : this.questionResourceKeys.size()) {
               case 0 -> Collections.emptyList();
               case 1 -> Collections.singletonList(this.questionResourceKeys.get(0));
               default -> Collections.unmodifiableList(new ArrayList<>(this.questionResourceKeys));
            },
            this.numberOfQuestions,
            this.normalization,
            this.tokenDataProvider,
            this.hashFunctionPlugin,
            this.minLength,
            this.maxLength,
            this.answerRegexPattern,
            this.duplicateAnswersForbidden,
            this.checkUsingLatin1Encoding
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "BasicSecretQuestionSettings.BasicSecretQuestionSettingsBuilder(questionResourceKeys="
            + this.questionResourceKeys
            + ", numberOfQuestions="
            + this.numberOfQuestions
            + ", normalization="
            + this.normalization
            + ", tokenDataProvider="
            + this.tokenDataProvider
            + ", hashFunctionPlugin="
            + this.hashFunctionPlugin
            + ", minLength="
            + this.minLength
            + ", maxLength="
            + this.maxLength
            + ", answerRegexPattern="
            + this.answerRegexPattern
            + ", duplicateAnswersForbidden="
            + this.duplicateAnswersForbidden
            + ", checkUsingLatin1Encoding="
            + this.checkUsingLatin1Encoding
            + ")";
      }
   }
}
