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

import com.airlock.iam.base.api.domain.model.exception.FailureException;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.flow.api.application.configuration.step.AbstractFlowStepConfig;
import com.airlock.iam.flow.api.application.configuration.step.InternalGotoTargetConfig;
import com.airlock.iam.plugin.api.config.ConfigurationContext;
import com.airlock.iam.plugin.api.config.ConfigurationEnvironment;
import com.airlock.iam.plugin.api.config.PluginConfiguration;
import com.airlock.iam.plugin.api.exception.ConfigurationException;
import com.airlock.iam.plugin.api.exception.PropertyConfigurationException;
import com.airlock.iam.plugin.api.exception.PropertyTypeMismatchException;
import com.airlock.iam.plugin.api.valuereader.PluginPropertyValueReader;
import com.airlock.iam.plugin.api.valuereader.StringPropertyValueReader;
import com.airlock.iam.plugin.framework.misc.config.validation.PluginValidationUtils;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.annotation.Nullable;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;

public final class FlowStepIdConfigValidators {
   private final ConfigurationEnvironment env;
   private final ConfigurationContext context;
   private final List<FlowStepPluginConfiguration> flowSteps;
   private final PluginConfiguration errorRoot;
   private final boolean validatingStandaloneSingleFlow;
   private final boolean validatingSingleFlow;

   public FlowStepIdConfigValidators(
      PluginConfiguration errorRoot,
      ConfigurationEnvironment env,
      ConfigurationContext context,
      boolean validatingStandaloneSingleFlow,
      PluginConfiguration... flowConfigs
   ) {
      this.env = env;
      this.context = context;
      this.errorRoot = errorRoot;
      if (flowConfigs.length == 0) {
         throw new FailureException("Must receive at least one flow config to validate.");
      }

      if (validatingStandaloneSingleFlow && flowConfigs.length > 1) {
         throw new FailureException("Inconsistent call of validator.");
      }

      this.validatingStandaloneSingleFlow = validatingStandaloneSingleFlow;
      this.validatingSingleFlow = flowConfigs.length == 1;
      this.flowSteps = Arrays.stream(flowConfigs)
         .filter(Objects::nonNull)
         .map(flowConfig -> findAllFlowSteps(flowConfig, env, context))
         .flatMap(Collection::stream)
         .collect(Collectors.toList());
   }

   public void validateFlowConstraints() throws ConfigurationException {
      this.validateStepIdsUniqueInFlow();
      Map<String, FlowStepPluginConfiguration> stepIds = this.collectFlowStepIds();
      if (this.validatingSingleFlow) {
         this.validateInteractiveGotoTargets(stepIds.keySet());
         this.validateInternalGotoTargetsExist(stepIds.keySet());
         this.validateOnFailureGotoTargetsExist(stepIds.keySet());
      }

      if (this.isMultiFlowOrStandaloneSingleFlow()) {
         this.validateDynamicStepActivations(stepIds);
      }
   }

   private boolean isMultiFlowOrStandaloneSingleFlow() {
      return !this.validatingSingleFlow || this.validatingStandaloneSingleFlow;
   }

   private void validateStepIdsUniqueInFlow() throws PropertyConfigurationException {
      Set<String> seenStepIds = new HashSet<>();

      for (FlowStepPluginConfiguration step : this.flowSteps) {
         Optional<String> stepId = step.getStepId(this.env, this.context);
         boolean unique = stepId.map(seenStepIds::add).orElse(true);
         if (!unique) {
            throw new PropertyConfigurationException(this.errorRoot, this.env, this.context, this.composeErrorMessage(stepId.get()), new String[]{"steps"});
         }
      }
   }

   private String composeErrorMessage(String stepId) {
      return "Step ID " + LogUtil.quote(stepId) + " is not unique in this " + (this.validatingSingleFlow ? "flow" : "multi-flow");
   }

   private void validateInteractiveGotoTargets(Set<String> stepIds) throws ConfigurationException {
      for (FlowStepPluginConfiguration step : this.flowSteps) {
         if (step.isInteractiveStep()) {
            Set<String> seenTargetStepIds = new HashSet<>();

            for (PluginConfiguration gotoTargetConfig : step.getInteractiveGotoTargets(this.env, this.context)) {
               this.validateGotoTargetExists(gotoTargetConfig, stepIds);
               this.validateInteractiveTargetStepsUnique(gotoTargetConfig, seenTargetStepIds, step);
            }
         }
      }
   }

