package com.airlock.iam.flow.shared.infrastructure.session;

import com.airlock.iam.base.api.domain.model.exception.FailureException;
import com.airlock.iam.base.api.domain.model.user.UserId;
import com.airlock.iam.base.domain.model.mapification.LazySupplier;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.base.misc.result.Result;
import com.airlock.iam.common.domain.model.session.ReadOnlyIamSessionData;
import com.airlock.iam.common.domain.model.session.SessionId;
import com.airlock.iam.common.domain.model.session.SessionTimeoutSettings;
import com.airlock.iam.common.domain.model.state.LockAndFindResult;
import com.airlock.iam.common.domain.model.state.RemoveAndUnlockResult;
import com.airlock.iam.common.domain.model.state.SessionStateId;
import com.airlock.iam.common.domain.model.state.StateTimeout;
import com.airlock.iam.common.domain.model.state.UpdateAndUnlockResult;
import com.airlock.iam.common.domain.service.session.CorrelationIdService;
import com.airlock.iam.common.domain.service.session.IamSessionRepository;
import com.airlock.iam.common.domain.service.state.StateRepository;
import com.airlock.iam.common.domain.service.state.StateRepositoryKey;
import com.airlock.iam.common.infrastructure.exceptions.DeserializationFailedException;
import com.airlock.iam.common.infrastructure.exceptions.DeserializationFailedType;
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.shared.api.domain.model.session.UserSessionStatus;
import com.airlock.iam.flow.shared.domain.model.session.FlowBasedIamSession;
import com.airlock.iam.flow.shared.domain.model.session.SingleUserLock;
import com.airlock.iam.flow.shared.domain.service.session.ReadOnlyIamSessionDataFactory;
import com.airlock.iam.plugin.api.config.ConfigurationContext;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.util.Optional;
import java.util.function.Supplier;

public final class FlowBasedIamSessionRepository implements IamSessionRepository<FlowBasedIamSession> {
   private static final ELogger LOG = ELoggerFactory.getLogger(FlowBasedIamSessionRepository.class);
   private final Provider<StateRepository> stateRepository;
   private final Provider<SessionTimeoutSettings> settings;
   private final Provider<FlowBasedIamSessionSerializer> serializer;
   private final Provider<SingleUserLock> singleUserLock;
   private final Provider<CorrelationIdService> correlationIdService;
   private final Provider<ReadOnlyIamSessionDataFactory> readOnlyIamSessionDataFactory;
   private final Provider<ConfigurationContext> contextProvider;
   private final Provider<StateTimeout> defaultTimeout;

   @Inject
   public FlowBasedIamSessionRepository(
      Provider<StateRepository> stateRepository,
      Provider<SessionTimeoutSettings> settings,
      Provider<FlowBasedIamSessionSerializer> serializer,
      Provider<SingleUserLock> singleUserLock,
      Provider<CorrelationIdService> correlationIdService,
      Provider<ReadOnlyIamSessionDataFactory> readOnlyIamSessionDataFactory,
      Provider<ConfigurationContext> contextProvider,
      Provider<StateTimeout> defaultTimeout
   ) {
      this.stateRepository = stateRepository;
      this.settings = settings;
      this.serializer = serializer;
      this.singleUserLock = singleUserLock;
      this.correlationIdService = correlationIdService;
      this.readOnlyIamSessionDataFactory = readOnlyIamSessionDataFactory;
      this.contextProvider = contextProvider;
      this.defaultTimeout = defaultTimeout;
   }

   public FlowBasedIamSession createNewSession() {
      return FlowBasedIamSession.createFreshIamSession(this.sessionIdSupplier());
   }

   @Override
   public Optional<FlowBasedIamSession> retrieveSession(SessionStateId sessionStateId) {
      LockAndFindResult result = ((StateRepository)this.stateRepository.get()).lockAndFind(keyForSessionId(sessionStateId));
      if (!result.entryExists()) {
         return Optional.empty();
      }

      if (result.isLockedBySomeoneElse()) {
         return Optional.of(FlowBasedIamSession.createIamSessionForLockNotAcquired(this.sessionIdSupplier(), sessionStateId));
      }

      Result<FlowBasedIamSession, FlowBasedIamSessionSerializer.DeserializationFailedResult> deserializationResult = ((FlowBasedIamSessionSerializer)this.serializer
            .get())
         .deserializeSession(Optional.of(sessionStateId), result.value().orElseThrow());
      if (deserializationResult.isPresent()) {
         FlowBasedIamSession iamSession = deserializationResult.get();
         iamSession.userId().ifPresent(userId -> ((SingleUserLock)this.singleUserLock.get()).setUserId(userId));
         return Optional.of(iamSession);
      }

      LOG.info("Session deserialization failed: " + deserializationResult.getFailure().message());
      if (deserializationResult.getFailure().type() == DeserializationFailedType.FLOW_FROM_OLD_VERSION) {
         Optional<FlowBasedIamSession> session = deserializationResult.getFailure().session();
         if (session.isPresent()) {
            String serialized = ((FlowBasedIamSessionSerializer)this.serializer.get()).serializeSession(session.get());
            ((StateRepository)this.stateRepository.get()).updateAndUnlock(keyForSessionId(sessionStateId), serialized, (StateTimeout)this.defaultTimeout.get());
         }
      } else {
         RemoveAndUnlockResult removeAndUnlockResult = ((StateRepository)this.stateRepository.get()).removeAndUnlock(keyForSessionId(sessionStateId));
         if (!removeAndUnlockResult.ownedAllLocks()) {
            LOG.error(
               EDefaultLogType.GENERIC_LOG_TYPE, "Did not own all required locks to gracefully remove IAM session from state repository, removing forcefully."
            );
            ((StateRepository)this.stateRepository.get()).removeAndForceUnlock(keyForSessionId(sessionStateId));
         }
      }

      throw new DeserializationFailedException(deserializationResult.getFailure().type(), deserializationResult.getFailure().message());
   }

