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.exception.ViolationException;
import com.airlock.iam.base.api.domain.model.mapification.MapifiedValues;
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.json.JsonCoding;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.base.misc.result.Result;
import com.airlock.iam.common.domain.model.session.SessionId;
import com.airlock.iam.common.domain.model.state.SessionStateId;
import com.airlock.iam.common.infrastructure.session.SessionNoncesMapifier;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
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.statemachine.FlowStateMachine;
import com.airlock.iam.flow.infrastructure.serialization.StateMachineSerializer;
import com.airlock.iam.flow.shared.domain.model.session.FlowBasedIamSession;
import com.airlock.iam.flow.shared.domain.model.session.LazyUserSession;
import com.airlock.iam.flow.shared.domain.model.session.UserSessionImpl;
import com.airlock.iam.flow.shared.infrastructure.service.UserSessionMapifier;
import com.airlock.iam.plugin.api.config.ConfigurationContext;
import com.airlock.iam.system.version.IamVersion;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.util.Optional;
import java.util.function.Supplier;

public final class FlowBasedIamSessionSerializerImpl implements FlowBasedIamSessionSerializer {
   private static final ELogger LOG = ELoggerFactory.getLogger(FlowBasedIamSessionSerializerImpl.class);
   private final Provider<UserSessionMapifier> userSessionMapifier;
   private final Provider<StateMachineSerializer> stateMachineSerializer;
   private final Provider<IamVersion> iamVersionProvider;

   @Inject
   public FlowBasedIamSessionSerializerImpl(
      Provider<UserSessionMapifier> userSessionMapifier, Provider<StateMachineSerializer> stateMachineSerializer, Provider<IamVersion> iamVersionProvider
   ) {
      this.userSessionMapifier = userSessionMapifier;
      this.stateMachineSerializer = stateMachineSerializer;
      this.iamVersionProvider = iamVersionProvider;
   }

   @Override
   public String serializeSession(FlowBasedIamSession session) {
      if (!session.isLockAcquired()) {
         throw new FailureException("Session should never be serialized if lock could not be acquired");
      }

      long start = System.currentTimeMillis();
      String serialized = new FlowBasedIamSessionValues(
            Optional.of(session.sessionId()),
            session.sessionNonces().orElseThrow(() -> new FailureException("Cannot serialize IAM session without nonces")),
            this.mapifiedUserSession(session.getLazyUserSession()),
            session.getStateMachine().filter(s -> this.containsActiveFlowSession(session)).map(this::serializedStateMachine),
            (IamVersion)this.iamVersionProvider.get(),
            session.getConfigurationContext().orElseGet(ConfigurationContext::defaultContext)
         )
         .serialize();
      LOG.debug("Serialized IAM session in " + (System.currentTimeMillis() - start) + "ms");
      return serialized;
   }

   private boolean containsActiveFlowSession(FlowBasedIamSession session) {
      LazyUserSession userSession = session.getLazyUserSession();
      if (userSession.getSupplier().isEmpty()) {
         return false;
      } else {
         return userSession.hasBeenSupplied()
            ? hasActiveFlowSession(session)
            : ((UserSessionMapifier)this.userSessionMapifier.get()).containsActiveFlowSession(userSession.getSupplier().orElseThrow().getRawValue());
      }
   }

   private static boolean hasActiveFlowSession(FlowBasedIamSession session) {
      return flowSession(session).map(fs -> fs.getStatus().isBefore(FlowSessionStatus.TERMINATED)).orElse(false);
   }

   private String serializedStateMachine(Supplier<FlowStateMachine> machineProvider) {
      return machineProvider instanceof LazySupplier<?, FlowStateMachine> lazySupplier && !lazySupplier.hasBeenSupplied()
         ? (String)lazySupplier.getRawValue()
         : ((StateMachineSerializer)this.stateMachineSerializer.get()).serializeStateMachine(machineProvider.get());
   }

   private MapifiedValues mapifiedUserSession(LazyUserSession lazyUserSession) {
      LazySupplier<MapifiedValues, UserSessionImpl> provider = lazyUserSession.getSupplier().orElseThrow();
      return provider.hasBeenSupplied() ? ((UserSessionMapifier)this.userSessionMapifier.get()).mapifyUserSession(provider.get()) : provider.getRawValue();
   }