   private void validateDynamicStepActivations(Map<String, FlowStepPluginConfiguration> stepIds) throws ConfigurationException {
      for (FlowStepPluginConfiguration step : this.flowSteps) {
         if (step.isInteractiveStep()) {
            Set<String> seenTargetStepIds = new HashSet<>();

            for (PluginConfiguration dynamicActivationConfig : step.getDynamicStepActivations(this.env, this.context)) {
               this.validateDynamicStepActivationTargetExists(dynamicActivationConfig, stepIds);
               this.validateDynamicActivationTargetsUnique(dynamicActivationConfig, seenTargetStepIds, step);
            }
         }
      }
   }

   private void validateInteractiveTargetStepsUnique(PluginConfiguration gotoTargetConfig, Set<String> seen, FlowStepPluginConfiguration stepConfig) throws PropertyConfigurationException {
      PluginConfiguration targetStepIdConfig = PluginPropertyValueReader.effectivePluginPropertyValue(gotoTargetConfig, "targetStepId", this.env, this.context);
      String stepId = stepId(targetStepIdConfig, this.env, this.context);
      boolean unique = seen.add(stepId);
      if (!unique) {
         throw new PropertyConfigurationException(
            stepConfig.getPluginConfiguration(),
            this.env,
            this.context,
            "Target step ID " + LogUtil.quote(stepId) + " is not unique. Multiple goto targets to the same step are not possible.",
            new String[]{"interactiveGotoTargets"}
         );
      }
   }

   private void validateOnFailureGotoTargetsExist(Set<String> stepIdsInFlow) throws PropertyConfigurationException {
      for (FlowStepPluginConfiguration step : this.flowSteps) {
         for (PluginConfiguration targetStepIdConfig : step.getOnFailureGotos(this.env, this.context).values()) {
            String stepId = stepId(targetStepIdConfig, this.env, this.context);
            if (StringUtils.isNotBlank(stepId) && !stepIdsInFlow.contains(stepId)) {
               throw new PropertyConfigurationException(
                  step.getPluginConfiguration(),
                  this.env,
                  this.context,
                  "Goto target step ID " + LogUtil.quote(stepId) + " does not exist in this flow",
                  new String[]{"onFailureGotos"}
               );
            }
         }
      }
   }

   private void validateInternalGotoTargetsExist(Set<String> stepIds) throws PropertyConfigurationException {
      for (FlowStepPluginConfiguration step : this.flowSteps) {
         for (PluginConfiguration gotoTargetConfig : this.findAllInternalGotoConfigs(step)) {
            this.validateGotoTargetExists(gotoTargetConfig, stepIds);
         }
      }
   }

   private void validateGotoTargetExists(PluginConfiguration gotoTargetConfig, Set<String> stepIds) throws PropertyConfigurationException {
      PluginConfiguration targetStepIdConfig = PluginPropertyValueReader.effectivePluginPropertyValue(gotoTargetConfig, "targetStepId", this.env, this.context);
      String stepId = stepId(targetStepIdConfig, this.env, this.context);
      if (StringUtils.isNotBlank(stepId) && !stepIds.contains(stepId)) {
         throw new PropertyConfigurationException(
            gotoTargetConfig, this.env, this.context, "No step with this ID exists in this flow", new String[]{"targetStepId"}
         );
      }
   }

   private void validateDynamicStepActivationTargetExists(PluginConfiguration stepActivationConfig, Map<String, FlowStepPluginConfiguration> steps) throws PropertyConfigurationException {
      PluginConfiguration targetStepIdConfig = PluginPropertyValueReader.effectivePluginPropertyValue(
         stepActivationConfig, "targetStepId", this.env, this.context
      );
      String stepId = stepId(targetStepIdConfig, this.env, this.context);
      if (StringUtils.isNotBlank(stepId) && !steps.containsKey(stepId)) {
         throw new PropertyConfigurationException(
            stepActivationConfig, this.env, this.context, "No step with this ID exists in this flow", new String[]{"targetStepId"}
         );
      }

      FlowStepPluginConfiguration targetStep = steps.get(stepId);
      if (targetStep != null && !targetStep.requiresActivation(this.env, this.context)) {
         throw new PropertyConfigurationException(
            stepActivationConfig, this.env, this.context, "The target step does not require activation", new String[]{"targetStepId"}
         );
      }
   }

