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

import com.airlock.iam.base.api.domain.model.exception.FailureException;
import com.airlock.iam.base.api.domain.model.exception.ViolationException;
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.flow.FlowResult;
import com.airlock.iam.flow.api.domain.model.step.FlowStep;
import com.airlock.iam.flow.api.domain.model.step.GenericStepError;
import com.airlock.iam.flow.api.domain.model.step.StepAction;
import com.airlock.iam.flow.api.domain.model.step.StepResult;
import com.airlock.iam.flow.domain.model.MultiFlowConfigurationContext;
import com.airlock.iam.flow.domain.model.flow.handover.FlowHandover;
import com.airlock.iam.flow.domain.model.result.FlowResultImpl;
import com.airlock.iam.flow.domain.model.session.InternalFlowSession;
import com.airlock.iam.flow.domain.model.session.StateMachineHolder;
import com.airlock.iam.flow.domain.model.statemachine.FlowState;
import com.airlock.iam.flow.domain.model.statemachine.FlowStateMachine;
import com.airlock.iam.flow.domain.model.statemachine.InternalFlow;
import com.airlock.iam.flow.domain.service.MultiFlowManager;
import com.airlock.iam.flow.impl.domain.model.step.DefaultStepResultFactory;
import com.airlock.iam.plugin.api.config.ConfigurationContext;
import com.google.common.base.Preconditions;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Function;
import org.apache.commons.lang3.StringUtils;

public final class StateMachineFlow implements InternalFlow {
   private static final ELogger LOG = ELoggerFactory.getLogger(StateMachineFlow.class);
   private final MultiFlowManager multiFlowManager;
   private final Provider<FlowHandover> flowHandover;
   private final Provider<ConfigurationContext> currentRequestsContext;
   private final Provider<FlowExceptionHandler> exceptionHandler;
   private final Provider<StepExceptionHandler> stepExceptionHandler;
   private final Provider<StateMachineHolder> stateMachineHolder;
   private final Provider<CurrentStepFactory> stepFactory;

   @Inject
   public StateMachineFlow(
      MultiFlowManager multiFlowManager,
      Provider<FlowHandover> flowHandover,
      Provider<ConfigurationContext> currentRequestsContext,
      Provider<FlowExceptionHandler> exceptionHandler,
      Provider<StepExceptionHandler> stepExceptionHandler,
      Provider<StateMachineHolder> stateMachineHolder,
      Provider<CurrentStepFactory> stepFactory
   ) {
      this.multiFlowManager = multiFlowManager;
      this.flowHandover = flowHandover;
      this.currentRequestsContext = currentRequestsContext;
      this.exceptionHandler = exceptionHandler;
      this.stepExceptionHandler = stepExceptionHandler;
      this.stateMachineHolder = stateMachineHolder;
      this.stepFactory = stepFactory;
   }

   @Override
   public <L extends FlowStep> FlowResult execute(Function<L, StepResult> stepMethod, Class<L> stepType) {
      return this.execute((step, stepConfig) -> stepMethod.apply((L)step), stepType, FlowStepConfig.class);
   }

   @Override
   public <L extends FlowStep, C extends FlowStepConfig> FlowResult execute(BiFunction<L, C, StepResult> stepMethod, Class<L> stepType, Class<C> stepConfigType) {
      Preconditions.checkArgument(stepType.isInterface(), "The provided step type must be an interface.");
      if (this.failed()) {
         return this.flowResult(this.getFlowSession().lastResult().orElseThrow());
      }

      if (this.succeeded()) {
         ((FlowExceptionHandler)this.exceptionHandler.get())
            .informFlowProcessors(new RuntimeException("Cannot process request on successfully completed flow."));
         this.multiFlowManager.destroyCurrentMultiFlow();
         return this.flowResult(new DefaultStepResultFactory().failDueToUnexpectedCall("Cannot process request on successfully completed flow.").build());
      }

      try {
         if (this.getStateMachine().hasError()) {
            throw new FailureException("An unexpected exception has occurred in the state machine. Aborting.");
         }

         this.assertConfigurationContextHasNotChanged();
         return this.executeStep(stepMethod, stepType, stepConfigType);
      } catch (RuntimeException e) {
         throw ((FlowExceptionHandler)this.exceptionHandler.get()).toFailureException(e);
      }
   }