   @Override
   public Result<FlowBasedIamSession, FlowBasedIamSessionSerializer.DeserializationFailedResult> deserializeSession(
      Optional<SessionStateId> sessionStateId, String serialized
   ) {
      try {
         IamVersion iamVersion = (IamVersion)this.iamVersionProvider.get();
         MapifiedValues values = fromJson(serialized);
         IamVersion dataVersion = IamVersion.fromString(values.get("iamVersion"));
         SessionId sessionId = new SessionId(values.get("sessionId"));
         if (dataVersion.compareTo(iamVersion) > 0) {
            return Result.failure(
               FlowBasedIamSessionSerializer.DeserializationFailedResult.sessionNotDeserializable(
                  "The serialized session with ID %s has a newer version (%s) than the current IAM version (%s). Discarding session."
                     .formatted(LogUtil.logSafeQuote(sessionId), dataVersion.toFullVersionString(), iamVersion.toFullVersionString())
               )
            );
         }

         boolean isSameVersion = dataVersion.equals(iamVersion);
         LazyUserSession userSession = this.extractUserSession(values, isSameVersion);
         ConfigurationContext context = demapifyConfigurationContext(values).orElse(ConfigurationContext.defaultContext());
         FlowBasedIamSession session = sessionStateId.isPresent()
            ? FlowBasedIamSession.createRestoredIamSession(
               sessionId, sessionStateId.get(), SessionNoncesMapifier.demapifyNonces(values.getValues("nonces")), context, userSession
            )
            : FlowBasedIamSession.createRestoredFromTicket(() -> sessionId, context, userSession);
         boolean hasFlowSession = values.getOptionalValues("userSession")
            .map(((UserSessionMapifier)this.userSessionMapifier.get())::containsActiveFlowSession)
            .orElse(false);
         if (hasFlowSession && !isSameVersion) {
            return Result.failure(
               FlowBasedIamSessionSerializer.DeserializationFailedResult.flowFromOldVersion("Aborting running flow during IAM update.", session)
            );
         }

         if (hasFlowSession && values.containsKey("stateMachine")) {
            String serializedMachine = values.get("stateMachine");
            session.setStateMachine(
               new LazySupplier<>(
                  serializedMachine,
                  rawValue -> ((StateMachineSerializer)this.stateMachineSerializer.get()).deserializeStateMachine(rawValue, flowSession(session).get())
               )
            );
         }

         return Result.result(session);
      } catch (RuntimeException e) {
         return Result.failure(FlowBasedIamSessionSerializer.DeserializationFailedResult.sessionNotDeserializable(e.getMessage()));
      }
   }

   private LazyUserSession extractUserSession(MapifiedValues values, boolean isSameVersion) {
      if (values.containsKey("userSession")) {
         MapifiedValues userSessionValues = values.getValues("userSession");
         boolean hasActiveFlow = ((UserSessionMapifier)this.userSessionMapifier.get()).containsActiveFlowSession(userSessionValues);
         if (hasActiveFlow && !isSameVersion) {
            userSessionValues = UserSessionMapifierImpl.withoutFlowSession(userSessionValues);
         }

         Optional<UserId> userId = UserSessionMapifierImpl.extractUserId(userSessionValues);
         return LazyUserSession.createLazyUserSession(
            userId, new LazySupplier<>(userSessionValues, ((UserSessionMapifier)this.userSessionMapifier.get())::demapifyUserSession)
         );
      } else {
         return LazyUserSession.alreadySupplied(new UserSessionImpl());
      }
   }

   private static Optional<InternalFlowSession> flowSession(FlowBasedIamSession session) {
      return session.getUserSession().getCurrentFlowSession();
   }

   private static MapifiedValues fromJson(String json) {
      try {
         return new MapifiedValues(JsonCoding.fromJson(json));
      } catch (ViolationException e) {
         throw new FailureException(e);
      }
   }

   private static Optional<ConfigurationContext> demapifyConfigurationContext(MapifiedValues values) {
      return values.transformOptional("context", ConfigurationContext::configurationContext).or(() -> Optional.of(ConfigurationContext.defaultContext()));
   }
}
