package com.airlock.iam.flow.infrastructure.factories;

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.elog.api.LogTypes;
import com.airlock.iam.flow.api.application.configuration.selection.SelectionFlowStepConfig;
import com.airlock.iam.flow.api.application.configuration.sequence.FlowStepSequence;
import com.airlock.iam.flow.api.application.configuration.step.FlowStepConfig;
import com.airlock.iam.flow.api.domain.model.selection.Option;
import com.airlock.iam.flow.api.domain.model.step.FlowStep;
import com.airlock.iam.flow.api.domain.model.step.StepId;
import com.airlock.iam.flow.api.domain.model.step.StepResult;
import com.airlock.iam.flow.domain.model.event.StateMachineFlowEvent;
import com.airlock.iam.flow.domain.model.statemachine.BasicFlowEvent;
import com.airlock.iam.flow.domain.model.statemachine.FlowState;
import com.airlock.iam.flow.domain.model.statemachine.StateMachineController;
import com.airlock.iam.flow.domain.model.statemachine.StateMachineFactory;
import com.airlock.iam.flow.domain.model.statemachine.runtime.ConstantState;
import com.airlock.iam.flow.domain.model.statemachine.runtime.FlowEventFactory;
import com.airlock.iam.flow.domain.model.statemachine.spring.CurrentAndNextStepAction;
import com.airlock.iam.flow.domain.model.statemachine.spring.CurrentStepAction;
import com.airlock.iam.flow.domain.model.statemachine.spring.SequentialAction;
import com.airlock.iam.flow.domain.model.statemachine.spring.SpringFlowStateMachine;
import com.airlock.iam.flow.domain.model.statemachine.spring.StateMachineUtil;
import com.airlock.iam.flow.domain.model.statemachine.spring.StepContext;
import com.airlock.iam.flow.domain.model.step.FlowStepConditionChecker;
import com.airlock.iam.flow.impl.domain.model.step.DefaultStepResultFactory;
import com.airlock.iam.flow.impl.domain.model.step.SelectedOption;
import jakarta.inject.Inject;
import java.util.List;
import java.util.Optional;
import org.apache.commons.lang3.Validate;
import org.springframework.beans.factory.support.StaticListableBeanFactory;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.action.Action;
import org.springframework.statemachine.config.StateMachineBuilder;
import org.springframework.statemachine.config.StateMachineBuilder.Builder;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
import org.springframework.statemachine.config.configurers.ExternalTransitionConfigurer;
import org.springframework.statemachine.config.configurers.InternalTransitionConfigurer;
import org.springframework.statemachine.listener.StateMachineListenerAdapter;
import reactor.core.publisher.Mono;

public final class SpringStateMachineFactory implements StateMachineFactory {
   private static final ELogger LOG = ELoggerFactory.getLogger(SpringStateMachineFactory.class);
   private final StepContextFactory stepContextFactory;

   @Inject
   public SpringStateMachineFactory(StepContextFactory stepContextFactory) {
      this.stepContextFactory = stepContextFactory;
   }

   public SpringFlowStateMachine create(List<FlowStepConfig> steps, StateMachineController controller) {
      ViolationException.checkNotEmpty(steps, "At least one step must be configured.");
      StepContextCache stepContextCache = this.stepContextFactory.createStepContextCache(steps);
      List<StepContext> stepContexts = stepContextCache.mapToStepContexts(steps);
      Builder<FlowState, StateMachineFlowEvent> builder = StateMachineBuilder.builder();

      try {
         builder.configureConfiguration().withConfiguration().beanFactory(new StaticListableBeanFactory());
         builder.configureStates()
            .withStates()
            .initial(ConstantState.CREATED)
            .end(ConstantState.SUCCEEDED)
            .end(ConstantState.FAILED)
            .state(ConstantState.SUCCEEDED);
         StepContext firstStepContext = stepContexts.getFirst();
         addStatesForSteps(stepContextCache.getOrderedStepContexts(), firstStepContext, builder, controller);
         wireInitialState(firstStepContext, builder);
         wireFlowStates(Optional.empty(), stepContexts, builder, controller, stepContextCache);
         wireGotoTransitions(controller, stepContextCache, builder);
      } catch (Exception e) {
         throw new FailureException(e);
      }

      StateMachine<FlowState, StateMachineFlowEvent> stateMachine = builder.build();
      stateMachine.addStateListener(new SpringStateMachineFactory.TransitionStateMachineListener());
      return new SpringFlowStateMachine(stateMachine, controller);
   }

