package com.airlock.iam.flow.shared.application.configuration.step;

import com.airlock.iam.flow.api.application.configuration.selection.FlowConditionConfig;
import com.airlock.iam.flow.api.application.configuration.step.AbstractFlowStepConfig;
import com.airlock.iam.flow.api.application.configuration.step.NonInteractiveStepConfig;
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.StepConfigForAuthenticationFlows;
import com.airlock.iam.flow.types.api.application.configuration.step.StepConfigForAuthorizationFlows;
import com.airlock.iam.flow.types.api.application.configuration.step.StepConfigForPublicSelfServiceFlows;
import com.airlock.iam.flow.types.api.application.configuration.step.StepConfigForSelfServiceFlows;
import com.airlock.iam.flow.types.api.application.configuration.step.StepConfigForTechClientRegFlows;
import com.airlock.iam.flow.types.api.application.configuration.step.StepConfigForTransactionApprovalFlows;
import com.airlock.iam.flow.types.api.application.configuration.step.StepConfigForUserSelfRegFlows;
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.GroupProperty;
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.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.scriptable.application.configuration.ScriptExpectedOutputTypeConfig;
import com.airlock.iam.scriptable.application.configuration.ScriptNamespaceConfig;
import com.airlock.iam.scriptable.application.configuration.ScriptSecretConfig;
import com.airlock.iam.scriptable.application.configuration.ScriptSecretUniquenessConfigValidator;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;

