package com.airlock.iam.saml2.infrastructure.service.idp.logout;

import com.airlock.iam.base.api.domain.model.exception.ViolationException;
import com.airlock.iam.base.api.domain.model.user.UserId;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.common.infrastructure.logger.LogTypes;
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.log.api.helpdesk.HelpdeskLogger;
import com.airlock.iam.saml2.application.service.Saml2LogoutResumeRedirectService;
import com.airlock.iam.saml2.application.service.idp.Saml2IdpInitiatedStartSloService;
import com.airlock.iam.saml2.domain.model.Saml2Binding;
import com.airlock.iam.saml2.domain.model.idp.Saml2IdpMetaAlias;
import com.airlock.iam.saml2.domain.model.idp.logout.Saml2IdpInitiatedSloId;
import com.airlock.iam.saml2.domain.model.session.FlowFederationSession;
import com.airlock.iam.saml2.domain.model.session.FlowFederationSessionRepository;
import com.airlock.iam.saml2.infrastructure.idp.logout.Saml2IdpInitiatedSloFederationSessionCache;
import com.airlock.iam.saml2.infrastructure.request.Saml2Response;
import com.airlock.iam.saml2.infrastructure.service.idp.Saml2IdpFederationLibrary;
import jakarta.annotation.Nullable;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import jakarta.ws.rs.core.UriBuilder;
import java.net.URI;
import java.util.Collections;
import java.util.Optional;
import java.util.regex.Pattern;

public final class Saml2IdpInitiatedStartSloServiceImpl implements Saml2IdpInitiatedStartSloService {
   private static final ELogger LOG = ELoggerFactory.getLogger(Saml2IdpInitiatedStartSloServiceImpl.class);
   private final Provider<Saml2Response> response;
   private final Provider<Saml2LogoutResumeRedirectService> logoutResumeRedirectService;
   private final Provider<FlowFederationSessionRepository> sessionRepository;
   private final Provider<Saml2IdpFederationLibrary> library;
   private final Provider<Pattern> uiLogoutResumeUriPattern;
   private final Provider<Saml2IdpInitiatedSloFederationSessionCache> federationSessionCache;
   private final Provider<FlowFederationSessionRepository> federationSessionRepository;
   private final HelpdeskLogger helpdeskLogger;

   @Inject
   Saml2IdpInitiatedStartSloServiceImpl(
      Provider<Saml2Response> response,
      Provider<Saml2LogoutResumeRedirectService> logoutResumeRedirectService,
      Provider<FlowFederationSessionRepository> sessionRepository,
      Provider<Saml2IdpFederationLibrary> library,
      Provider<Pattern> uiLogoutResumeUriPattern,
      Provider<Saml2IdpInitiatedSloFederationSessionCache> federationSessionCache,
      Provider<FlowFederationSessionRepository> federationSessionRepository,
      HelpdeskLogger helpdeskLogger
   ) {
      this.response = response;
      this.logoutResumeRedirectService = logoutResumeRedirectService;
      this.sessionRepository = sessionRepository;
      this.library = library;
      this.uiLogoutResumeUriPattern = uiLogoutResumeUriPattern;
      this.federationSessionCache = federationSessionCache;
      this.federationSessionRepository = federationSessionRepository;
      this.helpdeskLogger = helpdeskLogger;
   }

