package com.airlock.iam.flow.domain.model.statemachine.runtime;

import com.airlock.iam.base.api.domain.model.exception.FailureException;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.flow.api.application.configuration.step.FlowStepConfig;
import com.airlock.iam.flow.api.domain.model.processor.FlowProcessor;
import com.airlock.iam.flow.api.domain.model.processor.FlowProcessorFailureResult;
import com.airlock.iam.flow.api.domain.model.redflag.RedFlag;
import com.airlock.iam.flow.api.domain.model.step.AdditionalAttributes;
import com.airlock.iam.flow.api.domain.model.step.FlowStep;
import com.airlock.iam.flow.api.domain.model.step.StepResult;
import com.airlock.iam.flow.api.domain.model.step.StepResultData;
import com.airlock.iam.flow.api.domain.model.step.StepSession;
import com.airlock.iam.flow.domain.model.event.GenericStepResultEventFactory;
import com.airlock.iam.flow.domain.model.processor.FlowProcessorFunction;
import com.airlock.iam.flow.domain.model.processor.FlowProcessorRunner;
import com.airlock.iam.flow.domain.model.session.InternalFlowSession;
import com.airlock.iam.flow.domain.model.statemachine.StateMachineController;
import com.airlock.iam.flow.domain.model.step.FlowStepConditionChecker;
import com.airlock.iam.flow.domain.model.step.OnFailureGotos;
import com.airlock.iam.flow.domain.model.step.attributes.CustomAttributesAdder;
import com.airlock.iam.flow.domain.model.tag.SuccessTagsHelper;
import com.airlock.iam.flow.domain.service.FlowCompletionService;
import com.airlock.iam.flow.domain.service.FlowExitService;
import com.airlock.iam.flow.domain.service.MultiFlowManager;
import com.google.common.base.Preconditions;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.inject.Provider;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;

public class StateMachineControllerImpl implements StateMachineController {
   private static final ELogger LOG = ELoggerFactory.getLogger(StateMachineControllerImpl.class);
   private static final int RESULT_HISTORY_LIMIT = 10000;
   private final MultiFlowManager multiFlowManager;
   private final List<FlowProcessor> processors;
   private final SuccessTagsHelper successTags;
   private final FlowCompletionService flowCompletionService;
   private final FlowExitService flowExitService;
   private final StepExceptionHandler stepExceptionHandler;
   private final InternalFlowSession flowSession;
   private final Provider<GenericStepResultEventFactory> eventFactory;
   private final Provider<CustomAttributesAdder> customAttributesAdder;
   private final Provider<OnFailureGotos> onFailureGotos;

   @Override
   public void handleStepSucceeded(FlowStep currentStep, StepResult stepResult) {
      log("Step succeeded", currentStep, stepResult);
      this.handleStepResult(stepResult, currentStep);
   }

   @Override
   public void handleStepSkipped(FlowStep currentStep, StepResult stepResult) {
      log("Step skipped", currentStep, stepResult);
      this.handleStepResult(stepResult, currentStep);
   }

   @Override
   public void handleStepFailedRetry(FlowStep currentStep, StepResult stepResult) {
      log("Step failed", currentStep, stepResult);
      this.handleStepResult(stepResult, currentStep);
   }

   @Override
   public void handleStepFailed(FlowStep failingStep, StepResult stepResult) {
      log("Step failed", failingStep, stepResult);
      this.handleStepResult(stepResult, failingStep);
   }

   @Override
   public void handleSubflowSelected(FlowStep currentStep, StepResult stepResult, FlowStep firstSubflowStep) {
      log("Subflow selected", currentStep, firstSubflowStep, stepResult);
      this.handleStepResult(stepResult, currentStep);
   }

   @Override
   public void handleStepStay(FlowStep currentStep, StepResult stepResult) {
      log("Staying in step", currentStep, stepResult);
      this.handleStepResult(stepResult, currentStep);
   }

   @Override
   public void handleGoto(FlowStep from, StepResult stepResult, FlowStep to) {
      log("Goto executed", from, to, stepResult);
      this.handleStepResult(stepResult, from);
   }

   @Override
   public void handleFailGoto(FlowStep from, StepResult stepResult, FlowStep to) {
      log("Goto counting as failure executed", from, to, stepResult);
      this.handleStepResult(stepResult, from);
   }

   private void handleStepResult(StepResult stepResult, FlowStep origin) {
      this.flowSession.getFlowSessionTags().addTags(stepResult.obtainedTags());
      this.addResultToFlowSession(stepResult);
      if (stepResult.isLeavingStep()) {
         this.flowSession.finalizeStepIfNecessary(origin);
      }

      ((GenericStepResultEventFactory)this.eventFactory.get()).publishGenericStepResultEvent(stepResult);
   }

   @Override
   public void handleFlowSucceeded(FlowStep lastStep, StepResult stepResult) {
      this.verifyNoUnconsumedRedFlags();
      this.multiFlowManager.handleFlowSucceeded();
      this.processors.forEach(FlowProcessor::flowSucceeded);
      this.flowCompletionService.handleSuccessfulFlowCompletion();
      log("Flow completed successfully", lastStep, stepResult);
   }

