package com.airlock.iam.flow.shared.domain.service.airlock2fa;

import com.airlock.iam.airlock2fa.api.domain.model.Airlock2FAAccountId;
import com.airlock.iam.airlock2fa.api.domain.model.Airlock2FADeviceId;
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.base.misc.result.Result;
import com.airlock.iam.base.misc.time.TimeProvider;
import com.airlock.iam.common.api.domain.model.user.User;
import com.airlock.iam.common.domain.model.principal.UserPrincipal;
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.factor.domain.model.airlock2fa.Airlock2FALogData;
import com.airlock.iam.factor.domain.model.airlock2fa.Airlock2FAShortLivedOnlineQrCodeSettings;
import com.airlock.iam.factor.domain.model.airlock2fa.Airlock2FAUser;
import com.airlock.iam.factor.domain.model.airlock2fa.activation.Airlock2FAActivationCode;
import com.airlock.iam.factor.domain.model.airlock2fa.activation.Airlock2FAActivationCodeShort;
import com.airlock.iam.factor.domain.model.airlock2fa.activation.Airlock2FAActivationSession;
import com.airlock.iam.factor.domain.model.airlock2fa.activation.Airlock2FAActivationSettings;
import com.airlock.iam.factor.domain.model.airlock2fa.event.Airlock2FADeviceEventFactory;
import com.airlock.iam.factor.domain.model.airlock2fa.futurae.FuturaeErrorResponseException;
import com.airlock.iam.factor.domain.model.airlock2fa.qrcode.Airlock2FAExchangeTokensAnswer;
import com.airlock.iam.factor.domain.service.airlock2fa.Airlock2FAAuthApiDeviceManagementService;
import com.airlock.iam.factor.domain.service.airlock2fa.Airlock2FAUrlConverterService;
import com.airlock.iam.factor.domain.service.airlock2fa.QrCodeImageReader;
import com.airlock.iam.factor.domain.service.airlock2fa.activation.Airlock2FAActivationService;
import com.airlock.iam.factor.domain.service.airlock2fa.activation.Airlock2FAFlowBindingService;
import com.airlock.iam.factor.domain.service.airlock2fa.displayname.Airlock2FAAccountDisplayNameProvider;
import com.airlock.iam.factor.domain.service.airlock2fa.enrollment.Airlock2FAAuthApiEnrollmentService;
import com.airlock.iam.factor.domain.service.airlock2fa.repository.Airlock2FAUserRepository;
import com.airlock.iam.flow.api.domain.model.session.FlowAttributeRepository;
import com.airlock.iam.flow.shared.domain.model.airlock2fa.activation.Airlock2FAEnrolledDevicesFlowAttribute;
import com.airlock.iam.log.api.helpdesk.HelpdeskLogger;
import com.airlock.iam.log.api.signing.SigningLogger;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.Optional;

public class Airlock2FADefaultActivationService implements Airlock2FAActivationService {
   private static final ELogger LOG = ELoggerFactory.getLogger(Airlock2FADefaultActivationService.class);
   private final Provider<Airlock2FAAuthApiEnrollmentService> enrollmentService;
   private final Provider<Airlock2FAFlowBindingService> flowBindingService;
   private final Provider<Airlock2FAAuthApiDeviceManagementService> deviceManagementService;
   private final Provider<Airlock2FAUrlConverterService> urlConverterService;
   private final Provider<Airlock2FAUserRepository> airlock2FAUserRepository;
   private final Provider<QrCodeImageReader> qrCodeReader;
   private final Provider<Airlock2FAAccountDisplayNameProvider> displayNameProvider;
   private final Provider<UserPrincipal> user;
   private final Provider<Airlock2FADeviceEventFactory> eventFactory;
   private final Provider<FlowAttributeRepository> flowAttributes;
   private final SigningLogger signingLogger;
   private final HelpdeskLogger helpdeskLogger;
   private final Provider<TimeProvider> timeProvider;

