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

import com.airlock.iam.common.api.domain.model.store.user.UserStore;
import com.airlock.iam.common.api.domain.model.store.user.exception.UserEntityNotUniqueException;
import com.airlock.iam.core.domain.model.user.UserIdentityGenerator;
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.StringGeneratorRuntimeException;
import com.airlock.iam.plugin.api.annotation.config.Plugin;
import com.airlock.iam.plugin.api.annotation.lifecycle.AfterConfigActivation;
import com.airlock.iam.plugin.api.annotation.property.PluginMapProperty;
import com.airlock.iam.plugin.api.annotation.property.StringProperty;
import com.airlock.iam.plugin.api.exception.InitializeException;
import com.airlock.iam.plugin.framework.application.service.builder.plugin.PluginPropertyBuilderInitializer;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.Map;

@Plugin(description = "Generate a random identity from a customizable pattern of fixed and random components.")
public class CustomizableIdentityGenerator implements UserIdentityGenerator {
   @StringProperty(
      description = "Pattern defining how the string is generated.\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",
      optional = true,
      exampleValues = {"{distinct:5}", "user{digits:8}", "{lower26:1}{digits:3}{distinct:3}"},
      defaultValue = "user{digits:8}"
   )
   private String pattern;
   @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"
   )
   private Map<String, Alphabet> alphabets;
   @SuppressFBWarnings("UWF_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR")
   private SecretStringGenerator generator;
   private static final int MAX_TRIES = 1000;

   public CustomizableIdentityGenerator(String pattern, Map<String, Alphabet> alphabets) {
      this.pattern = pattern;
      this.alphabets = alphabets;
   }

   @AfterConfigActivation
   void afterConfigActivation() throws InitializeException {
      try {
         this.generator = ((ExtendedStringGenerator.ExtendedStringGeneratorBuilder)PluginPropertyBuilderInitializer.builder(
               ExtendedStringGenerator.ExtendedStringGeneratorBuilder.class
            ))
            .pattern(this.pattern)
            .alphabets(this.alphabets)
            .build();
      } catch (StringGeneratorRuntimeException e) {
         throw new InitializeException(e.getMessage());
      }
   }

   @Override
   public String generateIdentity(UserStore userStore, Map<String, Object> attributes) {
      for (int counter = 0; counter < 1000; counter++) {
         String candidate = this.generator.createString(null);
         if (userStore.findByIdentity(candidate).isEmpty()) {
            return candidate;
         }
      }

      throw UserEntityNotUniqueException.userEntityNotUniqueExceptionWithMessage("could not generate user ID");
   }

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

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

   @SuppressFBWarnings(justification = "generated code")
   public void setPattern(String pattern) {
      this.pattern = pattern;
   }

   @SuppressFBWarnings(justification = "generated code")
   public static class CustomizableIdentityGeneratorBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private String pattern;
      @SuppressFBWarnings(justification = "generated code")
      private Map<String, Alphabet> alphabets;

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

      @SuppressFBWarnings(justification = "generated code")
      public CustomizableIdentityGenerator.CustomizableIdentityGeneratorBuilder pattern(String pattern) {
         this.pattern = pattern;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public CustomizableIdentityGenerator.CustomizableIdentityGeneratorBuilder alphabets(Map<String, Alphabet> alphabets) {
         this.alphabets = alphabets;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public CustomizableIdentityGenerator build() {
         return new CustomizableIdentityGenerator(this.pattern, this.alphabets);
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "CustomizableIdentityGenerator.CustomizableIdentityGeneratorBuilder(pattern=" + this.pattern + ", alphabets=" + this.alphabets + ")";
      }
   }
}