   private static void wireGotoTransitions(
      StateMachineController controller, StepContextCache stepContextCache, Builder<FlowState, StateMachineFlowEvent> builder
   ) throws Exception {
      for (GotoFactory.GotoPair gotoPair : GotoFactory.computeAllGotoPairs(stepContextCache)) {
         wireGotoTransitions(gotoPair.origin(), gotoPair.target(), builder, controller);
      }
   }

   private static void addStatesForSteps(
      List<StepContext> stepContexts, StepContext firstStepContext, Builder<FlowState, StateMachineFlowEvent> builder, StateMachineController controller
   ) throws Exception {
      for (StepContext stepContext : stepContexts) {
         if (stepContext.getFlowState().getId().equals(firstStepContext.getFlowState().getId())) {
            builder.configureStates()
               .withStates()
               .stateEntry(stepContext.getFlowState(), entryAction(stepContext, controller::preProcessFirstStep), errorHandler());
         } else {
            builder.configureStates().withStates().stateEntry(stepContext.getFlowState(), entryAction(stepContext, controller::preProcessStep), errorHandler());
         }
      }
   }

   private static void wireInitialState(StepContext firstStep, Builder<FlowState, StateMachineFlowEvent> builder) throws Exception {
      ((ExternalTransitionConfigurer)builder.configureTransitions().withExternal().source(ConstantState.CREATED))
         .target(firstStep.getFlowState())
         .event(BasicFlowEvent.FLOW_STARTED);
   }

   private static void wireFlowStates(
      Optional<StepContext> subsequentStepContext,
      List<StepContext> stepContexts,
      Builder<FlowState, StateMachineFlowEvent> builder,
      StateMachineController controller,
      StepContextCache stepContextCache
   ) throws Exception {
      Validate.notEmpty(stepContexts, "There must be at least one flow step for both the main flow and for selection subflows!", new Object[0]);

      for (StepContext stepContext : stepContexts) {
         wireStayAndFailed(builder, stepContext, controller);
      }

      for (int i = 0; i < stepContexts.size() - 1; i++) {
         wireConsecutiveSteps(
            stepContexts.get(i), stepContexts.get(i + 1).getFlowState(), Optional.of(stepContexts.get(i + 1)), builder, controller, stepContextCache
         );
      }

      StepContext lastStepContext = stepContexts.getLast();
      if (subsequentStepContext.isPresent()) {
         wireConsecutiveSteps(lastStepContext, subsequentStepContext.get().getFlowState(), subsequentStepContext, builder, controller, stepContextCache);
      } else {
         wireConsecutiveSteps(lastStepContext, ConstantState.SUCCEEDED, Optional.empty(), builder, controller, stepContextCache);
      }
   }

   private static void wireGotoTransitions(
      StepContext from, StepContext to, Builder<FlowState, StateMachineFlowEvent> builder, StateMachineController controller
   ) throws Exception {
      ((ExternalTransitionConfigurer)((ExternalTransitionConfigurer)((StateMachineTransitionConfigurer)((ExternalTransitionConfigurer)((ExternalTransitionConfigurer)((ExternalTransitionConfigurer)builder.configureTransitions()
                           .withExternal()
                           .source(from.getFlowState()))
                        .target(to.getFlowState())
                        .event(FlowEventFactory.flowEventForGoto(getTargetStepId(to))))
                     .action(CurrentAndNextStepAction.currentAndNextStepAction(controller::handleGoto, from, to), errorHandler()))
                  .and())
               .withExternal()
               .source(from.getFlowState()))
            .target(to.getFlowState())
            .event(FlowEventFactory.flowEventForFailGoto(getTargetStepId(to))))
         .action(CurrentAndNextStepAction.currentAndNextStepAction(controller::handleFailGoto, from, to), errorHandler());
   }