   @Inject
   public Airlock2FADefaultActivationService(
      Provider<Airlock2FAAuthApiEnrollmentService> enrollmentService,
      Provider<Airlock2FAFlowBindingService> flowBindingService,
      Provider<Airlock2FAAuthApiDeviceManagementService> deviceManagementService,
      Provider<Airlock2FAUrlConverterService> urlConverterService,
      Provider<Airlock2FAUserRepository> airlock2FAUserRepository,
      Provider<QrCodeImageReader> qrCodeReader,
      Provider<Airlock2FAAccountDisplayNameProvider> displayNameProvider,
      Provider<UserPrincipal> user,
      Provider<Airlock2FADeviceEventFactory> eventFactory,
      Provider<FlowAttributeRepository> flowAttributes,
      SigningLogger signingLogger,
      HelpdeskLogger helpdeskLogger,
      Provider<TimeProvider> timeProvider
   ) {
      this.enrollmentService = enrollmentService;
      this.flowBindingService = flowBindingService;
      this.deviceManagementService = deviceManagementService;
      this.urlConverterService = urlConverterService;
      this.airlock2FAUserRepository = airlock2FAUserRepository;
      this.qrCodeReader = qrCodeReader;
      this.displayNameProvider = displayNameProvider;
      this.user = user;
      this.eventFactory = eventFactory;
      this.flowAttributes = flowAttributes;
      this.signingLogger = signingLogger;
      this.helpdeskLogger = helpdeskLogger;
      this.timeProvider = timeProvider;
   }

   @Override
   public Airlock2FAActivationSession startActivation(Airlock2FAActivationSettings activationSettings, Duration flowBindingTokenValidity) {
      Optional<Airlock2FAUser> airlock2FAUser = ((Airlock2FAUserRepository)this.airlock2FAUserRepository.get()).findBy(this.userId());
      Airlock2FAAuthApiEnrollmentService.Airlock2FAEnrollmentSession enrollmentSession = this.startEnrollment(airlock2FAUser, activationSettings);
      this.log(
         "Airlock 2FA enrollment started",
         Airlock2FALogData.airlock2FALogData(enrollmentSession.getAccountId())
            .withProvideActivationCodeShortEnabled(activationSettings.provideActivationCodeShortEnabled())
            .withEnrollmentId(enrollmentSession.getEnrollmentId())
            .withActivationSettings(activationSettings)
      );
      Instant activationStartTime = ((TimeProvider)this.timeProvider.get()).getCurrentInstant();
      Optional<Airlock2FAExchangeTokensAnswer> tokenAnswer = activationSettings.shortLivedQrCodeSettings()
         .map(settings -> this.fetchExchangeTokens(settings, enrollmentSession));
      return this.convertToActivationSession(activationSettings, flowBindingTokenValidity, enrollmentSession, tokenAnswer, activationStartTime);
   }

   private Airlock2FAAuthApiEnrollmentService.Airlock2FAEnrollmentSession startEnrollment(
      Optional<Airlock2FAUser> airlock2FAUser, Airlock2FAActivationSettings activationSettings
   ) {
      if (airlock2FAUser.isPresent()) {
         return ((Airlock2FAAuthApiEnrollmentService)this.enrollmentService.get())
            .startEnrollmentForExistingUser(airlock2FAUser.get().getAccountId(), activationSettings);
      }

      Airlock2FAAuthApiEnrollmentService.Airlock2FAEnrollmentSession enrollmentSession = ((Airlock2FAAuthApiEnrollmentService)this.enrollmentService.get())
         .startEnrollmentForNewUser(
            ((Airlock2FAAccountDisplayNameProvider)this.displayNameProvider.get()).accountDisplayName((User)this.user.get()), activationSettings
         );
      Airlock2FAAccountId accountId = enrollmentSession.getAccountId();
      ((Airlock2FAUserRepository)this.airlock2FAUserRepository.get()).insert(Airlock2FAUser.airlock2FAUser(this.userId(), accountId));
      return enrollmentSession;
   }

   private Airlock2FAActivationSession convertToActivationSession(
      Airlock2FAActivationSettings activationSettings,
      Duration flowBindingTokenValidity,
      Airlock2FAAuthApiEnrollmentService.Airlock2FAEnrollmentSession enrollmentSession,
      Optional<Airlock2FAExchangeTokensAnswer> tokenAnswer,
      Instant activationStartTime
   ) {
      Airlock2FAActivationCode activationCode = enrollmentSession.getActivationCode();
      return new Airlock2FAActivationSession(
         enrollmentSession.getAccountId(),
         activationCode,
         Optional.ofNullable(enrollmentSession.getActivationCodeShort()),
         enrollmentSession.getAppDeviceActivationUrl(),
         ((QrCodeImageReader)this.qrCodeReader.get()).retrieveFrom(enrollmentSession.getActivationQrCodeUrl().getUrl()),
         enrollmentSession.getEnrollmentId(),
         Optional.of(activationCode)
            .filter(code -> activationSettings.activationFlowBindingEnabled())
            .map(code -> ((Airlock2FAFlowBindingService)this.flowBindingService.get()).flowBindingToken(code, flowBindingTokenValidity)),
         tokenAnswer.map(Airlock2FAExchangeTokensAnswer::getExchangeTokens).orElse(List.of()),
         activationStartTime
      );
   }