   private void assertConfigurationContextHasNotChanged() {
      this.multiFlowManager
         .getCurrentFlowSession()
         .ifPresent(
            flowSession -> {
               Optional<MultiFlowConfigurationContext> multiFlowConfigurationContext = flowSession.flowAttributes()
                  .tryGetAttribute(MultiFlowConfigurationContext.class);
               ConfigurationContext flowConfigContext = multiFlowConfigurationContext.orElseThrow(
                     () -> new FailureException("Flow session must contain the configuration context of the request that started the session.")
                  )
                  .getContext();
               if (!flowConfigContext.equals(this.currentRequestsContext.get())) {
                  throw new ConfigurationChangeException(
                     String.format(
                        "Configuration context changed from '%s' to '%s' during multi-flow. Aborting multi-flow.",
                        flowConfigContext,
                        this.currentRequestsContext.get()
                     )
                  );
               }
            }
         );
   }

   private FlowResult flowResult(StepResult lastResult) {
      return FlowResultImpl.of(lastResult);
   }

   private <T extends FlowStep, C extends FlowStepConfig> FlowResult executeStep(
      BiFunction<T, C, StepResult> stepMethod, Class<T> stepType, Class<C> stepConfigType
   ) {
      StepResult stepResult = this.changeUnexpectedCallToFailRetry(this.callStep(stepMethod, stepType, stepConfigType));
      FlowResult flowResult = this.processStepResultInStateMachine(stepResult);
      if (flowResult.flowCompletedSuccessfully() != this.succeeded()) {
         throw new ViolationException("The success property of the flow result is inconsistent with the flow state!");
      } else {
         return ((FlowHandover)this.flowHandover.get()).runNextFlowsIfPresent(flowResult);
      }
   }

   @Override
   public Optional<StepAction> nextAction() {
      return this.getFlowSession().lastResult().flatMap(StepResult::nextAction);
   }

   @Override
   public boolean succeeded() {
      return this.getCurrentFlowState() == ConstantState.SUCCEEDED;
   }

   private boolean failed() {
      return this.getCurrentFlowState() == ConstantState.FAILED;
   }

   @Override
   public FlowResult lastResult() {
      return FlowResultImpl.of(this.getFlowSession().lastResult().orElseThrow(() -> new IllegalStateException("Flow not initialized!")));
   }

   private <L extends FlowStep, C extends FlowStepConfig> StepResult callStep(
      BiFunction<L, C, StepResult> stepMethod, Class<L> stepType, Class<C> stepConfigType
   ) {
      if (!this.getCurrentFlowState().hasStep()) {
         throw new FailureException("Attempting to call a step method on a state without a step");
      }

      Optional<L> typedCurrentStep = ((CurrentStepFactory)this.stepFactory.get()).currentFlowStep(stepType);
      if (typedCurrentStep.isEmpty()) {
         return this.currentStep()
            .stepResultFactory()
            .failDueToUnexpectedCall(
               String.format(
                  "attempted to invoke a %s, but the current step is a %s.",
                  this.toHumanReadableStepLogicClassName(stepType),
                  this.currentStep().getDescription()
               )
            )
            .build();
      }

      Optional<C> currentStepConfig = this.typedCurrentStepConfig(stepConfigType);
      if (currentStepConfig.isEmpty()) {
         return typedCurrentStep.get()
            .stepResultFactory()
            .failDueToUnexpectedCall(
               String.format(
                  "attempted to invoke a step with config %s, but the current step's config is %s.",
                  stepConfigType.getName(),
                  this.currentStepConfig().getClass().getName()
               )
            )
            .build();
      }

      try {
         return stepMethod.apply(typedCurrentStep.get(), currentStepConfig.get());
      } catch (RuntimeException e) {
         return ((StepExceptionHandler)this.stepExceptionHandler.get()).handleExceptionFromStep(e, typedCurrentStep.get());
      }
   }

   private StepResult changeUnexpectedCallToFailRetry(StepResult stepResult) {
      if (stepResult.hasError(GenericStepError.UNEXPECTED_CALL) && !stepResult.hasType(StepResult.StepResultType.FAIL_RETRY)) {
         Optional<StepResult> lastResult = this.getFlowSession().lastResult();
         return lastResult.flatMap(StepResult::nextAction).isEmpty()
            ? stepResult
            : stepResult.toBuilder()
               .type(StepResult.StepResultType.FAIL_RETRY)
               .nextAction(lastResult.get().nextAction().orElseThrow())
               .additionalAttributes(lastResult.get().additionalAttributes())
               .build();
      } else {
         return stepResult;
      }
   }

   private FlowStep currentStep() {
      Preconditions.checkState(this.getCurrentFlowState().hasStep());
      Optional<FlowStep> step = this.getCurrentFlowState().getStep();
      Preconditions.checkState(step.isPresent());
      return step.get();
   }