   @Override
   public void handleFlowFailed(FlowStep failingStep, StepResult stepResult) {
      this.processors.forEach(FlowProcessor::flowFailed);
      this.multiFlowManager.handleFlowFailed();
      log("Flow failed", failingStep, stepResult);
   }

   @Override
   public StepResult preProcessFirstStep(FlowStep firstStep, FlowStepConfig config, FlowStepConditionChecker conditionChecker) {
      Optional<FlowProcessorFailureResult> result = this.handlePreFlowStarted();
      if (result.isPresent()) {
         StepResult stepResult = result.get().toStepResult();
         this.addResultToFlowSession(stepResult);
         log("Flow could not be started", firstStep, stepResult);
         return stepResult;
      } else {
         return this.preProcessStep(firstStep, config, conditionChecker);
      }
   }

   private Optional<FlowProcessorFailureResult> handlePreFlowStarted() {
      for (FlowProcessor processor : this.processors) {
         Optional<FlowProcessorFailureResult> result = processor.preFlowStarted();
         if (result.isPresent()) {
            return result;
         }
      }

      return Optional.empty();
   }

   @Override
   public StepResult preProcessStep(FlowStep currentStep, FlowStepConfig config, FlowStepConditionChecker conditionChecker) {
      return this.evaluateConditionResult(currentStep, conditionChecker)
         .map(result -> this.postProcessConditionCheckResult(result, currentStep))
         .orElseGet(() -> this.preInitializeStep(currentStep).orElseGet(() -> {
            StepResult initResult = this.initStep(currentStep);
            return this.postProcessInitializeStepResult(initResult, currentStep, config);
         }));
   }

   @Override
   public StepResult postProcessStepResult(StepResult stepResult, FlowStep currentFlowStep, FlowStepConfig stepConfig) {
      StepResult newStepResult = this.successTags.addOnSuccessTags(stepResult, stepConfig);
      newStepResult = ((OnFailureGotos)this.onFailureGotos.get()).executeGotoOnFailure(newStepResult, stepConfig);
      newStepResult = ((CustomAttributesAdder)this.customAttributesAdder.get()).addCustomAttributes(newStepResult);
      return this.runProcessors(newStepResult, currentFlowStep, FlowProcessor::postProcessStepResult);
   }

   private void addResultToFlowSession(StepResult currentResult) {
      if (this.flowSession.getResultHistorySize() > 10000) {
         throw new FailureException("Result history of current flow exceeded limit. Infinite loop?");
      }

      StepResult result = currentResult;
      if (!isFailureReturningToClient(currentResult)) {
         result = this.withResultDataFromPreviousResult(currentResult);
         result = this.withAdditionalAttributesFromPreviousResult(result);
      }

      this.flowSession.addResult(result);
   }

   private static boolean isFailureReturningToClient(StepResult stepResult) {
      return stepResult.isFailure() && stepResult.isReturningToClient();
   }

   private StepResult withAdditionalAttributesFromPreviousResult(StepResult currentResult) {
      Optional<AdditionalAttributes> previousAdditionalAttributes = this.flowSession
         .lastResult()
         .filter(Predicate.not(StepResult::isReturningToClient))
         .map(StepResult::additionalAttributes)
         .filter(Predicate.not(AdditionalAttributes::isEmpty))
         .map(AdditionalAttributes::copy);
      if (previousAdditionalAttributes.isPresent()) {
         LOG.debug("Keeping additional attributes from previous step.");
      }

      return previousAdditionalAttributes.<AdditionalAttributes>map(previousAttributes -> previousAttributes.putAll(currentResult.additionalAttributes()))
         .map(mergedAttributes -> replaceAttributes(currentResult, mergedAttributes))
         .orElse(currentResult);
   }

   private StepResult withResultDataFromPreviousResult(StepResult currentResult) {
      Optional<StepResultData> previousResultData = this.flowSession
         .lastResult()
         .filter(prevRes -> prevRes.hasAnyType(StepResult.StepResultType.SUCCESS, StepResult.StepResultType.SKIP))
         .filter(r -> currentResult.resultData().isEmpty())
         .flatMap(StepResult::resultData);
      if (previousResultData.isPresent()) {
         LOG.debug("Keeping result data from previous step.");
      }

      return previousResultData.<StepResult>map(data -> replaceResultData(currentResult, data)).orElse(currentResult);
   }

   private static StepResult replaceAttributes(StepResult result, AdditionalAttributes attributes) {
      return result.toBuilder().additionalAttributes(attributes).build();
   }

   private static StepResult replaceResultData(StepResult result, StepResultData data) {
      return result.toBuilder().resultData(data).build();
   }

   private void verifyNoUnconsumedRedFlags() {
      Set<RedFlag> redFlags = this.flowSession.getRedFlags();
      if (!redFlags.isEmpty()) {
         String commaSeparatedFlags = redFlags.stream().map(RedFlag::getName).collect(Collectors.joining(", "));
         throw new FailureException(
            "Found unconsumed red flags after completing the last step in the flow: "
               + commaSeparatedFlags
               + ". Change the configuration such that every red flag is taken down by some step."
         );
      }
   }

