package com.airlock.iam.oauth2.domain.model.logoutaction;

import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.base.misc.result.Result;
import com.airlock.iam.common.domain.model.logoutaction.LogoutAction;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.google.common.annotations.VisibleForTesting;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

public final class OidcBackchannelLogoutAction implements LogoutAction {
   private static final ELogger LOG = ELoggerFactory.getLogger(OidcBackchannelLogoutAction.class);
   private final Provider<OidcBackchannelLogoutGenerator> tokenGenerator;
   private final Duration backchannelLogoutTimeout;

   @Inject
   public OidcBackchannelLogoutAction(Provider<OidcBackchannelLogoutGenerator> tokenGenerator) {
      this(tokenGenerator, Duration.ofSeconds(30L));
   }

   @VisibleForTesting
   OidcBackchannelLogoutAction(Provider<OidcBackchannelLogoutGenerator> tokenGenerator, Duration backchannelLogoutTimeout) {
      this.tokenGenerator = tokenGenerator;
      this.backchannelLogoutTimeout = backchannelLogoutTimeout;
   }

   @Override
   public boolean isApplicable() {
      return true;
   }

   @Override
   public void execute() {
      List<BackchannelLogout> backchannelLogouts = ((OidcBackchannelLogoutGenerator)this.tokenGenerator.get()).generateLogouts();

      try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
         scheduleBackchannelLogouts(backchannelLogouts, executor).forEach(this::logResult);
      }
   }

   private static List<OidcBackchannelLogoutAction.ScheduledBackchannelLogout> scheduleBackchannelLogouts(
      List<BackchannelLogout> backchannelLogouts, ExecutorService executor
   ) {
      return backchannelLogouts.stream()
         .map(
            backchannelLogout -> new OidcBackchannelLogoutAction.ScheduledBackchannelLogout(
               backchannelLogout, executor.submit(() -> Result.result(backchannelLogout))
            )
         )
         .toList();
   }

   private void logResult(OidcBackchannelLogoutAction.ScheduledBackchannelLogout scheduledBackchannelLogout) {
      Future<Result<String, Throwable>> result = scheduledBackchannelLogout.result;
      BackchannelLogout backchannelLogout = scheduledBackchannelLogout.backchannelLogout;

      try {
         Result<String, Throwable> backchannelLogoutResult = result.get(this.backchannelLogoutTimeout.getSeconds(), TimeUnit.SECONDS);
         if (backchannelLogoutResult.isFailure()) {
            LOG.info(backchannelLogoutResult.getFailure().getMessage());
         } else {
            LOG.info(backchannelLogoutResult.get());
         }
      } catch (ExecutionException e) {
         LOG.info(
            String.format(
               "Error while waiting for back-channel logout result of OP %s to RP %s at %s: %s",
               LogUtil.logSafeQuote(backchannelLogout.getOpenIdProvider()),
               LogUtil.logSafeQuote(backchannelLogout.getRelyingParty()),
               LogUtil.logSafeQuote(backchannelLogout.getBackchannelLogoutUri()),
               e.getMessage()
            )
         );
      } catch (InterruptedException e) {
         LOG.info(
            String.format(
               "Interrupted while waiting for back-channel logout result of OP %s to RP %s at %s.",
               LogUtil.logSafeQuote(backchannelLogout.getOpenIdProvider()),
               LogUtil.logSafeQuote(backchannelLogout.getRelyingParty()),
               LogUtil.logSafeQuote(backchannelLogout.getBackchannelLogoutUri())
            )
         );
         result.cancel(true);
      } catch (TimeoutException e) {
         LOG.info(
            String.format(
               "Timed out while waiting for back-channel logout of OP %s to RP %s at %s.",
               LogUtil.logSafeQuote(backchannelLogout.getOpenIdProvider()),
               LogUtil.logSafeQuote(backchannelLogout.getRelyingParty()),
               LogUtil.logSafeQuote(backchannelLogout.getBackchannelLogoutUri())
            )
         );
         result.cancel(true);
      }
   }

   private record ScheduledBackchannelLogout(BackchannelLogout backchannelLogout, Future<Result<String, Throwable>> result) {
   }
}
