package com.airlock.iam.flow.shared.domain.service;

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.domain.model.session.FlowAttribute;
import com.airlock.iam.flow.api.domain.model.session.FlowSession;
import com.airlock.iam.flow.api.domain.model.session.MultiFlowScopedFlowAttribute;
import com.airlock.iam.flow.api.domain.model.session.SingleFlowScopedFlowAttribute;
import com.airlock.iam.flow.application.configuration.flow.FlowConfigHash;
import com.airlock.iam.flow.domain.model.MultiFlowConfigurationContext;
import com.airlock.iam.flow.domain.model.session.FlowSessionStatus;
import com.airlock.iam.flow.domain.model.session.InternalFlowSession;
import com.airlock.iam.flow.domain.model.session.MultiFlowSession;
import com.airlock.iam.flow.domain.service.FlowSessionStarter;
import com.airlock.iam.flow.domain.service.MultiFlowManager;
import com.airlock.iam.flow.domain.service.UnexpectedMultiFlowTypeException;
import com.airlock.iam.flow.shared.api.domain.model.session.UserSessionScopedFlowAttribute;
import com.airlock.iam.flow.shared.domain.model.session.MultiFlowCapableUserSession;
import com.airlock.iam.flow.types.api.domain.model.session.KnownMultiFlowType;
import com.airlock.iam.flow.types.api.domain.model.session.MultiFlowType;
import com.airlock.iam.plugin.api.config.ConfigurationContext;
import com.google.common.base.Preconditions;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.util.Optional;
import org.apache.commons.lang3.Strings;

public class MultiFlowManagerImpl implements MultiFlowManager {
   private static final ELogger LOG = ELoggerFactory.getLogger(MultiFlowManagerImpl.class);
   private final Provider<MultiFlowCapableUserSession> userSession;
   private final Provider<FlowSessionStarter> flowSessionStarter;
   private final Provider<ConfigurationContext> configurationContext;
   private final Provider<FlowSessionUserSessionConnector> sessionConnector;

   @Inject
   public MultiFlowManagerImpl(
      Provider<MultiFlowCapableUserSession> userSession,
      Provider<FlowSessionStarter> flowSessionStarter,
      Provider<ConfigurationContext> configurationContext,
      Provider<FlowSessionUserSessionConnector> sessionConnector
   ) {
      this.userSession = userSession;
      this.flowSessionStarter = flowSessionStarter;
      this.configurationContext = configurationContext;
      this.sessionConnector = sessionConnector;
   }

   @Override
   public void expectMultiFlowType(MultiFlowType expectedType) throws UnexpectedMultiFlowTypeException {
      Optional<MultiFlowSession> multiFlowSession = ((MultiFlowCapableUserSession)this.userSession.get()).getMultiFlowSession();
      if (multiFlowSession.isPresent()) {
         MultiFlowType actualType = multiFlowSession.get().getType();
         if (!Strings.CS.equals(actualType.name(), expectedType.name())) {
            this.destroyCurrentMultiFlow();
            throw new UnexpectedMultiFlowTypeException(expectedType, actualType);
         }
      }
   }

   @Override
   public Optional<MultiFlowType> getCurrentMultiFlowType() {
      return ((MultiFlowCapableUserSession)this.userSession.get()).getMultiFlowSession().map(MultiFlowSession::getType);
   }

   @Override
   public boolean hasCurrentFlow() {
      return ((MultiFlowCapableUserSession)this.userSession.get()).getMultiFlowSession().isPresent();
   }

   @Override
   public InternalFlowSession prepareFirstFlowSession(MultiFlowSession multiFlowSession) {
      ViolationException.checkNotPresent(
         ((MultiFlowCapableUserSession)this.userSession.get()).getMultiFlowSession(), "Cannot create new multi-flow session when one is already present"
      );
      FlowConfigHash firstFlowConfigHash = multiFlowSession.getAllConfigHashes().getFirst();
      LOG.info(String.format("%s: Preparing first flow", sessionDescription(multiFlowSession)));
      InternalFlowSession firstFlowSession = this.createNewFlowSession(firstFlowConfigHash);
      ((MultiFlowCapableUserSession)this.userSession.get()).setMultiFlowSession(multiFlowSession);
      firstFlowSession.flowAttributes()
         .putOrMergeAttribute(MultiFlowConfigurationContext.multiFlowConfigurationContext((ConfigurationContext)this.configurationContext.get()));
      return firstFlowSession;
   }

   @Override
   public InternalFlowSession prepareNextFlowSession() {
      MultiFlowSession multiFlowSession = ViolationException.checkPresent(
         ((MultiFlowCapableUserSession)this.userSession.get()).getMultiFlowSession(), "Cannot continue absent multi-flow session"
      );
      FlowConfigHash nextFlowConfigHash = ViolationException.checkPresent(
         multiFlowSession.nextFlowConfigHash(), String.format("%s: Cannot prepare next flow as next flow ID is absent", sessionDescription(multiFlowSession))
      );
      LOG.debug(String.format("%s: Preparing next flow", sessionDescription(multiFlowSession)));
      FlowSession previousFlowSession = ((MultiFlowCapableUserSession)this.userSession.get()).getCurrentFlowSession().orElseThrow();
      InternalFlowSession flowSession = this.createNewFlowSession(nextFlowConfigHash);
      takeOverMultiFlowScopedAttributesToNextFlowSession(previousFlowSession, flowSession);
      return flowSession;
   }