   @Override
   public Airlock2FAAuthApiEnrollmentService.Airlock2FAEnrollmentStatus checkEnrollmentStatus(
      Airlock2FAAccountId accountId, Airlock2FAActivationCode activationCode
   ) {
      return ((Airlock2FAAuthApiEnrollmentService)this.enrollmentService.get()).checkEnrollmentStatus(accountId, activationCode);
   }

   @Override
   public Result<Airlock2FAAuthApiEnrollmentService.Airlock2FAEnrollmentStatus, FuturaeErrorResponseException> tryCheckEnrollmentStatus(
      Airlock2FAAccountId accountId, Airlock2FAActivationCode activationCode
   ) {
      return ((Airlock2FAAuthApiEnrollmentService)this.enrollmentService.get()).tryCheckEnrollmentStatus(accountId, activationCode);
   }

   @Override
   public void handleSuccess(Airlock2FAAccountId accountId, Airlock2FADeviceId deviceId, Airlock2FALogData logData) {
      ((FlowAttributeRepository)this.flowAttributes.get())
         .putOrMergeAttribute(Airlock2FAEnrolledDevicesFlowAttribute.airlock2FAEnrolledDevicesFlowAttribute(deviceId));
      ((Airlock2FADeviceEventFactory)this.eventFactory.get()).publishAirlock2FADeviceActivatedEvent(this.userId(), accountId, deviceId);
      this.log("Successfully activated Airlock 2FA device", logData);
   }

   @Override
   public void handleExpired(Airlock2FALogData logData) {
      this.log("Failed to activate Airlock 2FA device because the enrollment session has expired", logData);
   }

   @Override
   public void handleFailed(Airlock2FALogData logData) {
      String message = "Failed to activate Airlock 2FA device";
      this.log(message, logData);
      LOG.warn(EDefaultLogType.GENERIC_LOG_TYPE, message);
   }

   @Override
   public void handleSkipped(Airlock2FALogData logData) {
      this.log("Skipping Airlock 2FA activation since there are already devices available.", logData);
   }

   private Airlock2FAExchangeTokensAnswer fetchExchangeTokens(
      Airlock2FAShortLivedOnlineQrCodeSettings settings, Airlock2FAAuthApiEnrollmentService.Airlock2FAEnrollmentSession enrollmentSession
   ) {
      Airlock2FAActivationCodeShort activationCodeShort = enrollmentSession.getActivationCodeShort();
      if (activationCodeShort == null) {
         throw new ViolationException("Short activation code is not present. This must be enabled to use short-lived Online QR Codes during enrollment.");
      } else {
         return ((Airlock2FAAuthApiEnrollmentService)this.enrollmentService.get())
            .fetchEnrollmentExchangeTokens(
               enrollmentSession.getAccountId(),
               activationCodeShort,
               settings.tokenLifetime(),
               settings.exchangeTokenCount(),
               enrollmentSession.getAppDeviceActivationUrl().getUriScheme(),
               enrollmentSession.getUniversalLink().getDomain()
            );
      }
   }

   private void log(String message, Airlock2FALogData logData) {
      this.signingLogger.log(String.format("%s (for user %s) %s", message, LogUtil.quote(this.userId()), logData));
      this.helpdeskLogger.log(this.userId(), message + " " + logData);
   }

   private UserId userId() {
      return ((UserPrincipal)this.user.get()).getId();
   }

   @SuppressFBWarnings(justification = "generated code")
   public static Airlock2FADefaultActivationService.Airlock2FADefaultActivationServiceBuilder builder() {
      return new Airlock2FADefaultActivationService.Airlock2FADefaultActivationServiceBuilder();
   }

   @SuppressFBWarnings(justification = "generated code")
   public static class Airlock2FADefaultActivationServiceBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private Provider<Airlock2FAAuthApiEnrollmentService> enrollmentService;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<Airlock2FAFlowBindingService> flowBindingService;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<Airlock2FAAuthApiDeviceManagementService> deviceManagementService;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<Airlock2FAUrlConverterService> urlConverterService;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<Airlock2FAUserRepository> airlock2FAUserRepository;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<QrCodeImageReader> qrCodeReader;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<Airlock2FAAccountDisplayNameProvider> displayNameProvider;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<UserPrincipal> user;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<Airlock2FADeviceEventFactory> eventFactory;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<FlowAttributeRepository> flowAttributes;
      @SuppressFBWarnings(justification = "generated code")
      private SigningLogger signingLogger;
      @SuppressFBWarnings(justification = "generated code")
      private HelpdeskLogger helpdeskLogger;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<TimeProvider> timeProvider;