   private Optional<StepResult> evaluateConditionResult(FlowStep currentStep, FlowStepConditionChecker conditionChecker) {
      if (conditionChecker.skipThisStep()) {
         LOG.debug("Skipping step " + description(currentStep));
         this.skipStep(currentStep);
         return Optional.of(currentStep.stepResultFactory().skip().build());
      } else if (!conditionChecker.preconditionSatisfied()) {
         LOG.info("Terminate user session because a step precondition was not fulfilled.");
         this.flowExitService.exitFlow();
         return Optional.of(currentStep.stepResultFactory().internalError().build());
      } else {
         return Optional.empty();
      }
   }

   private void skipStep(FlowStep currentStep) {
      FlowProcessorRunner.flowProcessorRunner(this.processors).onSkip(currentStep);
   }

   private Optional<StepResult> preInitializeStep(FlowStep currentStep) {
      Optional<StepResult> result = FlowProcessorRunner.flowProcessorRunner(this.processors).preProcess(currentStep, FlowProcessor::preInitializeStep);
      if (result.isPresent()) {
         LOG.debug("Preprocessing result for step " + description(currentStep));
         Preconditions.checkState(
            result.get().isLeavingStep(), "Step pre-initialization cannot return result that does not leave step, but result was " + result.get().type()
         );
      }

      return result;
   }

   private StepResult initStep(FlowStep step) {
      Optional<StepSession> stepSession = step.createSession();
      if (stepSession.isPresent()) {
         this.flowSession.setCurrentStepSession(stepSession.get());
      }

      this.flowSession.stepInitializationStarted();
      StepResult result = this.tryCallInitialize(step);
      Preconditions.checkState(
         !result.isFailure() || result.isLeavingStep(), "Step initialization cannot fail without leaving step, but result was " + result.type()
      );
      log("Initialized step", step, result);
      return result;
   }

   private StepResult tryCallInitialize(FlowStep step) {
      try {
         return step.initialize();
      } catch (RuntimeException e) {
         return this.stepExceptionHandler.handleExceptionFromStep(e, step);
      }
   }

   private StepResult postProcessInitializeStepResult(StepResult stepResult, FlowStep currentFlowStep, FlowStepConfig config) {
      StepResult newStepResult = this.successTags.addOnSuccessTags(stepResult, config);
      newStepResult = ((OnFailureGotos)this.onFailureGotos.get()).executeGotoOnFailure(newStepResult, config);
      newStepResult = ((CustomAttributesAdder)this.customAttributesAdder.get()).addCustomAttributes(newStepResult);
      StepResult postProcessingResult = this.runProcessors(newStepResult, currentFlowStep, FlowProcessor::postProcessInitializeStepResult);
      LOG.debug("Post processed initialize step result => " + postProcessingResult);
      return postProcessingResult;
   }

   private StepResult postProcessConditionCheckResult(StepResult stepResult, FlowStep currentFlowStep) {
      return this.runProcessors(stepResult, currentFlowStep, FlowProcessor::postProcessInitializeStepResult);
   }

   private StepResult runProcessors(StepResult stepResult, FlowStep currentFlowStep, FlowProcessorFunction processing) {
      return FlowProcessorRunner.flowProcessorRunner(this.processors).postProcess(stepResult, currentFlowStep, processing);
   }

   private static void log(String message, FlowStep currentStep, StepResult result) {
      log(message, currentStep, Optional.empty(), result);
   }

   private static void log(String message, FlowStep currentStep, FlowStep nextStep, StepResult result) {
      log(message, currentStep, Optional.of(nextStep), result);
   }

   private static void log(String message, FlowStep step, Optional<FlowStep> nextStep, StepResult result) {
      LOG.debug(
         message + " - step: " + description(step) + ", " + nextStep.<String>map(s -> "next step: " + description(s) + ", ").orElse("") + "result: " + result
      );
   }

   private static String description(FlowStep step) {
      return LogUtil.quote(step.getDescription());
   }

   @SuppressFBWarnings(justification = "generated code")
   public StateMachineControllerImpl(
      MultiFlowManager multiFlowManager,
      List<FlowProcessor> processors,
      SuccessTagsHelper successTags,
      FlowCompletionService flowCompletionService,
      FlowExitService flowExitService,
      StepExceptionHandler stepExceptionHandler,
      InternalFlowSession flowSession,
      Provider<GenericStepResultEventFactory> eventFactory,
      Provider<CustomAttributesAdder> customAttributesAdder,
      Provider<OnFailureGotos> onFailureGotos
   ) {
      this.multiFlowManager = multiFlowManager;
      this.processors = processors;
      this.successTags = successTags;
      this.flowCompletionService = flowCompletionService;
      this.flowExitService = flowExitService;
      this.stepExceptionHandler = stepExceptionHandler;
      this.flowSession = flowSession;
      this.eventFactory = eventFactory;
      this.customAttributesAdder = customAttributesAdder;
      this.onFailureGotos = onFailureGotos;
   }
}