   @Override
   public Optional<URI> startIdpInitiatedSlo(Saml2IdpInitiatedSloId sloId) {
      Optional<FlowFederationSession> session = ((Saml2IdpInitiatedSloFederationSessionCache)this.federationSessionCache.get()).readOnce(sloId);
      if (session.isPresent()) {
         if (!session.get().isSingleLogoutInProgress()) {
            this.helpdeskLogger
               .log(
                  this.getUserId(),
                  "Unable to do IdP-initiated SLO, either user's FlowFederationSession has not yet been marked for SLO, or SLO has already been performed."
               );
            return Optional.of(((Saml2LogoutResumeRedirectService)this.logoutResumeRedirectService.get()).cleanupSessionsAndRedirectToUiLogoutResumeUri());
         } else if (!session.get().isIdpInitiatedSingleLogoutInProgressOnIdp()) {
            this.logWrongSloTypeInProgress();
            return Optional.of(((Saml2LogoutResumeRedirectService)this.logoutResumeRedirectService.get()).cleanupSessionsAndRedirectToUiLogoutResumeUri());
         } else {
            session.get().setBlockRestRequests(true);
            ((FlowFederationSessionRepository)this.federationSessionRepository.get()).updateFederationSession(session.get());
            Optional<Saml2IdpMetaAlias> metaAlias = session.get()
               .getValues("IDPMetaAlias")
               .orElse(Collections.emptyList())
               .stream()
               .findFirst()
               .map(Saml2IdpMetaAlias::idpMetaAlias);
            if (metaAlias.isEmpty()) {
               LOG.warn(LogTypes.REQUEST_ERROR, "SAML2 IdP SLO: No IDPMetaAlias found. Logging out.");
               return Optional.of(((Saml2LogoutResumeRedirectService)this.logoutResumeRedirectService.get()).cleanupSessionsAndRedirectToUiLogoutResumeUri());
            } else {
               Saml2Binding sloBinding = ((Saml2IdpFederationLibrary)this.library.get()).doIdpInitiatedSlo(metaAlias.get());
               return this.isSloComplete(sloBinding)
                  ? Optional.of(((Saml2LogoutResumeRedirectService)this.logoutResumeRedirectService.get()).cleanupSessionsAndRedirectToUiLogoutResumeUri())
                  : Optional.empty();
            }
         }
      } else {
         this.helpdeskLogger.log(this.getUserId(), "Unable to do IdP initiated SLO, user's FlowFederationSession not available anymore.");
         return Optional.of(((Saml2LogoutResumeRedirectService)this.logoutResumeRedirectService.get()).cleanupSessionsAndRedirectToUiLogoutResumeUri());
      }
   }

   private void logWrongSloTypeInProgress() {
      this.helpdeskLogger
         .log(
            this.getUserId(),
            "Request received to do an IdP-initiated SLO (single logout) but another type of SLO has already been started on this session before. This looks like a different SLO process has been interrupted (e.g. a SAML SP failed to redirect the client back to us). Continuing logout."
         );
   }

   @Override
   public void validateAndStoreUiLogoutResumeUri(String uiLogoutResumeUriParameter) {
      boolean valid = ((Pattern)this.uiLogoutResumeUriPattern.get()).matcher(uiLogoutResumeUriParameter).matches();
      if (!valid) {
         String message = String.format(
            "SAML2 Idp initiated SLO: Validation of location parameter failed. Parameter value %s does not match the validation pattern %s.",
            LogUtil.logSafeQuote(uiLogoutResumeUriParameter),
            LogUtil.logSafeQuote(((Pattern)this.uiLogoutResumeUriPattern.get()).pattern())
         );
         throw new ViolationException(message);
      }

      URI uiLogoutResumeUri = UriBuilder.fromUri(uiLogoutResumeUriParameter).build(new Object[0]);
      if (!uiLogoutResumeUri.isAbsolute()) {
         throw new ViolationException("SAML2 Idp initiated SLO: Logout resume URI must be absolute.");
      }

      ((Saml2LogoutResumeRedirectService)this.logoutResumeRedirectService.get()).storeUiLogoutResumeUri(uiLogoutResumeUri);
   }

   boolean isSloComplete(Saml2Binding sloBinding) {
      if ("urn:oasis:names:tc:SAML:2.0:bindings:SOAP".equalsIgnoreCase(sloBinding.value())) {
         LOG.info(EDefaultLogType.GENERIC_LOG_TYPE, "SAML2 Idp initiated SLO: Successfully logged out user using SOAP messages.");
         return true;
      } else if (((Saml2Response)this.response.get()).isHandledBySaml2Library()) {
         LOG.info(EDefaultLogType.GENERIC_LOG_TYPE, "SAML2 Idp initiated SLO: Redirected to next SP for logout.");
         return false;
      } else {
         return true;
      }
   }

   @Nullable
   private UserId getUserId() {
      return ((FlowFederationSessionRepository)this.sessionRepository.get())
         .getFederationSession()
         .flatMap(FlowFederationSession::getAuthenticatedUserId)
         .orElse(null);
   }
}
