package com.airlock.iam.common.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.misc.log.LogUtil;
import com.airlock.iam.common.application.configuration.state.IamSessionSameSitePolicy;
import com.airlock.iam.common.domain.model.session.IamSession;
import com.airlock.iam.common.domain.model.session.ReadOnlyIamSessionData;
import com.airlock.iam.common.domain.model.session.SessionNonces;
import com.airlock.iam.common.domain.service.session.IamSessionRepository;
import com.airlock.iam.common.domain.service.session.IamSessionService;
import com.airlock.iam.common.domain.service.session.SessionTicketRestore;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.Optional;

public final class IamSessionServiceImpl<T extends IamSession> implements IamSessionService<T> {
   private static final ELogger LOG = ELoggerFactory.getLogger(IamSessionServiceImpl.class);
   private final Provider<HttpServletRequest> request;
   private final Provider<HttpServletResponse> response;
   private final Provider<IamSessionRepository<T>> sessionRepository;
   private final Provider<SessionTicketRestore<T>> sessionTicketRestore;
   private final Provider<IamSessionSameSitePolicy> sameSitePolicy;

   @Inject
   public IamSessionServiceImpl(
      Provider<HttpServletRequest> request,
      Provider<HttpServletResponse> response,
      Provider<IamSessionRepository<T>> sessionRepository,
      Provider<SessionTicketRestore<T>> sessionTicketRestore,
      Provider<IamSessionSameSitePolicy> sameSitePolicy
   ) {
      this.request = request;
      this.response = response;
      this.sessionRepository = sessionRepository;
      this.sessionTicketRestore = sessionTicketRestore;
      this.sameSitePolicy = sameSitePolicy;
   }

   @Override
   public T retrieveOrCreateSession() {
      return this.retrieveSession().orElseGet(this::createNewSession);
   }

   private Optional<T> retrieveSession() {
      Optional<IamSessionCookieHelper.IamSessionCookie> cookie = IamSessionCookieHelper.readIamSessionCookie((HttpServletRequest)this.request.get());
      if (cookie.isEmpty()) {
         LOG.debug("No session ID cookie found in request.");
         return this.tryRestoringSessionFromTicket();
      }

      Optional<T> session = ((IamSessionRepository)this.sessionRepository.get()).retrieveSession(cookie.get().sessionStateId());
      if (session.isEmpty()) {
         LOG.info("No session found for session ID from cookie.");
         return this.tryRestoringSessionFromTicket();
      }

      T existingSession = session.get();
      if (!existingSession.isLockAcquired()) {
         LOG.info("Lock could not be acquired for current IAM session");
         return session;
      }

      if (!existingSession.sessionNonces().map(nonces -> nonces.isValid(cookie.get().nonce())).orElse(false)) {
         LOG.info("Session ID nonce in cookie is not valid, ignoring session ID cookie.");
         ((IamSessionRepository)this.sessionRepository.get()).unlockSession(existingSession.sessionStateId());
         return Optional.empty();
      }

      Optional<UserId> userId = existingSession.userId();
      if (userId.isPresent()) {
         LOG.debug(
            "Found IAM session with ID " + existingSession.sessionId().id() + " in state repository for user " + LogUtil.logSafeQuote(userId.get()) + "."
         );
      } else {
         LOG.debug("Found IAM session with ID " + existingSession.sessionId().id() + " in state repository for unidentified user.");
      }

      session.ifPresent(this::removeExpiredNonces);
      return session;
   }

   private T createNewSession() {
      LOG.info("Created new IAM session");
      return (T)((IamSessionRepository)this.sessionRepository.get()).createNewSession();
   }

   private Optional<T> tryRestoringSessionFromTicket() {
      return ((SessionTicketRestore)this.sessionTicketRestore.get()).restoreSessionFromTicket();
   }

   @Override
   public Optional<ReadOnlyIamSessionData> retrieveReadOnlySessionData() {
      Optional<IamSessionCookieHelper.IamSessionCookie> cookie = IamSessionCookieHelper.readIamSessionCookie((HttpServletRequest)this.request.get());
      if (cookie.isEmpty()) {
         LOG.debug("No session ID cookie found in request.");
         return ((SessionTicketRestore)this.sessionTicketRestore.get()).extractReadOnlyDataFromSessionTicket();
      } else {
         Optional<ReadOnlyIamSessionData> data = ((IamSessionRepository)this.sessionRepository.get())
            .retrieveReadOnlySessionData(cookie.get().sessionStateId());
         if (data.isEmpty()) {
            LOG.info("No session found for session ID from cookie.");
            return ((SessionTicketRestore)this.sessionTicketRestore.get()).extractReadOnlyDataFromSessionTicket();
         } else {
            return data;
         }
      }
   }

   @Override
   public void updateSession(T session) {
      this.removeExpiredNonces(session);
      ((IamSessionRepository)this.sessionRepository.get()).updateSession(session);
      IamSessionCookieHelper.IamSessionCookie cookie = new IamSessionCookieHelper.IamSessionCookie(
         session.sessionStateId(),
         session.sessionNonces().map(SessionNonces::current).orElseThrow(() -> new FailureException("Cannot write cookie for IAM session without nonces"))
      );
      IamSessionCookieHelper.writeIamSessionCookie(
         cookie,
         (HttpServletRequest)this.request.get(),
         (HttpServletResponse)this.response.get(),
         ((IamSessionSameSitePolicy)this.sameSitePolicy.get()).getPolicy()
      );
   }

   private void removeExpiredNonces(T session) {
      session.sessionNonces().map(SessionNonces::withoutExpiredNonces).ifPresent(session::setSessionNonces);
   }

   @Override
   public void removeSession(T session) {
      ((IamSessionRepository)this.sessionRepository.get()).removeSession(session.sessionStateId());
      IamSessionCookieHelper.deleteIamSessionCookie((HttpServletRequest)this.request.get(), (HttpServletResponse)this.response.get());
   }

   @Override
   public void renewSessionNonce(IamSession session) {
      session.setSessionNonces(session.sessionNonces().orElseThrow().withRenewedNonce());
      LOG.info("Renewed session ID nonce.");
   }
}