      @SuppressFBWarnings(justification = "generated code")
      Airlock2FADefaultActivationServiceBuilder() {
      }

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FADefaultActivationService.Airlock2FADefaultActivationServiceBuilder enrollmentService(
         Provider<Airlock2FAAuthApiEnrollmentService> enrollmentService
      ) {
         this.enrollmentService = enrollmentService;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FADefaultActivationService.Airlock2FADefaultActivationServiceBuilder flowBindingService(
         Provider<Airlock2FAFlowBindingService> flowBindingService
      ) {
         this.flowBindingService = flowBindingService;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FADefaultActivationService.Airlock2FADefaultActivationServiceBuilder deviceManagementService(
         Provider<Airlock2FAAuthApiDeviceManagementService> deviceManagementService
      ) {
         this.deviceManagementService = deviceManagementService;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FADefaultActivationService.Airlock2FADefaultActivationServiceBuilder urlConverterService(
         Provider<Airlock2FAUrlConverterService> urlConverterService
      ) {
         this.urlConverterService = urlConverterService;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FADefaultActivationService.Airlock2FADefaultActivationServiceBuilder airlock2FAUserRepository(
         Provider<Airlock2FAUserRepository> airlock2FAUserRepository
      ) {
         this.airlock2FAUserRepository = airlock2FAUserRepository;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FADefaultActivationService.Airlock2FADefaultActivationServiceBuilder qrCodeReader(Provider<QrCodeImageReader> qrCodeReader) {
         this.qrCodeReader = qrCodeReader;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FADefaultActivationService.Airlock2FADefaultActivationServiceBuilder displayNameProvider(
         Provider<Airlock2FAAccountDisplayNameProvider> displayNameProvider
      ) {
         this.displayNameProvider = displayNameProvider;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FADefaultActivationService.Airlock2FADefaultActivationServiceBuilder user(Provider<UserPrincipal> user) {
         this.user = user;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FADefaultActivationService.Airlock2FADefaultActivationServiceBuilder eventFactory(Provider<Airlock2FADeviceEventFactory> eventFactory) {
         this.eventFactory = eventFactory;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FADefaultActivationService.Airlock2FADefaultActivationServiceBuilder flowAttributes(Provider<FlowAttributeRepository> flowAttributes) {
         this.flowAttributes = flowAttributes;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FADefaultActivationService.Airlock2FADefaultActivationServiceBuilder signingLogger(SigningLogger signingLogger) {
         this.signingLogger = signingLogger;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FADefaultActivationService.Airlock2FADefaultActivationServiceBuilder helpdeskLogger(HelpdeskLogger helpdeskLogger) {
         this.helpdeskLogger = helpdeskLogger;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FADefaultActivationService.Airlock2FADefaultActivationServiceBuilder timeProvider(Provider<TimeProvider> timeProvider) {
         this.timeProvider = timeProvider;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FADefaultActivationService build() {
         return new Airlock2FADefaultActivationService(
            this.enrollmentService,
            this.flowBindingService,
            this.deviceManagementService,
            this.urlConverterService,
            this.airlock2FAUserRepository,
            this.qrCodeReader,
            this.displayNameProvider,
            this.user,
            this.eventFactory,
            this.flowAttributes,
            this.signingLogger,
            this.helpdeskLogger,
            this.timeProvider
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "Airlock2FADefaultActivationService.Airlock2FADefaultActivationServiceBuilder(enrollmentService="
            + this.enrollmentService
            + ", flowBindingService="
            + this.flowBindingService
            + ", deviceManagementService="
            + this.deviceManagementService
            + ", urlConverterService="
            + this.urlConverterService
            + ", airlock2FAUserRepository="
            + this.airlock2FAUserRepository
            + ", qrCodeReader="
            + this.qrCodeReader
            + ", displayNameProvider="
            + this.displayNameProvider
            + ", user="
            + this.user
            + ", eventFactory="
            + this.eventFactory
            + ", flowAttributes="
            + this.flowAttributes
            + ", signingLogger="
            + this.signingLogger
            + ", helpdeskLogger="
            + this.helpdeskLogger
            + ", timeProvider="
            + this.timeProvider
            + ")";
      }
   }
}