   private void validateDynamicActivationTargetsUnique(PluginConfiguration stepActivationConfig, Set<String> seen, FlowStepPluginConfiguration stepConfig) throws PropertyConfigurationException {
      PluginConfiguration targetStepIdConfig = PluginPropertyValueReader.effectivePluginPropertyValue(
         stepActivationConfig, "targetStepId", this.env, this.context
      );
      String stepId = stepId(targetStepIdConfig, this.env, this.context);
      boolean unique = seen.add(stepId);
      if (!unique) {
         throw new PropertyConfigurationException(
            stepConfig.getPluginConfiguration(),
            this.env,
            this.context,
            "Target step ID " + LogUtil.quote(stepId) + " is not unique. Multiple dynamic activations for the same step are not possible.",
            new String[]{"dynamicStepActivations"}
         );
      }
   }

   private Map<String, FlowStepPluginConfiguration> collectFlowStepIds() throws PropertyTypeMismatchException {
      Map<String, FlowStepPluginConfiguration> ids = new HashMap<>();

      for (FlowStepPluginConfiguration step : this.flowSteps) {
         Optional<String> stepId = step.getStepId(this.env, this.context);
         stepId.ifPresent(id -> ids.put(id, step));
      }

      return ids;
   }

   private List<PluginConfiguration> findAllInternalGotoConfigs(FlowStepPluginConfiguration stepConfig) {
      return PluginValidationUtils.collectPluginConfigurationsFromSubTree(
         stepConfig.getPluginConfiguration(), this.env, this.context, PluginValidationUtils.pluginsOfClass(InternalGotoTargetConfig.class)
      );
   }

   public static List<FlowStepPluginConfiguration> findAllFlowSteps(PluginConfiguration flowConfig, ConfigurationEnvironment env, ConfigurationContext context) {
      return PluginValidationUtils.collectPluginConfigurationsFromSubTree(
            flowConfig, env, context, PluginValidationUtils.pluginsOfClass(AbstractFlowStepConfig.class)
         )
         .stream()
         .map(FlowStepPluginConfiguration::wrapFlowStepPluginConfiguration)
         .collect(Collectors.toList());
   }

   @Nullable
   private static String stepId(@Nullable PluginConfiguration stepIdConfig, ConfigurationEnvironment env, ConfigurationContext context) throws PropertyTypeMismatchException {
      return stepIdConfig != null ? StringPropertyValueReader.effectiveStringConfigValue(stepIdConfig, "id", env, context) : null;
   }

   public static Optional<String> extractFlowIdFromIdConfig(
      @Nullable PluginConfiguration idConfig, ConfigurationEnvironment environment, ConfigurationContext context
   ) throws PropertyTypeMismatchException {
      return Optional.ofNullable(idConfig != null ? StringPropertyValueReader.effectiveStringConfigValue(idConfig, "flowId", environment, context) : null);
   }

   public static Optional<String> extractFlowId(
      PluginConfiguration flowConfig, String propertyName, ConfigurationEnvironment environment, ConfigurationContext context
   ) throws PropertyTypeMismatchException {
      PluginConfiguration idConfig = PluginPropertyValueReader.effectivePluginPropertyValue(flowConfig, propertyName, environment, context);
      return extractFlowIdFromIdConfig(idConfig, environment, context);
   }

   @SuppressFBWarnings(justification = "generated code")
   public FlowStepIdConfigValidators(
      ConfigurationEnvironment env,
      ConfigurationContext context,
      List<FlowStepPluginConfiguration> flowSteps,
      PluginConfiguration errorRoot,
      boolean validatingStandaloneSingleFlow,
      boolean validatingSingleFlow
   ) {
      this.env = env;
      this.context = context;
      this.flowSteps = flowSteps;
      this.errorRoot = errorRoot;
      this.validatingStandaloneSingleFlow = validatingStandaloneSingleFlow;
      this.validatingSingleFlow = validatingSingleFlow;
   }
}