   private static StepId getTargetStepId(StepContext stepContext) {
      return stepContext.getConfig().getStepId().orElseThrow(() -> new FailureException("Target step must have a Step ID"));
   }

   private static void wireStayAndFailed(Builder<FlowState, StateMachineFlowEvent> builder, StepContext stepContext, StateMachineController controller) throws Exception {
      ((ExternalTransitionConfigurer)((ExternalTransitionConfigurer)((StateMachineTransitionConfigurer)((ExternalTransitionConfigurer)((ExternalTransitionConfigurer)((ExternalTransitionConfigurer)((StateMachineTransitionConfigurer)((InternalTransitionConfigurer)((InternalTransitionConfigurer)((InternalTransitionConfigurer)((StateMachineTransitionConfigurer)((InternalTransitionConfigurer)((InternalTransitionConfigurer)((InternalTransitionConfigurer)builder.configureTransitions()
                                                   .withInternal()
                                                   .source(stepContext.getFlowState()))
                                                .event(BasicFlowEvent.STEP_FAILED_RETRY))
                                             .action(CurrentStepAction.currentStepAction(controller::handleStepFailedRetry, stepContext), errorHandler()))
                                          .and())
                                       .withInternal()
                                       .source(stepContext.getFlowState()))
                                    .event(BasicFlowEvent.STEP_STAY))
                                 .action(CurrentStepAction.currentStepAction(controller::handleStepStay, stepContext), errorHandler()))
                              .and())
                           .withExternal()
                           .source(stepContext.getFlowState()))
                        .target(ConstantState.FAILED)
                        .event(BasicFlowEvent.STEP_FAILED))
                     .action(stepFailedAndFlowFailedAction(controller, stepContext), errorHandler()))
                  .and())
               .withExternal()
               .source(stepContext.getFlowState()))
            .target(ConstantState.FAILED)
            .event(BasicFlowEvent.STEP_INTERNAL_ERROR))
         .action(stepFailedAndFlowFailedAction(controller, stepContext), errorHandler());
   }

   private static SequentialAction stepFailedAndFlowFailedAction(StateMachineController controller, StepContext failingStep) {
      return SequentialAction.builder()
         .action(CurrentStepAction.currentStepAction(controller::handleStepFailed, failingStep))
         .action(CurrentStepAction.currentStepAction(controller::handleFlowFailed, failingStep))
         .build();
   }

   private static void wireConsecutiveSteps(
      StepContext currentStepContext,
      FlowState nextState,
      Optional<StepContext> nextStepContext,
      Builder<FlowState, StateMachineFlowEvent> builder,
      StateMachineController controller,
      StepContextCache stepContextCache
   ) throws Exception {
      FlowStepConfig currentConfig = currentStepContext.getConfig();
      FlowState currentState = currentStepContext.getFlowState();
      ((ExternalTransitionConfigurer)((ExternalTransitionConfigurer)builder.configureTransitions().withExternal().source(currentState))
            .target(nextState)
            .event(BasicFlowEvent.STEP_SKIP))
         .action(skipTransitionAction(currentStepContext, nextState, nextStepContext, controller), errorHandler());
      if (currentConfig instanceof SelectionFlowStepConfig) {
         wireSelectionSubflows(builder, controller, (SelectionFlowStepConfig)currentConfig, currentStepContext, nextStepContext, stepContextCache);
      } else {
         ((ExternalTransitionConfigurer)((ExternalTransitionConfigurer)builder.configureTransitions().withExternal().source(currentState))
               .target(nextState)
               .event(BasicFlowEvent.CONTINUE))
            .action(successTransitionAction(currentStepContext, nextState, nextStepContext, controller), errorHandler());
      }
   }