@Plugin(
   description = "A non-interactive flow step that runs the configured script. The script will be provided with inputs from the configured value providers. The result of the script is validated against the configured output map and is stored in the session. It can be retrieved using the \"Script Execution Result Value Map Provider\" plugin in subsequent steps. <p>IAM uses Lua as its scripting engine. Please refer to the official documentation on <a href=\"https://www.lua.org/\" target=\"_blank\">https://www.lua.org/</a> for more information about the Lua language and its features.</p>",
   displayName = "Scriptable Step"
)
@Groups(openGroups = "Basic Settings", groupSequence = {"Basic Settings", "Advanced Settings", "Tags/Guards", "Flow Control"})
public final class ScriptableStepConfig
   extends AbstractFlowStepConfig
   implements StepConfigForAuthorizationFlows,
   StepConfigForAuthenticationFlows,
   StepConfigForUserSelfRegFlows,
   StepConfigForPublicSelfServiceFlows,
   StepConfigForTechClientRegFlows,
   StepConfigForTransactionApprovalFlows,
   StepConfigForSelfServiceFlows,
   NonInteractiveStepConfig {
   public static final String SCRIPTABLE_STEP_PLUGIN_NAME = "Scriptable Step";
   private static final String SCRIPTABLE_STEP_FUNCTION_NAME = "iam_on_step_init ()";
   @PluginListProperty(
      description = "<p>Defines a mapping of input values that are made available to the lua script. The inputs can be accessed from inside the script as a table through the \"iam\" API object: <code>iam.input_map</code></p> <p>If a value cannot be provided by the configured map, its corresponding key will not exist in the resulting mapping. When writing a script, one should always first check if the key is present before trying to operate on its value.</p> <p>If two value providers contain an entry with the same key, then only the entry of the provider that appears last in the list will be available within the script. </p><p>Note that simple date values without a specific time (i.e. LocalDate) are passed as string arguments in the format <code>\"yyyy-MM-dd\"</code>, while more specific date-time values are provided as epoch time with millisecond precision.</p><p><strong>Security Note: The inputs to the Scriptable Step may contain sensitive user data. Handle these inputs carefully and make sure that they are not forwarded to the logger. IAM will <i>not</i> obfuscate this data automatically.</strong></p>",
      minElements = 1,
      optional = true,
      displayName = "Inputs"
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private List<ValueMapProviderConfig> inputs;
   public static final String INPUTS_DISPLAY_NAME = "Inputs";
   @StringProperty(
      description = "<p>The Lua script that will be run when the step is initialized.</p><p>Each script must define the following function:</p><pre><code>function iam_on_step_init ()\n    return &lt;output_map&gt;\nend</code></pre> <p>The values returned by this function can be accessed using the \"Script Execution Result Value Map Provider\" with matching namespace. Note that the outputs returned by the script must be declared in the \"Outputs\" property. If no outputs are expected, then the function's return statement can either be removed, or return an empty mapping.</p>",
      displayName = "Script",
      isMultiLineText = true,
      optional = false
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private String script;
   @PluginMapProperty(
      description = "<p>Declares the mapping of key-value pairs that are expected to be returned by the Lua script. The outputs are made available under the namespace configured for the step.</p> <p>The script's outputs are required to match the expected output exactly. If the script's output contains additional results, this will also produce an error.</p> <p>Individual key-value pairs may be tagged as optional, in which case they are not required to be in the script's output. Note that the order in which the key-value pairs are listed does not matter.</p> <p>If left undefined, then the output map is expected to be empty.</p>",
      displayName = "Outputs",
      optional = true,
      minElements = 1
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private Map<String, ScriptExpectedOutputTypeConfig> outputs;
   public static final String OUTPUTS_DISPLAY_NAME = "Outputs";
   @PluginListProperty(
      description = "Secret strings that are passed in plaintext as environment variables to the IAM script process.\n<p>The script is always executed on the server and therefore no secrets are transmitted to an external client, e.g. browser.</p>\n<p>An example are HTTP basic authentication credentials for the configuration of a REST client.</p>\n<p>Secrets must only be referenced in the script by calling the API method <code>iam.secrets:get(identifier)</code>\nwith their configured string identifier.</p>\n<p><strong>Security Note: Secrets must be handled with care. Make sure that they are not forwarded to the\nlogger, as IAM will <i>not</i> obfuscate them automatically.</strong></p>",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private List<ScriptSecretConfig> inputSecrets;
   private static final String INPUT_SECRETS_FIELD = "inputSecrets";
   @PluginProperty(
      description = "<p>The namespace under which the output of the step is stored. If left empty, it is stored under the default namespace.</p> <p>To retrieve the output values of this step, configure the same namespace in the corresponding Script Execution Result Value Map Provider.</p>",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private ScriptNamespaceConfig namespace;

   @ConfigurationValidator
   static void validateUniqueSecrets(PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context, Environment validationEnvironment) throws ConfigurationException {
      new ScriptSecretUniquenessConfigValidator(config, context, env).validate("inputSecrets");
   }

   ScriptableStepConfig(
      FlowConditionConfig skipCondition,
      FlowConditionConfig preCondition,
      boolean requiresActivation,
      List<TagConfig> tagsOnSuccess,
      @Nullable StepIdConfig stepId,
      Map<String, StepIdConfig> onFailureGotos,
      List<ValueMapProviderConfig> customResponseAttributes,
      List<ValueMapProviderConfig> customFailureResponseAttributes,
      List<ValueMapProviderConfig> inputs,
      List<ScriptSecretConfig> inputSecrets,
      String script,
      Map<String, ScriptExpectedOutputTypeConfig> outputs,
      ScriptNamespaceConfig namespace
   ) {
      super(skipCondition, preCondition, requiresActivation, tagsOnSuccess, stepId, onFailureGotos, customResponseAttributes, customFailureResponseAttributes);
      this.inputs = inputs;
      this.inputSecrets = inputSecrets;
      this.script = script;
      this.outputs = outputs;
      this.namespace = namespace;
   }

   public List<ValueMapProviderConfig> getInputs() {
      return this.inputs;
   }

   public String getScript() {
      return this.script;
   }

   public Map<String, ScriptExpectedOutputTypeConfig> getOutputs() {
      return this.outputs;
   }

   public List<ScriptSecretConfig> getInputSecrets() {
      return this.inputSecrets;
   }

   @Nullable
   public ScriptNamespaceConfig getNamespace() {
      return this.namespace;
   }

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

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

   @SuppressFBWarnings(justification = "generated code")
   public static class ScriptableStepConfigBuilder {
      @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 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 ArrayList<ValueMapProviderConfig> inputs;
      @SuppressFBWarnings(justification = "generated code")
      private ArrayList<ScriptSecretConfig> inputSecrets;
      @SuppressFBWarnings(justification = "generated code")
      private String script;
      @SuppressFBWarnings(justification = "generated code")
      private Map<String, ScriptExpectedOutputTypeConfig> outputs;
      @SuppressFBWarnings(justification = "generated code")
      private ScriptNamespaceConfig namespace;

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

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

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

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

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

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

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

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

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

      @SuppressFBWarnings(justification = "generated code")
      public ScriptableStepConfig.ScriptableStepConfigBuilder input(ValueMapProviderConfig input) {
         if (this.inputs == null) {
            this.inputs = new ArrayList<>();
         }

         this.inputs.add(input);
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public ScriptableStepConfig.ScriptableStepConfigBuilder inputs(Collection<? extends ValueMapProviderConfig> inputs) {
         if (inputs == null) {
            throw new NullPointerException("inputs cannot be null");
         }

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

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

      @SuppressFBWarnings(justification = "generated code")
      public ScriptableStepConfig.ScriptableStepConfigBuilder clearInputs() {
         if (this.inputs != null) {
            this.inputs.clear();
         }

         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public ScriptableStepConfig.ScriptableStepConfigBuilder inputSecret(ScriptSecretConfig inputSecret) {
         if (this.inputSecrets == null) {
            this.inputSecrets = new ArrayList<>();
         }

         this.inputSecrets.add(inputSecret);
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public ScriptableStepConfig.ScriptableStepConfigBuilder inputSecrets(Collection<? extends ScriptSecretConfig> inputSecrets) {
         if (inputSecrets == null) {
            throw new NullPointerException("inputSecrets cannot be null");
         }

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

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

      @SuppressFBWarnings(justification = "generated code")
      public ScriptableStepConfig.ScriptableStepConfigBuilder clearInputSecrets() {
         if (this.inputSecrets != null) {
            this.inputSecrets.clear();
         }

         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public ScriptableStepConfig.ScriptableStepConfigBuilder script(String script) {
         this.script = script;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public ScriptableStepConfig.ScriptableStepConfigBuilder outputs(Map<String, ScriptExpectedOutputTypeConfig> outputs) {
         this.outputs = outputs;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public ScriptableStepConfig.ScriptableStepConfigBuilder namespace(ScriptNamespaceConfig namespace) {
         this.namespace = namespace;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public ScriptableStepConfig build() {
         List inputs = switch (this.inputs == null ? 0 : this.inputs.size()) {
            case 0 -> Collections.emptyList();
            case 1 -> Collections.singletonList(this.inputs.get(0));
            default -> Collections.unmodifiableList(new ArrayList<>(this.inputs));
         };

         return new ScriptableStepConfig(
            this.skipCondition,
            this.preCondition,
            this.requiresActivation,
            this.tagsOnSuccess,
            this.stepId,
            this.onFailureGotos,
            this.customResponseAttributes,
            this.customFailureResponseAttributes,
            inputs,
            switch (this.inputSecrets == null ? 0 : this.inputSecrets.size()) {
               case 0 -> Collections.emptyList();
               case 1 -> Collections.singletonList(this.inputSecrets.get(0));
               default -> Collections.unmodifiableList(new ArrayList<>(this.inputSecrets));
            },
            this.script,
            this.outputs,
            this.namespace
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "ScriptableStepConfig.ScriptableStepConfigBuilder(skipCondition="
            + this.skipCondition
            + ", preCondition="
            + this.preCondition
            + ", requiresActivation="
            + this.requiresActivation
            + ", tagsOnSuccess="
            + this.tagsOnSuccess
            + ", stepId="
            + this.stepId
            + ", onFailureGotos="
            + this.onFailureGotos
            + ", customResponseAttributes="
            + this.customResponseAttributes
            + ", customFailureResponseAttributes="
            + this.customFailureResponseAttributes
            + ", inputs="
            + this.inputs
            + ", inputSecrets="
            + this.inputSecrets
            + ", script="
            + this.script
            + ", outputs="
            + this.outputs
            + ", namespace="
            + this.namespace
            + ")";
      }
   }
}