   private static String sessionDescription(MultiFlowSession multiFlowSession) {
      return multiFlowSession.getType() == KnownMultiFlowType.AUTHENTICATION_AND_AUTHORIZATION
         ? String.format("Application Access for ID '%s'", multiFlowSession.getCurrentFlowId())
         : String.format("Flow with type %s and ID '%s'", multiFlowSession.getType(), multiFlowSession.getCurrentFlowId());
   }

   private static void takeOverMultiFlowScopedAttributesToNextFlowSession(FlowSession previousFlowSesison, FlowSession nextFlowSession) {
      previousFlowSesison.flowAttributes()
         .values()
         .filter(MultiFlowManagerImpl::isMultiFlowScoped)
         .forEach(attr -> nextFlowSession.flowAttributes().putOrMergeAttribute(attr));
   }

   private static boolean isMultiFlowScoped(FlowAttribute<?> attribute) {
      checkNoUnexpectedType(attribute);
      return attribute instanceof MultiFlowScopedFlowAttribute;
   }

   private static void checkNoUnexpectedType(FlowAttribute<?> attribute) {
      Preconditions.checkState(
         attribute instanceof SingleFlowScopedFlowAttribute
            || attribute instanceof MultiFlowScopedFlowAttribute
            || attribute instanceof UserSessionScopedFlowAttribute,
         "Unexpected scope of a flowAttribute: " + attribute
      );
   }

   @Override
   public boolean shouldContinueWithNextFlow() {
      return ((MultiFlowCapableUserSession)this.userSession.get()).getMultiFlowSession().map(MultiFlowSession::hasNextFlow).orElse(false);
   }

   private InternalFlowSession createNewFlowSession(FlowConfigHash flowConfigHash) {
      ViolationException.checkNotPresent(
         ((MultiFlowCapableUserSession)this.userSession.get()).getCurrentUnterminatedFlowSession(),
         "Cannot create new flow session when there is an unterminated one present"
      );
      InternalFlowSession flowSession = ((FlowSessionUserSessionConnector)this.sessionConnector.get())
         .createFlowSessionFromUserSession(flowConfigHash, (MultiFlowCapableUserSession)this.userSession.get());
      ((MultiFlowCapableUserSession)this.userSession.get()).setCurrentFlowSession(flowSession);
      LOG.debug("Created new state machine session" + ((MultiFlowCapableUserSession)this.userSession.get()).userId().map(uid -> " for user " + uid).orElse(""));
      return flowSession;
   }

   @Override
   public void startFlowSessionIfNotYetStarted() {
      Optional<InternalFlowSession> flowSession = ((MultiFlowCapableUserSession)this.userSession.get()).getCurrentFlowSession();
      Preconditions.checkState(flowSession.isPresent());
      if (flowSession.get().getStatus().isBefore(FlowSessionStatus.STARTED)) {
         ((FlowSessionStarter)this.flowSessionStarter.get()).startFlowSession(flowSession.get(), this);
      }
   }

   @Override
   public void finishSuccessfulFlow() {
      InternalFlowSession flowSession = ((MultiFlowCapableUserSession)this.userSession.get())
         .getCurrentFlowSession()
         .orElseThrow(() -> new FailureException("A flow session must be present when finishing a flow!"));
      ((FlowSessionUserSessionConnector)this.sessionConnector.get())
         .finalizeFlowSessionToUserSession(flowSession, (MultiFlowCapableUserSession)this.userSession.get());
      ((MultiFlowCapableUserSession)this.userSession.get()).getMultiFlowSession().ifPresent(m -> m.rememberSucceeded(flowSession.getFlowConfigHash()));
      flowSession.terminate();
   }

   @Override
   public boolean destroyCurrentMultiFlow() {
      return ((MultiFlowCapableUserSession)this.userSession.get()).destroyCurrentMultiFlow();
   }

   @Override
   public void destroyAndClearTerminatedFlowSession() {
      Optional<InternalFlowSession> flowSession = ((MultiFlowCapableUserSession)this.userSession.get()).getCurrentFlowSession();
      if (flowSession.isPresent() && !flowSession.get().getStatus().isBefore(FlowSessionStatus.TERMINATED)) {
         flowSession.get().destroy();
         ((MultiFlowCapableUserSession)this.userSession.get()).clearCurrentFlowSession();
      }
   }

   @Override
   public Optional<InternalFlowSession> getCurrentFlowSession() {
      return ((MultiFlowCapableUserSession)this.userSession.get()).getCurrentFlowSession();
   }

   @Override
   public void handleFlowSucceeded() {
      this.finishSuccessfulFlow();
   }

   @Override
   public void handleFlowFailed() {
      this.destroyCurrentMultiFlow();
   }
}
