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

import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.core.misc.authen.UsernameTransformer;
import com.airlock.iam.flow.api.application.configuration.flowprocessor.FlowProcessorConfig;
import com.airlock.iam.flow.api.application.configuration.flowprocessor.FlowProcessorsConfig;
import com.airlock.iam.flow.api.application.configuration.sequence.FlowStepSequence;
import com.airlock.iam.flow.api.domain.model.flow.FlowId;
import com.airlock.iam.flow.application.configuration.FlowConfig;
import com.airlock.iam.flow.application.configuration.flow.FlowIdConfig;
import com.airlock.iam.flow.application.configuration.validation.FlowStepIdConfigValidators;
import com.airlock.iam.flow.application.configuration.validation.FlowStepPluginConfiguration;
import com.airlock.iam.flow.shared.application.configuration.FlowUserNameTransformerConfig;
import com.airlock.iam.flow.types.api.application.configuration.step.StepConfigForPublicSelfServiceFlows;
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.GroupProperty;
import com.airlock.iam.plugin.api.annotation.property.PluginListProperty;
import com.airlock.iam.plugin.api.annotation.property.PluginProperty;
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.framework.application.service.builder.plugin.PluginPropertyBuilderInitializer;
import com.airlock.iam.publicselfservice.api.application.configuration.PublicSelfServiceRestrictionConfig;
import com.airlock.iam.publicselfservice.application.configuration.processors.DefaultPublicSelfServiceProcessorsConfig;
import com.airlock.iam.publicselfservice.application.configuration.restrictions.PublicSelfServiceRestrictionsConfig;
import com.airlock.iam.publicselfservice.application.configuration.steps.FlowContinuationStepConfig;
import com.airlock.iam.publicselfservice.application.configuration.steps.FlowContinuationTokenConsumptionStepConfig;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

