package com.airlock.iam.flow.application.service;

import com.airlock.iam.base.api.domain.model.exception.EntityNotFoundException;
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.domain.model.flow.FlowId;
import com.airlock.iam.flow.api.domain.model.flow.FlowResult;
import com.airlock.iam.flow.api.domain.model.step.GenericStepError;
import com.airlock.iam.flow.application.configuration.flow.FlowConfigHash;
import com.airlock.iam.flow.application.service.flow.GenericFlowConfigRepository;
import com.airlock.iam.flow.application.service.session.GenericFlowSessionRepository;
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.service.MultiFlowManager;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.util.Optional;

public class GenericFlowSelectionService {
   private static final ELogger LOG = ELoggerFactory.getLogger(GenericFlowSelectionService.class);
   private final GenericFlowConfigRepository configRepository;
   private final Provider<MultiFlowManager> multiFlowManager;
   private final Provider<GenericFlowSessionRepository> flowSessionRepository;
   private final Provider<FlowHandover> flowHandover;

   @Inject
   public GenericFlowSelectionService(
      GenericFlowConfigRepository configRepository,
      Provider<MultiFlowManager> multiFlowManager,
      Provider<GenericFlowSessionRepository> flowSessionRepository,
      Provider<FlowHandover> flowHandover
   ) {
      this.configRepository = configRepository;
      this.multiFlowManager = multiFlowManager;
      this.flowSessionRepository = flowSessionRepository;
      this.flowHandover = flowHandover;
   }

   public FlowResult accessFlow(FlowId flowId) {
      FlowConfigHash flowConfigHash = this.findFlowConfigHash(flowId);
      Optional<InternalFlowSession> currentFlowSession = ((MultiFlowManager)this.multiFlowManager.get()).getCurrentFlowSession();
      return currentFlowSession.isPresent() ? this.tryContinueCurrentFlow(currentFlowSession.get(), flowConfigHash) : this.startNewFlow(flowConfigHash, flowId);
   }

   public FlowConfigHash findFlowConfigHash(FlowId flowId) {
      Optional<FlowConfigHash> flowConfigHash = this.configRepository.resolveFlowId(flowId);
      if (flowConfigHash.isEmpty()) {
         ((MultiFlowManager)this.multiFlowManager.get()).destroyCurrentMultiFlow();
         throw new EntityNotFoundException("Unknown flow ID:" + LogUtil.logSafeString(flowId));
      } else {
         return flowConfigHash.get();
      }
   }

   public FlowResult selectDefaultFlow() {
      return this.configRepository.getDefaultFlowId().map(this::accessFlow).orElseThrow(() -> new FailureException("This flow type has no default flow"));
   }

   public FlowResult tryContinueCurrentFlow(InternalFlowSession currentFlowSession, FlowConfigHash flowConfigHash) {
      FlowResult lastResult = this.lastFlowResult(currentFlowSession);
      if (!currentFlowSession.getFlowConfigHash().equals(flowConfigHash)) {
         LOG.info("Currently on different flow. Flows must be completed or aborted before a new flow can be started.");
         return FlowResultImpl.withError(lastResult, GenericStepError.UNEXPECTED_CALL);
      } else {
         return FlowResultImpl.builder().nextStep(lastResult.nextStep()).additionalAttributes(lastResult.additionalAttributes()).build();
      }
   }

   public FlowResult startNewFlow(FlowConfigHash flowConfigHash, FlowId flowId) {
      InternalFlowSession flowSession = this.prepareNewFlow(flowConfigHash);
      return this.startPreparedFlow(flowConfigHash, flowId, flowSession);
   }

   public InternalFlowSession prepareNewFlow(FlowConfigHash flowConfigHash) {
      return ((GenericFlowSessionRepository)this.flowSessionRepository.get()).prepareNewFlowSession(flowConfigHash);
   }

   public FlowResult startPreparedFlow(FlowConfigHash flowConfigHash, FlowId flowId, InternalFlowSession flowSession) {
      LOG.info(String.format("Starting new flow '%s'", flowId.getFlowId()));
      ((MultiFlowManager)this.multiFlowManager.get()).startFlowSessionIfNotYetStarted();
      FlowResult lastResult = this.lastFlowResult(flowSession);
      if (!flowSession.getFlowConfigHash().equals(flowConfigHash)) {
         LOG.info("Currently on different flow. Flows must be completed or aborted before a new flow can be started.");
         return FlowResultImpl.withError(lastResult, GenericStepError.UNEXPECTED_CALL);
      }

      if (lastResult.nextStep().isEmpty()) {
         ((FlowHandover)this.flowHandover.get()).runNextFlowsIfPresent(lastResult);
      }

      return lastResult;
   }

   private FlowResult lastFlowResult(InternalFlowSession flowSession) {
      return flowSession.lastResult()
         .map(FlowResultImpl::of)
         .map(GenericFlowSelectionService::failRetryToStay)
         .orElseThrow(() -> new FailureException("Current flow should have a last result"));
   }

   private static FlowResult failRetryToStay(FlowResult result) {
      return result.isFailed() && result.nextStep().isPresent()
         ? FlowResultImpl.builder()
            .nextStep(result.nextStep())
            .data(result.data())
            .metadata(result.metadata())
            .additionalAttributes(result.additionalAttributes())
            .build()
         : result;
   }
}