   private static void wireSelectionSubflows(
      Builder<FlowState, StateMachineFlowEvent> builder,
      StateMachineController controller,
      SelectionFlowStepConfig selectionStepConfig,
      StepContext currentStepContext,
      Optional<StepContext> nextStepContext,
      StepContextCache stepContextCache
   ) throws Exception {
      for (Option option : StepContextFactory.sortedOptions(selectionStepConfig)) {
         FlowStepSequence subflow = selectionStepConfig.getSubFlowForOption(option);
         List<StepContext> subFlowSteps = stepContextCache.mapToStepContexts(subflow.getSteps());
         wireFlowStates(nextStepContext, subFlowSteps, builder, controller, stepContextCache);
         StepContext firstSubflowStepContext = subFlowSteps.getFirst();
         ((ExternalTransitionConfigurer)((ExternalTransitionConfigurer)builder.configureTransitions().withExternal().source(currentStepContext.getFlowState()))
               .target(firstSubflowStepContext.getFlowState())
               .event(FlowEventFactory.flowEventForSubflow(SelectedOption.selectedOption(option.getName()))))
            .action(
               CurrentAndNextStepAction.currentAndNextStepAction(controller::handleSubflowSelected, currentStepContext, firstSubflowStepContext),
               errorHandler()
            );
      }
   }

   private static Action<FlowState, StateMachineFlowEvent> entryAction(StepContext stepContext, SpringStateMachineFactory.EntryFunction function) {
      return context -> {
         StepResult stepResult = function.apply(
            (FlowStep)stepContext.getStep().get(), stepContext.getConfig(), (FlowStepConditionChecker)stepContext.getConditionChecker().get()
         );
         StateMachineUtil.sendEvent(stepResult, context.getStateMachine());
      };
   }

   private static Action<FlowState, StateMachineFlowEvent> skipTransitionAction(
      StepContext currentStepContext, FlowState nextState, Optional<StepContext> nextStepContext, StateMachineController controller
   ) {
      if (nextStepContext.isPresent()) {
         return CurrentStepAction.currentStepAction(controller::handleStepSkipped, currentStepContext);
      }

      ensureSuccessState(nextState);
      return SequentialAction.builder()
         .action(CurrentStepAction.currentStepAction(controller::handleStepSkipped, currentStepContext))
         .action(CurrentStepAction.currentStepAction(controller::handleFlowSucceeded, currentStepContext))
         .build();
   }

   private static Action<FlowState, StateMachineFlowEvent> successTransitionAction(
      StepContext currentStepContext, FlowState nextState, Optional<StepContext> nextStepContext, StateMachineController controller
   ) {
      if (nextStepContext.isPresent()) {
         return CurrentStepAction.currentStepAction(controller::handleStepSucceeded, currentStepContext);
      }

      ensureSuccessState(nextState);
      return SequentialAction.builder()
         .action(CurrentStepAction.currentStepAction(controller::handleStepSucceeded, currentStepContext))
         .action(CurrentStepAction.currentStepAction(controller::handleFlowSucceeded, currentStepContext))
         .build();
   }

   private static void ensureSuccessState(FlowState state) {
      if (!state.equals(ConstantState.SUCCEEDED)) {
         throw new ViolationException("Expected the success state for the final transition.");
      }
   }

   private static Action<FlowState, StateMachineFlowEvent> errorHandler() {
      return context -> {
         LOG.error(LogTypes.SERVER_INFO_SYS, "Internal error step result origin error handler");
         context.getStateMachine()
            .sendEvent(Mono.just(FlowEventFactory.flowEventMessageFor(new DefaultStepResultFactory().internalError().build())))
            .blockLast();
         context.getStateMachine().setStateMachineError(context.getException());
      };
   }

   @FunctionalInterface
   private interface EntryFunction {
      StepResult apply(FlowStep var1, FlowStepConfig var2, FlowStepConditionChecker var3);
   }

   private static final class TransitionStateMachineListener extends StateMachineListenerAdapter<FlowState, StateMachineFlowEvent> {
      public void stateMachineError(StateMachine<FlowState, StateMachineFlowEvent> stateMachine, Exception exception) {
         SpringStateMachineFactory.LOG.error(LogTypes.SERVER_INFO_SYS, "An exception in the state machine has occurred.", exception);
      }
   }
}