@Plugin(description = "Configuration for a public self-service flow.", displayName = "Public Self-Service Flow")
@Groups(groupSequence = {"Basic Settings", "Advanced Settings"}, openGroups = "Basic Settings")
public class PublicSelfServiceFlowConfig implements FlowConfig, FlowUserNameTransformerConfig {
   public static final String PUBLIC_SELF_SERVICE_FLOW_DISPLAY_NAME = "Public Self-Service Flow";
   @PluginProperty(description = "Unique ID for this flow, which is used for selecting or referencing a flow.", displayName = "Flow ID", optional = false)
   @GroupProperty(groupIdentifier = "Basic Settings")
   private FlowIdConfig flowId;
   public static final String FLOW_ID_PROPERTY = "flowId";
   @PluginListProperty(description = "Steps of the flow.", optional = false, minElements = 1)
   @GroupProperty(groupIdentifier = "Basic Settings")
   private List<StepConfigForPublicSelfServiceFlows> steps;
   @PluginProperty(
      description = "Restrictions to define which users are allowed to perform this public self-service. These restrictions are checked for each step of the flow. Typically, it is also possible to configure whether feedback to the user is enabled or not (user enumeration protection).",
      optional = false
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private PublicSelfServiceRestrictionsConfig restrictions;
   @PluginProperty(
      description = "Processors are notified at various stages of the flow and offer hooks for custom logic.",
      defaultValue = DefaultPublicSelfServiceProcessorsConfig.class,
      optional = true
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private FlowProcessorsConfig processors;
   public static final String PROCESSORS_PROPERTY = "processors";
   @PluginListProperty(
      description = "Username transformers may transform the provided username into the single unique user ID required for the flow.<br />The transformation of a username takes place in the first step before the user is loaded. Note that username transformers have no effect on the propagated username value. Transformers can be chained, i.e. a first transformer could normalize the original name, where the next transformer looks up the normalized name in a database for potential transformation matches.<br />In contrary to the above description of chaining, a transformer can also signal that it already found the final user ID and the chain must stop after it.<br/>For further details please refer to the documentation of the username transformer plugins.",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private List<UsernameTransformer> usernameTransformers;
   @BooleanProperty(
      description = "<p>If enabled, the next authentication flow after completing this public self-service flow will be initialized with the user identity and tags from the public self-service flow. By combining this feature with authentication flows where steps can be skipped based on tags from public self-service, a non-interactive authentication after completed public self-service can be achieved.</p><p>Information is propagated only if a user has been identified in the public flow.</p>",
      optional = true,
      defaultValue = "false"
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private boolean initializeNextAuthFlow;

   public FlowIdConfig getId() {
      return this.flowId;
   }

   @Override
   public FlowId getFlowId() {
      return this.getId().getId();
   }

   @Override
   public FlowStepSequence getSteps() {
      return ((FlowStepSequence.FlowStepSequenceBuilder)PluginPropertyBuilderInitializer.pluginBuilder(FlowStepSequence.FlowStepSequenceBuilder.class))
         .steps(this.steps)
         .build();
   }

   @Override
   public List<FlowProcessorConfig> getProcessors() {
      return this.processors.getProcessors();
   }

   public List<PublicSelfServiceRestrictionConfig> getRestrictions() {
      return this.restrictions.getRestrictions();
   }

   @Override
   public List<UsernameTransformer> getUsernameTransformers() {
      return this.usernameTransformers;
   }

   public boolean isInitializeNextAuthFlow() {
      return this.initializeNextAuthFlow;
   }

   @ConfigurationValidator
   static void validateFlowConstraints(
      PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context, Environment validationEnvironment
   ) throws ConfigurationException {
      new FlowStepIdConfigValidators(config, env, context, true, config).validateFlowConstraints();
   }

   @ConfigurationValidator
   static void validateFlowContinuationConstraints(
      PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context, Environment validationEnvironment
   ) throws ConfigurationException {
      List<FlowStepPluginConfiguration> flowSteps = FlowStepIdConfigValidators.findAllFlowSteps(config, env, context);
      if (containsFlowContinuationStepWithDelayedTokenConsumption(flowSteps, env, context) && !containsFlowContinuationTokenConsumptionStep(flowSteps)) {
         throw new PropertyConstraintViolationException(
            "steps",
            config,
            env,
            context,
            String.format(
               "If a %s is defined with %s disabled, a separate %s is required.",
               LogUtil.quote("Flow Continuation Step"),
               LogUtil.quote("Immediate Token Consumption"),
               LogUtil.quote("Flow Continuation Token Consumption Step")
            )
         );
      }
   }

   private static boolean containsFlowContinuationStepWithDelayedTokenConsumption(
      List<FlowStepPluginConfiguration> flowSteps, ConfigurationEnvironment env, ConfigurationContext context
   ) {
      List<FlowStepPluginConfiguration> flowContinuationSteps = flowSteps.stream().filter(step -> step.isOfType(FlowContinuationStepConfig.class)).toList();
      return flowContinuationSteps.stream().anyMatch(step -> delayedTokenConsumptionActivated(step, env, context));
   }

   private static boolean delayedTokenConsumptionActivated(
      FlowStepPluginConfiguration flowContinuationStep, ConfigurationEnvironment env, ConfigurationContext context
   ) {
      return !flowContinuationStep.getEffectiveBooleanProperty("consumesToken", env, context);
   }

   private static boolean containsFlowContinuationTokenConsumptionStep(List<FlowStepPluginConfiguration> flowSteps) {
      return flowSteps.stream().anyMatch(step -> step.isOfType(FlowContinuationTokenConsumptionStepConfig.class));
   }

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

   @SuppressFBWarnings(justification = "generated code")
   public PublicSelfServiceFlowConfig(
      FlowIdConfig flowId,
      List<StepConfigForPublicSelfServiceFlows> steps,
      PublicSelfServiceRestrictionsConfig restrictions,
      FlowProcessorsConfig processors,
      List<UsernameTransformer> usernameTransformers,
      boolean initializeNextAuthFlow
   ) {
      this.flowId = flowId;
      this.steps = steps;
      this.restrictions = restrictions;
      this.processors = processors;
      this.usernameTransformers = usernameTransformers;
      this.initializeNextAuthFlow = initializeNextAuthFlow;
   }

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

   @SuppressFBWarnings(justification = "generated code")
   public static class PublicSelfServiceFlowConfigBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private FlowIdConfig flowId;
      @SuppressFBWarnings(justification = "generated code")
      private ArrayList<StepConfigForPublicSelfServiceFlows> steps;
      @SuppressFBWarnings(justification = "generated code")
      private PublicSelfServiceRestrictionsConfig restrictions;
      @SuppressFBWarnings(justification = "generated code")
      private FlowProcessorsConfig processors;
      @SuppressFBWarnings(justification = "generated code")
      private ArrayList<UsernameTransformer> usernameTransformers;
      @SuppressFBWarnings(justification = "generated code")
      private boolean initializeNextAuthFlow;

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

      @SuppressFBWarnings(justification = "generated code")
      public PublicSelfServiceFlowConfig.PublicSelfServiceFlowConfigBuilder flowId(FlowIdConfig flowId) {
         this.flowId = flowId;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public PublicSelfServiceFlowConfig.PublicSelfServiceFlowConfigBuilder step(StepConfigForPublicSelfServiceFlows step) {
         if (this.steps == null) {
            this.steps = new ArrayList<>();
         }

         this.steps.add(step);
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public PublicSelfServiceFlowConfig.PublicSelfServiceFlowConfigBuilder steps(Collection<? extends StepConfigForPublicSelfServiceFlows> steps) {
         if (steps == null) {
            throw new NullPointerException("steps cannot be null");
         }

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

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

      @SuppressFBWarnings(justification = "generated code")
      public PublicSelfServiceFlowConfig.PublicSelfServiceFlowConfigBuilder clearSteps() {
         if (this.steps != null) {
            this.steps.clear();
         }

         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public PublicSelfServiceFlowConfig.PublicSelfServiceFlowConfigBuilder restrictions(PublicSelfServiceRestrictionsConfig restrictions) {
         this.restrictions = restrictions;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public PublicSelfServiceFlowConfig.PublicSelfServiceFlowConfigBuilder processors(FlowProcessorsConfig processors) {
         this.processors = processors;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public PublicSelfServiceFlowConfig.PublicSelfServiceFlowConfigBuilder usernameTransformer(UsernameTransformer usernameTransformer) {
         if (this.usernameTransformers == null) {
            this.usernameTransformers = new ArrayList<>();
         }

         this.usernameTransformers.add(usernameTransformer);
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public PublicSelfServiceFlowConfig.PublicSelfServiceFlowConfigBuilder usernameTransformers(Collection<? extends UsernameTransformer> usernameTransformers) {
         if (usernameTransformers == null) {
            throw new NullPointerException("usernameTransformers cannot be null");
         }

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

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

      @SuppressFBWarnings(justification = "generated code")
      public PublicSelfServiceFlowConfig.PublicSelfServiceFlowConfigBuilder clearUsernameTransformers() {
         if (this.usernameTransformers != null) {
            this.usernameTransformers.clear();
         }

         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public PublicSelfServiceFlowConfig.PublicSelfServiceFlowConfigBuilder initializeNextAuthFlow(boolean initializeNextAuthFlow) {
         this.initializeNextAuthFlow = initializeNextAuthFlow;
         return this;
      }

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

         return new PublicSelfServiceFlowConfig(
            this.flowId, steps, this.restrictions, this.processors, switch (this.usernameTransformers == null ? 0 : this.usernameTransformers.size()) {
               case 0 -> Collections.emptyList();
               case 1 -> Collections.singletonList(this.usernameTransformers.get(0));
               default -> Collections.unmodifiableList(new ArrayList<>(this.usernameTransformers));
            }, this.initializeNextAuthFlow
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "PublicSelfServiceFlowConfig.PublicSelfServiceFlowConfigBuilder(flowId="
            + this.flowId
            + ", steps="
            + this.steps
            + ", restrictions="
            + this.restrictions
            + ", processors="
            + this.processors
            + ", usernameTransformers="
            + this.usernameTransformers
            + ", initializeNextAuthFlow="
            + this.initializeNextAuthFlow
            + ")";
      }
   }
}
