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

import com.airlock.iam.authentication.application.service.flow.session.AuthenticationFlowSessionRepository;
import com.airlock.iam.authentication.domain.model.session.CurrentApplicationId;
import com.airlock.iam.authentication.domain.model.targetapp.TargetApplication;
import com.airlock.iam.base.api.domain.model.exception.FailureException;
import com.airlock.iam.base.api.domain.model.exception.ViolationException;
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.ApplicationId;
import com.airlock.iam.flow.api.domain.model.flow.Flow;
import com.airlock.iam.flow.api.domain.model.flow.FlowResult;
import com.airlock.iam.flow.api.domain.model.session.FlowSession;
import com.airlock.iam.flow.api.domain.model.step.GenericStepError;
import com.airlock.iam.flow.api.domain.model.step.StepResult;
import com.airlock.iam.flow.application.configuration.flow.FlowConfigHash;
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.statemachine.runtime.ConfigurationChangeException;
import com.airlock.iam.flow.domain.model.statemachine.runtime.FlowExceptionHandler;
import com.airlock.iam.flow.domain.service.MultiFlowManager;
import com.airlock.iam.login.api.domain.model.location.ForwardLocation;
import com.airlock.iam.login.domain.model.targetapp.TargetApplicationAccessForwardURI;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.net.URI;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;

public class InternalApplicationAccessService {
   private static final ELogger LOG = ELoggerFactory.getLogger(InternalApplicationAccessService.class);
   private final Provider<MultiFlowManager> multiFlowManager;
   private final Provider<Optional<CurrentApplicationId>> applicationId;
   private final Provider<Flow> flow;
   private final Provider<FlowHandover> flowHandover;
   private final Provider<AuthenticationFlowSessionRepository> flowSessionRepository;
   private final Provider<FlowExceptionHandler> exceptionHandler;

   @Inject
   public InternalApplicationAccessService(
      Provider<MultiFlowManager> multiFlowManager,
      Provider<Optional<CurrentApplicationId>> applicationId,
      Provider<Flow> flow,
      Provider<FlowHandover> flowHandover,
      Provider<AuthenticationFlowSessionRepository> flowSessionRepository,
      Provider<FlowExceptionHandler> exceptionHandler
   ) {
      this.multiFlowManager = multiFlowManager;
      this.applicationId = applicationId;
      this.flow = flow;
      this.flowHandover = flowHandover;
      this.flowSessionRepository = flowSessionRepository;
      this.exceptionHandler = exceptionHandler;
   }

   public FlowResult startNewFlow(TargetApplication targetApp, InternalApplicationAccessService.FlowSessionUpdater flowSessionUpdater) {
      Optional<InternalFlowSession> currentFlowSession = ((MultiFlowManager)this.multiFlowManager.get()).getCurrentFlowSession();
      if (currentFlowSession.isPresent()) {
         LOG.info(
            String.format(
               "A flow is already in progress for application %s, user is NOT authorized to access application %s",
               LogUtil.quote(((CurrentApplicationId)((Optional)this.applicationId.get()).orElseThrow()).id()),
               LogUtil.quote(targetApp.getApplicationId().getApplicationId())
            )
         );
         return this.getUnexpectedCallErrorFromLastFlowStep(currentFlowSession.get());
      } else {
         return this.checkAccessForApplication(targetApp, flowSessionUpdater, false);
      }
   }

   public FlowResult startNewFlowOrReuseIfSameLocationUri(TargetApplication targetApp, ForwardLocation forwardLocation) {
      Optional<InternalFlowSession> currentFlowSession = ((MultiFlowManager)this.multiFlowManager.get()).getCurrentFlowSession();
      if (currentFlowSession.isPresent()) {
         CurrentApplicationId flowSessionAppId = ViolationException.checkPresent(
            (Optional<CurrentApplicationId>)this.applicationId.get(), "Expecting application ID to be present."
         );
         Optional<URI> currentFlowSessionLocation = locationFromCurrentFlowSession(currentFlowSession);
         if (currentFlowSessionLocation.isEmpty() || !currentFlowSessionLocation.get().equals(forwardLocation.value())) {
            LOG.info(
               String.format(
                  "A flow is already in progress with application URI %s for application %s, user is NOT authorized to access application %s with URI %s",
                  LogUtil.logSafeQuote(currentFlowSessionLocation.isEmpty() ? "<not available>" : currentFlowSessionLocation.get().toASCIIString()),
                  LogUtil.quote(flowSessionAppId),
                  LogUtil.quote(targetApp.getApplicationId().getApplicationId()),
                  LogUtil.logSafeQuote(forwardLocation)
               )
            );
            return this.getUnexpectedCallErrorFromLastFlowStep(currentFlowSession.get());
         }

         if (targetApp.getFlowByConfigHash(currentFlowSession.get().getFlowConfigHash()).isEmpty()) {
            throw new ConfigurationChangeException("Cannot continue flow because configuration has changed");
         }
      }

      return this.checkAccessForApplication(targetApp, InternalApplicationAccessService.FlowSessionUpdater.forwardLocationUpdater(forwardLocation), false);
   }

   private static Optional<URI> locationFromCurrentFlowSession(Optional<InternalFlowSession> currentFlowSession) {
      return currentFlowSession.map(FlowSession::flowAttributes)
         .flatMap(attributes -> attributes.tryGetAttribute(TargetApplicationAccessForwardURI.class))
         .map(TargetApplicationAccessForwardURI::getUnverifiedForwardUri);
   }

   public FlowResult access(TargetApplication targetApp, Optional<ForwardLocation> forwardLocation) {
      return this.accessInternal(targetApp, forwardLocation, false);
   }

   public FlowResult accessOneShot(TargetApplication targetApp) {
      return this.accessInternal(targetApp, Optional.empty(), true);
   }