   private Supplier<SessionId> sessionIdSupplier() {
      return new LazySupplier<>(null, x -> ((CorrelationIdService)this.correlationIdService.get()).determineNewSessionId());
   }

   public void updateSession(FlowBasedIamSession session) {
      if (session.getLazyUserSession().hasBeenSupplied() && session.getUserSession().getStatus() == UserSessionStatus.TERMINATED) {
         throw new FailureException("An already terminated UserSession must not be stored.");
      }

      session.setConfigurationContext((ConfigurationContext)this.contextProvider.get());
      String sessionJson = ((FlowBasedIamSessionSerializer)this.serializer.get()).serializeSession(session);
      Optional<UserId> currentUserLock = ((SingleUserLock)this.singleUserLock.get()).getUserId();
      currentUserLock.ifPresentOrElse(
         userId -> LOG.debug("User ID " + LogUtil.quote(userId) + " will be stored as additional lock key."),
         () -> LOG.debug("No identified User ID to be stored as additional lock key.")
      );
      String user = session.userId().map(userId -> "user " + LogUtil.logSafeQuote(userId)).orElse("unidentified user");
      if (session.isUnpersisted()) {
         boolean success = currentUserLock.<Boolean>map(
               userId -> ((StateRepository)this.stateRepository.get())
                  .insertAndUnlockAdditionalKeys(keyForSessionId(session.sessionStateId()), sessionJson, this.stateTimeout(), keyForUserId(userId))
                  .valueWasInserted()
            )
            .orElseGet(
               () -> ((StateRepository)this.stateRepository.get())
                  .insert(keyForSessionId(session.sessionStateId()), sessionJson, this.stateTimeout())
                  .valueWasInserted()
            );
         if (!success) {
            throw new FailureException("Failed to store new IAM session in state repository for " + user + ".");
         }

         LOG.debug("Stored new IAM session in state repository for " + user + ".");
      } else {
         UpdateAndUnlockResult result = currentUserLock.<UpdateAndUnlockResult>map(
               userId -> ((StateRepository)this.stateRepository.get())
                  .updateAndUnlock(keyForSessionId(session.sessionStateId()), sessionJson, this.stateTimeout(), keyForUserId(userId))
            )
            .orElseGet(
               () -> ((StateRepository)this.stateRepository.get()).updateAndUnlock(keyForSessionId(session.sessionStateId()), sessionJson, this.stateTimeout())
            );
         if (!result.valueWasUpdated()) {
            throw new FailureException("Failed to update IAM session in state repository for " + user + ".");
         }

         LOG.debug("Updating IAM session in state repository for " + user + ".");
      }
   }

   private StateTimeout stateTimeout() {
      return StateTimeout.timeout(((SessionTimeoutSettings)this.settings.get()).idleTimeout(), ((SessionTimeoutSettings)this.settings.get()).lifetime());
   }

   @Override
   public void removeSession(SessionStateId sessionStateId) {
      LOG.debug("Removing IAM session from state repository.");
      this.unlockUser();
      ((StateRepository)this.stateRepository.get()).removeAndForceUnlock(keyForSessionId(sessionStateId));
   }

   @Override
   public void unlockSession(SessionStateId sessionStateId) {
      ((StateRepository)this.stateRepository.get()).unlock(keyForSessionId(sessionStateId));
      ((SingleUserLock)this.singleUserLock.get()).reset();
   }

   @Override
   public Optional<ReadOnlyIamSessionData> retrieveReadOnlySessionData(SessionStateId sessionStateId) {
      Optional<String> result = ((StateRepository)this.stateRepository.get()).findReadOnly(keyForSessionId(sessionStateId));
      if (result.isEmpty()) {
         return Optional.empty();
      } else {
         Result<FlowBasedIamSession, FlowBasedIamSessionSerializer.DeserializationFailedResult> deserializationResult = ((FlowBasedIamSessionSerializer)this.serializer
               .get())
            .deserializeSession(Optional.of(sessionStateId), result.orElseThrow());
         if (deserializationResult.isPresent()) {
            FlowBasedIamSession iamSession = deserializationResult.get();
            return Optional.of(((ReadOnlyIamSessionDataFactory)this.readOnlyIamSessionDataFactory.get()).readOnlyIamSessionData(iamSession));
         } else {
            LOG.info("Session deserialization failed: " + deserializationResult.getFailure().message());
            return Optional.empty();
         }
      }
   }

   private void unlockUser() {
      ((SingleUserLock)this.singleUserLock.get()).getUserId().ifPresentOrElse(userId -> {
         if (((StateRepository)this.stateRepository.get()).unlock(keyForUserId(userId))) {
            LOG.debug("Released lock on user ID " + LogUtil.quote(userId) + ".");
         } else {
            LOG.debug("Could not release lock on currently locked user ID " + LogUtil.quote(userId) + ".");
         }
      }, () -> LOG.debug("No currently locked user ID to be released."));
   }

   private static StateRepositoryKey keyForSessionId(SessionStateId sessionStateId) {
      return new StateRepositoryKey(sessionStateId.id(), StateRepositoryKey.StandardStateEntryType.SESSION);
   }

   private static StateRepositoryKey keyForUserId(UserId userId) {
      return new StateRepositoryKey(userId.getValue(), StateRepositoryKey.StandardStateEntryType.USER);
   }
}
