package com.airlock.iam.flow.shared.domain.model.session;

import com.airlock.iam.base.api.domain.model.exception.ViolationException;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.common.api.domain.model.user.UserIdentity;
import com.airlock.iam.elog.api.EDefaultLogType;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.flow.api.domain.model.redflag.RedFlag;
import com.airlock.iam.flow.api.domain.model.session.FlowAttributeRepository;
import com.airlock.iam.flow.api.domain.model.step.FlowStep;
import com.airlock.iam.flow.api.domain.model.step.StepResult;
import com.airlock.iam.flow.api.domain.model.step.StepSession;
import com.airlock.iam.flow.api.domain.model.tag.Tag;
import com.airlock.iam.flow.api.domain.model.tag.TagName;
import com.airlock.iam.flow.application.configuration.flow.FlowConfigHash;
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.tag.FlowSessionTags;
import com.airlock.iam.flow.domain.service.session.DefaultFlowAttributeRepository;
import com.airlock.iam.flow.shared.api.domain.model.session.UserDependentFlowSession;
import com.google.common.base.Preconditions;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.annotation.Nullable;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import lombok.NonNull;

public class FlowSessionImpl implements InternalFlowSession, UserDependentFlowSession {
   private static final ELogger LOG = ELoggerFactory.getLogger(FlowSessionImpl.class);
   private FlowSessionStatus status;
   private final FlowConfigHash flowConfigHash;
   private final FlowSessionTags flowSessionTags;
   private Optional<UserIdentity> userIdentity;
   private final Set<RedFlag> redFlags;
   private final Deque<StepResult> history;
   private final FlowAttributeRepository attributeRepository;
   private StepSession currentStepSession;
   private boolean mustFinalizeCurrentStep;

   public FlowSessionImpl(FlowConfigHash flowConfigHash, Set<Tag> initialTags) {
      this.flowConfigHash = flowConfigHash;
      this.flowSessionTags = new FlowSessionTags(Tag.tagNamesOf(initialTags));
      this.status = FlowSessionStatus.CREATED;
      this.redFlags = new HashSet<>();
      this.history = new ArrayDeque<>();
      this.attributeRepository = new DefaultFlowAttributeRepository();
      this.userIdentity = Optional.empty();
   }

   public static FlowSessionImpl restoreFlowSession(
      FlowSessionStatus status,
      FlowConfigHash flowConfigHash,
      FlowSessionTags flowSessionTags,
      @NonNull Optional<UserIdentity> userIdentity,
      Set<RedFlag> redFlags,
      Deque<StepResult> history,
      FlowAttributeRepository flowAttributes,
      @Nullable StepSession currentStepSession,
      boolean mustFinalizeCurrentStep
   ) {
      if (userIdentity == null) {
         throw new NullPointerException("userIdentity is marked non-null but is null");
      } else {
         return new FlowSessionImpl(
            status, flowConfigHash, flowSessionTags, userIdentity, redFlags, history, flowAttributes, currentStepSession, mustFinalizeCurrentStep
         );
      }
   }

   @Override
   public void start() {
      this.checkUnstarted();
      this.status = FlowSessionStatus.STARTED;
   }

   @Override
   public Optional<StepResult> lastResult() {
      return this.history.isEmpty() ? Optional.empty() : Optional.of(this.history.getLast());
   }

   @Override
   public Deque<StepResult> getStepResultHistory() {
      return new ArrayDeque<>(this.history);
   }

   @Override
   public void addResult(StepResult result) {
      this.checkUnterminated();
      Preconditions.checkNotNull(result, "step result must not be null");
      this.history.addLast(result);
   }

   @Override
   public int getResultHistorySize() {
      return this.history.size();
   }

   @Override
   public Optional<StepSession> getCurrentStepSession() {
      return Optional.ofNullable(this.currentStepSession);
   }

   @Override
   public void setCurrentStepSession(StepSession session) {
      this.checkUnterminated();
      this.currentStepSession = (StepSession)Preconditions.checkNotNull(session, "step session must not be null");
   }

   @Override
   public void clearCurrentStepSession() {
      this.currentStepSession = null;
   }

   @Override
   public Set<RedFlag> getRedFlags() {
      return Set.copyOf(this.redFlags);
   }

