package com.airlock.iam.flow.shared.domain.model.cronto;

import com.airlock.iam.base.api.domain.model.plugin.LogicForConfig;
import com.airlock.iam.base.api.domain.model.user.UserId;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.common.domain.model.user.PrincipalInfo;
import com.airlock.iam.core.misc.cronto.CrontoHandler;
import com.airlock.iam.core.misc.impl.authen.CrontoUsedDeviceInfo;
import com.airlock.iam.core.misc.impl.cronto.pushnotification.PushAppHandlerWithActivation;
import com.airlock.iam.core.misc.impl.token.cronto.CrontoDeviceToken;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.flow.api.domain.model.step.StepResult;
import com.airlock.iam.flow.api.domain.model.step.StepSession;
import com.airlock.iam.flow.shared.application.configuration.cronto.CrontoPushActivationStepConfig;
import com.airlock.iam.flow.shared.domain.service.PollingService;
import com.airlock.iam.log.api.helpdesk.HelpdeskLogger;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

@LogicForConfig(CrontoPushActivationStepConfig.class)
public final class CrontoPushActivationStepImpl implements CrontoPushActivationStep {
   private static final ELogger LOG = ELoggerFactory.getLogger(CrontoPushActivationStepImpl.class);
   private static final String CHALLENGE_ATTRIBUTE = "pushActivationChallenge";
   private final Provider<Optional<CrontoUsedDeviceInfo>> usedDevice;
   private final Provider<PrincipalInfo> user;
   private final Provider<CrontoPushActivationStepSession> session;
   private final Provider<CrontoHandler> crontoHandler;
   private final Provider<PushAppHandlerWithActivation> pushAppHandler;
   private final HelpdeskLogger helpdeskLogger;
   private final PollingService<CrontoPushActivationStepSession, StepResult> pollingService;

   @Inject
   public CrontoPushActivationStepImpl(
      Provider<Optional<CrontoUsedDeviceInfo>> usedDevice,
      Provider<PrincipalInfo> user,
      Provider<CrontoPushActivationStepSession> session,
      Provider<CrontoHandler> crontoHandler,
      Provider<PushAppHandlerWithActivation> pushAppHandler,
      HelpdeskLogger helpdeskLogger
   ) {
      this.usedDevice = usedDevice;
      this.user = user;
      this.crontoHandler = crontoHandler;
      this.pushAppHandler = pushAppHandler;
      this.session = session;
      this.helpdeskLogger = helpdeskLogger;
      this.pollingService = new PollingService<>(CrontoPushActivationStepSession::getPollingCache, this::checkCompletedInternal, this::activationPossibleResult);
   }

   @Override
   public Optional<StepSession> createSession() {
      return Optional.of(
         CrontoPushActivationStepSession.createNewSession(
            this.collectPushEnabledDevices(),
            CrontoPushActivationChallenge.of(((PushAppHandlerWithActivation)this.pushAppHandler.get()).generatePushActivationChallenge(this.userId()))
         )
      );
   }

   @Override
   public StepResult initialize() {
      LOG.info("Asking user " + LogUtil.quote(this.userId()) + " to activate Cronto push device.");
      return this.activationPossibleResult((CrontoPushActivationStepSession)this.session.get());
   }

   @Override
   public StepResult skip() {
      if (((Optional)this.usedDevice.get()).isEmpty()) {
         return this.stepResultFactory().skip().build();
      }

      this.helpdeskLogger.log(this.userId(), "Selected to skip Cronto push activation.");
      ((PushAppHandlerWithActivation)this.pushAppHandler.get())
         .updateTotalUsagesWhenPushActivationSkipped(this.userId(), ((CrontoUsedDeviceInfo)((Optional)this.usedDevice.get()).orElseThrow()).getDeviceId());
      return this.stepResultFactory().skip().build();
   }

   @Override
   public StepResult never() {
      if (((Optional)this.usedDevice.get()).isEmpty()) {
         return this.stepResultFactory().skip().build();
      }

      CrontoUsedDeviceInfo device = (CrontoUsedDeviceInfo)((Optional)this.usedDevice.get()).get();
      ((CrontoHandler)this.crontoHandler.get()).getPushDevicesHandler().disallowPushNotificationFor(this.loadDevice(device));
      this.helpdeskLogger.log(this.userId(), "Selected to never activate push for Cronto device " + LogUtil.quote(device.getDeviceId()));
      return this.stepResultFactory().skip().build();
   }

   @Override
   public StepResult checkCompleted(boolean queryOnly) {
      return this.pollingService.poll((CrontoPushActivationStepSession)this.session.get(), queryOnly);
   }

   private StepResult checkCompletedInternal(CrontoPushActivationStepSession stepSession) {
      Set<String> newlyActivated = new HashSet<>(this.collectPushEnabledDevices());
      newlyActivated.removeAll(stepSession.getAlreadyActivatedDevices());
      if (newlyActivated.isEmpty()) {
         return this.activationPossibleResult(stepSession);
      }

      for (String deviceId : newlyActivated) {
         this.helpdeskLogger.log(this.userId(), "Successfully activated push for Cronto device " + LogUtil.quote(deviceId));
      }

      return this.stepResultFactory().success().build();
   }

   private StepResult activationPossibleResult(CrontoPushActivationStepSession stepSession) {
      return this.stepResultFactory()
         .stay(CrontoPushActivationStep.CrontoPushActivationStepActions.CRONTO_PUSH_ACTIVATION_POSSIBLE)
         .addAdditionalAttribute("pushActivationChallenge", stepSession.getChallenge())
         .build();
   }

   @Override
   public StepResult retrievePushActivationChallenge() {
      return ((CrontoPushActivationStepSession)this.session.get()).getPollingCache().hasResult()
         ? this.stepResultFactory().failDueToUnexpectedCall("Call is not allowed after the final polling result is ready.").build()
         : this.stepResultFactory()
            .stay(CrontoPushActivationStep.CrontoPushActivationStepActions.CRONTO_PUSH_ACTIVATION_POSSIBLE)
            .resultData(((CrontoPushActivationStepSession)this.session.get()).getChallenge())
            .build();
   }

   @Override
   public String getDescription() {
      return "CrontoSign Swiss Push Activation Step";
   }

   private CrontoDeviceToken loadDevice(CrontoUsedDeviceInfo deviceInfo) {
      return ((CrontoHandler)this.crontoHandler.get()).getTokenByDeviceId(this.userId(), deviceInfo.getDeviceId());
   }

   private Set<String> collectPushEnabledDevices() {
      return ((CrontoHandler)this.crontoHandler.get())
         .getPushAppHandler()
         .getPushEnabledDevices(this.userId())
         .stream()
         .map(CrontoDeviceToken::getDeviceIdForDisplay)
         .collect(Collectors.toSet());
   }

   private UserId userId() {
      return ((PrincipalInfo)this.user.get()).getKnownIdentity().orElseThrow().id();
   }
}