   private FlowResult accessInternal(TargetApplication targetApp, Optional<ForwardLocation> forwardLocation, boolean oneShot) {
      Optional<InternalFlowSession> currentFlowSession = ((MultiFlowManager)this.multiFlowManager.get()).getCurrentFlowSession();
      InternalApplicationAccessService.FlowSessionUpdater flowSessionUpdater = forwardLocation.map(
            InternalApplicationAccessService.FlowSessionUpdater::forwardLocationUpdater
         )
         .orElseGet(InternalApplicationAccessService.FlowSessionUpdater::noUpdater);
      if (currentFlowSession.isEmpty()) {
         return this.checkAccessForApplication(targetApp, flowSessionUpdater, oneShot);
      }

      CurrentApplicationId flowSessionAppId = ViolationException.checkPresent(
         (Optional<CurrentApplicationId>)this.applicationId.get(), "Expecting application ID to be present."
      );
      if (!targetApp.getApplicationId().equals(flowSessionAppId.applicationId())) {
         LOG.info(
            String.format(
               "Currently on flow for application %s, user is NOT authorized to access application %s",
               LogUtil.quote(flowSessionAppId),
               LogUtil.quote(targetApp.getApplicationId().getApplicationId())
            )
         );
         return this.getUnexpectedCallErrorFromLastFlowStep(currentFlowSession.get());
      }

      Optional<URI> currentFlowSessionLocation = locationFromCurrentFlowSession(currentFlowSession);
      if (!forwardLocation.isPresent() || !currentFlowSessionLocation.isEmpty() && currentFlowSessionLocation.get().equals(forwardLocation.get().value())) {
         List<FlowConfigHash> possibleFlowConfigHashes = targetApp.getAllFlowConfigHashes();
         if (!possibleFlowConfigHashes.contains(currentFlowSession.get().getFlowConfigHash())) {
            ((MultiFlowManager)this.multiFlowManager.get()).destroyCurrentMultiFlow();
            throw new ConfigurationChangeException(
               String.format(
                  "Destroying flow session due to a context switch during a running flow or a manual configuration change. FlowConfigHash '%s' was not found.",
                  currentFlowSession.get().getFlowConfigHash()
               )
            );
         } else {
            return this.checkAccessForApplication(targetApp, flowSessionUpdater, oneShot);
         }
      } else {
         LOG.info(
            String.format(
               "A flow for application %s is already in progress with forward location %s, user is NOT authorized to access application forward location %s",
               LogUtil.quote(flowSessionAppId),
               LogUtil.logSafeQuote(currentFlowSessionLocation.isEmpty() ? "<not available>" : currentFlowSessionLocation.get().toASCIIString()),
               LogUtil.logSafeQuote(forwardLocation)
            )
         );
         return this.getUnexpectedCallErrorFromLastFlowStep(currentFlowSession.get());
      }
   }

   private FlowResult getUnexpectedCallErrorFromLastFlowStep(InternalFlowSession flowSession) {
      StepResult lastResult = ViolationException.checkPresent(flowSession.lastResult(), "Expected a stepResult for an existing flowSession");
      return FlowResultImpl.withError(FlowResultImpl.of(lastResult), GenericStepError.UNEXPECTED_CALL);
   }

   private FlowResult checkAccessForApplication(
      TargetApplication targetApp, InternalApplicationAccessService.FlowSessionUpdater flowSessionUpdater, boolean oneShot
   ) {
      Flow currentFlow = this.prepareOrRestoreCurrentFlow(targetApp, flowSessionUpdater);
      FlowResult lastFlowResult = ((FlowHandover)this.flowHandover.get()).runNextFlowsIfPresent(currentFlow.lastResult());
      ApplicationId applicationId = targetApp.getApplicationId();
      if (lastFlowResult.flowCompletedSuccessfully()) {
         LOG.info("Flow succeeded, user is authorized to access application " + LogUtil.quote(applicationId));
      } else if (lastFlowResult.nextStep().isPresent()) {
         LOG.info("Flow in progress, user is NOT yet authorized to access application " + LogUtil.quote(applicationId));
      } else {
         LOG.info("Flow cannot be completed, user is currently NOT authorized to access application " + LogUtil.quote(applicationId));
      }

      return lastFlowResult.nextStep().isPresent() && !oneShot ? FlowResultImpl.withError(lastFlowResult, GenericStepError.NOT_AUTHORIZED) : lastFlowResult;
   }

   private Flow prepareOrRestoreCurrentFlow(TargetApplication targetApp, InternalApplicationAccessService.FlowSessionUpdater flowSessionUpdater) {
      try {
         InternalFlowSession flowSession = ((AuthenticationFlowSessionRepository)this.flowSessionRepository.get())
            .getCurrentOrPrepareFirstFlowSession(targetApp);
         FailureException.checkTrue(((MultiFlowManager)this.multiFlowManager.get()).getCurrentFlowSession().isPresent(), "Flow session must be present");
         flowSessionUpdater.accept(flowSession);
         return (Flow)this.flow.get();
      } catch (RuntimeException e) {
         throw ((FlowExceptionHandler)this.exceptionHandler.get()).toFailureException(e);
      }
   }

   @FunctionalInterface
   public interface FlowSessionUpdater extends Consumer<FlowSession> {
      static InternalApplicationAccessService.FlowSessionUpdater noUpdater() {
         return flowSession -> {};
      }

      static InternalApplicationAccessService.FlowSessionUpdater forwardLocationUpdater(ForwardLocation location) {
         return flowSession -> flowSession.flowAttributes()
            .putOrMergeAttribute(TargetApplicationAccessForwardURI.targetApplicationAccessForwardURI(location.value()));
      }
   }
}