   @Override
   public void raiseRedFlag(RedFlag redFlag) {
      this.checkUnterminated();
      this.redFlags.add(redFlag);
   }

   @Override
   public void takeDownRedFlag(RedFlag redFlag) {
      this.checkUnterminated();
      if (!this.redFlags.remove(redFlag)) {
         throw new ViolationException(String.format("Cannot take down red flag '%s' that isn't raised.", redFlag.getName()));
      }
   }

   @Override
   public Set<TagName> getAllFlowTagNames() {
      return Set.copyOf(this.flowSessionTags.getAllTagNames());
   }

   @Override
   public FlowAttributeRepository flowAttributes() {
      return this.attributeRepository;
   }

   @Override
   public void stepInitializationStarted() {
      this.mustFinalizeCurrentStep = true;
   }

   @Override
   public void finalizeStepIfNecessary(FlowStep step) {
      if (!this.mustFinalizeCurrentStep) {
         LOG.debug(String.format("Step %s already finalized. Skipping finalization.", LogUtil.quote(step.getDescription())));
      } else {
         try {
            step.finalizeStep();
         } catch (Exception e) {
            LOG.error(EDefaultLogType.GENERIC_LOG_TYPE, "Exception while finalizing " + LogUtil.quote(step.getDescription()), e);
         } finally {
            this.mustFinalizeCurrentStep = false;
            this.clearCurrentStepSession();
         }
      }
   }

   @Override
   public void terminate() {
      this.checkUnterminated();
      this.status = FlowSessionStatus.TERMINATED;
   }

   @Override
   public void destroy() {
      this.checkUndestroyed();
      this.flowSessionTags.destroy();
      this.currentStepSession = null;
      this.attributeRepository.removeAllAttributes();
      this.userIdentity = Optional.empty();
      this.status = FlowSessionStatus.DESTROYED;
   }

   @Override
   public Optional<UserIdentity> getUserIdentity() {
      return this.userIdentity;
   }

   @Override
   public void setUserIdentity(UserIdentity newUserIdentity) {
      if (this.userIdentity.isPresent() && !this.userIdentity.get().hasSameId(newUserIdentity)) {
         throw new ViolationException("Cannot change user ID during flow session");
      }

      this.userIdentity = Optional.of(newUserIdentity);
   }

   private void checkUnstarted() {
      Preconditions.checkState(this.status.isBefore(FlowSessionStatus.STARTED), "Flow session should not have been started at this point");
   }

   @Override
   public void checkUnterminated() {
      Preconditions.checkState(this.status.isBefore(FlowSessionStatus.TERMINATED), "Flow session should not have been terminated at this point");
   }

   @Override
   public void checkUndestroyed() {
      Preconditions.checkState(this.status.isBefore(FlowSessionStatus.DESTROYED), "Flow session should not have been destroyed at this point.");
   }

   @SuppressFBWarnings(justification = "generated code")
   private FlowSessionImpl(
      FlowSessionStatus status,
      FlowConfigHash flowConfigHash,
      FlowSessionTags flowSessionTags,
      Optional<UserIdentity> userIdentity,
      Set<RedFlag> redFlags,
      Deque<StepResult> history,
      FlowAttributeRepository attributeRepository,
      StepSession currentStepSession,
      boolean mustFinalizeCurrentStep
   ) {
      this.status = status;
      this.flowConfigHash = flowConfigHash;
      this.flowSessionTags = flowSessionTags;
      this.userIdentity = userIdentity;
      this.redFlags = redFlags;
      this.history = history;
      this.attributeRepository = attributeRepository;
      this.currentStepSession = currentStepSession;
      this.mustFinalizeCurrentStep = mustFinalizeCurrentStep;
   }

   @SuppressFBWarnings(justification = "generated code")
   @Override
   public FlowSessionStatus getStatus() {
      return this.status;
   }

   @SuppressFBWarnings(justification = "generated code")
   @Override
   public FlowConfigHash getFlowConfigHash() {
      return this.flowConfigHash;
   }

   @SuppressFBWarnings(justification = "generated code")
   @Override
   public FlowSessionTags getFlowSessionTags() {
      return this.flowSessionTags;
   }

   @SuppressFBWarnings(justification = "generated code")
   @Override
   public boolean mustFinalizeCurrentStep() {
      return this.mustFinalizeCurrentStep;
   }
}