   private boolean hasCurrentStep() {
      return ((StateMachineHolder)this.stateMachineHolder.get()).getStateMachine().isPresent() && this.getCurrentFlowState().hasStep();
   }

   private <C extends FlowStepConfig> Optional<C> typedCurrentStepConfig(Class<C> stepConfigType) {
      try {
         return Optional.of(stepConfigType.cast(this.currentStepConfig()));
      } catch (ClassCastException e) {
         return Optional.empty();
      }
   }

   private FlowStepConfig currentStepConfig() {
      Preconditions.checkState(this.getCurrentFlowState().hasStep());
      Optional<FlowStepConfig> stepConfig = this.getCurrentFlowState().getStepConfig();
      Preconditions.checkState(stepConfig.isPresent());
      return stepConfig.get();
   }

   private <T extends FlowStep> String toHumanReadableStepLogicClassName(Class<T> stepType) {
      return StringUtils.join(StringUtils.splitByCharacterTypeCamelCase(stepType.getSimpleName()), ' ');
   }

   private FlowResult processStepResultInStateMachine(StepResult result) {
      this.getStateMachine().processStepResult(result);
      return this.flowResult(this.getFlowSession().lastResult().orElseThrow());
   }

   public FlowState getCurrentFlowState() {
      return this.getStateMachine().getCurrentFlowState();
   }

   private FlowStateMachine getStateMachine() {
      return ((StateMachineHolder)this.stateMachineHolder.get())
         .getStateMachine()
         .orElseThrow(() -> new IllegalStateException("Flow session is expected to contain state machine."))
         .get();
   }

   private InternalFlowSession getFlowSession() {
      return this.multiFlowManager
         .getCurrentFlowSession()
         .orElseThrow(() -> new IllegalStateException("Current flow session is expected to exist at this point."));
   }

   @Override
   public void finalizeCurrentStep() {
      if (this.hasCurrentStep()) {
         this.getFlowSession().finalizeStepIfNecessary(this.currentStep());
      }
   }

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

   @SuppressFBWarnings(justification = "generated code")
   public static class StateMachineFlowBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private MultiFlowManager multiFlowManager;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<FlowHandover> flowHandover;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<ConfigurationContext> currentRequestsContext;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<FlowExceptionHandler> exceptionHandler;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<StepExceptionHandler> stepExceptionHandler;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<StateMachineHolder> stateMachineHolder;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<CurrentStepFactory> stepFactory;

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

      @SuppressFBWarnings(justification = "generated code")
      public StateMachineFlow.StateMachineFlowBuilder multiFlowManager(MultiFlowManager multiFlowManager) {
         this.multiFlowManager = multiFlowManager;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public StateMachineFlow.StateMachineFlowBuilder flowHandover(Provider<FlowHandover> flowHandover) {
         this.flowHandover = flowHandover;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public StateMachineFlow.StateMachineFlowBuilder currentRequestsContext(Provider<ConfigurationContext> currentRequestsContext) {
         this.currentRequestsContext = currentRequestsContext;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public StateMachineFlow.StateMachineFlowBuilder exceptionHandler(Provider<FlowExceptionHandler> exceptionHandler) {
         this.exceptionHandler = exceptionHandler;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public StateMachineFlow.StateMachineFlowBuilder stepExceptionHandler(Provider<StepExceptionHandler> stepExceptionHandler) {
         this.stepExceptionHandler = stepExceptionHandler;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public StateMachineFlow.StateMachineFlowBuilder stateMachineHolder(Provider<StateMachineHolder> stateMachineHolder) {
         this.stateMachineHolder = stateMachineHolder;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public StateMachineFlow.StateMachineFlowBuilder stepFactory(Provider<CurrentStepFactory> stepFactory) {
         this.stepFactory = stepFactory;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public StateMachineFlow build() {
         return new StateMachineFlow(
            this.multiFlowManager,
            this.flowHandover,
            this.currentRequestsContext,
            this.exceptionHandler,
            this.stepExceptionHandler,
            this.stateMachineHolder,
            this.stepFactory
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "StateMachineFlow.StateMachineFlowBuilder(multiFlowManager="
            + this.multiFlowManager
            + ", flowHandover="
            + this.flowHandover
            + ", currentRequestsContext="
            + this.currentRequestsContext
            + ", exceptionHandler="
            + this.exceptionHandler
            + ", stepExceptionHandler="
            + this.stepExceptionHandler
            + ", stateMachineHolder="
            + this.stateMachineHolder
            + ", stepFactory="
            + this.stepFactory
            + ")";
      }
   }
}
