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

import com.airlock.iam.airlock2fa.api.domain.model.Airlock2FAAccountId;
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.api.domain.model.user.User;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.elog.api.LogTypes;
import com.airlock.iam.factor.domain.model.airlock2fa.Airlock2FADeviceAndFactorApplicability;
import com.airlock.iam.factor.domain.model.airlock2fa.Airlock2FADeviceSettings;
import com.airlock.iam.factor.domain.model.airlock2fa.Airlock2FAFactor;
import com.airlock.iam.factor.domain.model.airlock2fa.Airlock2FAFactorSet;
import com.airlock.iam.factor.domain.model.airlock2fa.Airlock2FAPreAuthenticationData;
import com.airlock.iam.factor.domain.model.airlock2fa.Airlock2FAUser;
import com.airlock.iam.factor.domain.model.airlock2fa.UserStatus;
import com.airlock.iam.factor.domain.service.airlock2fa.Airlock2FAPreAuthenticationService;
import com.airlock.iam.factor.domain.service.airlock2fa.repository.Airlock2FAUserRepository;
import com.airlock.iam.flow.api.domain.model.step.FlowCondition;
import com.airlock.iam.flow.shared.application.configuration.condition.Airlock2FAHasDeviceConditionConfig;
import com.airlock.iam.log.api.helpdesk.HelpdeskLogger;
import com.airlock.iam.log.api.signing.SigningLogger;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.time.Duration;
import java.util.Optional;
import lombok.NonNull;

@LogicForConfig(Airlock2FAHasDeviceConditionConfig.class)
public final class Airlock2FAHasDeviceCondition implements FlowCondition {
   private static final ELogger LOG = ELoggerFactory.getLogger(Airlock2FAHasDeviceCondition.class);
   private final Provider<User> user;
   private final Provider<Airlock2FAFactorSet> enabledFactors;
   private final Provider<Airlock2FAUserRepository> repository;
   private final Provider<Airlock2FAPreAuthenticationService> service;
   private final Provider<SigningLogger> signingLogger;
   private final Provider<HelpdeskLogger> helpdeskLogger;
   private final Provider<Airlock2FAHasDeviceConditionSettings> settings;

   @Inject
   public Airlock2FAHasDeviceCondition(
      @NonNull Provider<User> user,
      @NonNull Provider<Airlock2FAFactorSet> enabledFactors,
      @NonNull Provider<Airlock2FAUserRepository> repository,
      @NonNull Provider<Airlock2FAPreAuthenticationService> service,
      @NonNull Provider<SigningLogger> signingLogger,
      @NonNull Provider<HelpdeskLogger> helpdeskLogger,
      @NonNull Provider<Airlock2FAHasDeviceConditionSettings> settings
   ) {
      if (user == null) {
         throw new NullPointerException("user is marked non-null but is null");
      }

      if (enabledFactors == null) {
         throw new NullPointerException("enabledFactors is marked non-null but is null");
      }

      if (repository == null) {
         throw new NullPointerException("repository is marked non-null but is null");
      }

      if (service == null) {
         throw new NullPointerException("service is marked non-null but is null");
      }

      if (signingLogger == null) {
         throw new NullPointerException("signingLogger is marked non-null but is null");
      }

      if (helpdeskLogger == null) {
         throw new NullPointerException("helpdeskLogger is marked non-null but is null");
      }

      if (settings == null) {
         throw new NullPointerException("settings is marked non-null but is null");
      }

      this.user = user;
      this.enabledFactors = enabledFactors;
      this.repository = repository;
      this.service = service;
      this.signingLogger = signingLogger;
      this.helpdeskLogger = helpdeskLogger;
      this.settings = settings;
   }

   @Override
   public boolean isFulfilled() {
      String iamUsername = ((User)this.user.get()).getName();
      Optional<Airlock2FAAccountId> accountId = this.findAirlock2FAAccountId(iamUsername);
      if (accountId.isEmpty()) {
         LOG.debug(String.format("IAM user %s does not have an Airlock 2FA account", LogUtil.logSafeQuote(iamUsername)));
         return false;
      }

      Airlock2FAPreAuthenticationService.PreAuthenticationAnswer answer = ((Airlock2FAPreAuthenticationService)this.service.get())
         .preAuthenticate(new Airlock2FAPreAuthenticationData(accountId.get(), iamUsername));
      this.logPreAuthenticationAnswer(answer);
      if (answer.getResult().equals(Airlock2FAPreAuthenticationService.PreAuthenticationResult.ALLOW)) {
         if (((Airlock2FAHasDeviceConditionSettings)this.settings.get()).bypassModeEnabled()) {
            return true;
         }

         LOG.debug(
            String.format("IAM user %s has bypass mode enabled but bypass mode is disabled in the Airlock2FA settings", LogUtil.logSafeQuote(iamUsername))
         );
      }

      return !answer.isAuthenticationAllowed() ? false : this.hasApplicableDevices(iamUsername, accountId.get(), answer);
   }

   private boolean hasApplicableDevices(String iamUsername, Airlock2FAAccountId accountId, Airlock2FAPreAuthenticationService.PreAuthenticationAnswer answer) {
      Airlock2FADeviceAndFactorApplicability applicability = this.deviceAndFactorApplicability(answer);
      Airlock2FADeviceSettings deviceSettings = ((Airlock2FAHasDeviceConditionSettings)this.settings.get()).deviceSettings();
      if (((Airlock2FAFactorSet)this.enabledFactors.get()).contains(Airlock2FAFactor.OFFLINE_QR_CODE)
         && deviceSettings.payloadEncryptionSettings().requireEncryptedPayloadForHwTokens()) {
         applicability.hardwareTokensCapableOfQrCode()
            .forEach(
               device -> LOG.warn(
                  LogTypes.CONFIGURATION_ERROR,
                  "Factor %s is not allowed for hardware device with id \"%s\" because payload encryption is enabled and unencrypted payload for HW tokens is disabled."
                     .formatted(Airlock2FAFactor.OFFLINE_QR_CODE.getDisplayName(), device.getId())
               )
            );
      }

      if (applicability.devicesForSelection().isEmpty()) {
         LOG.debug(
            String.format(
               "IAM user '%s' with Airlock 2FA account '%s' does not have any applicable devices for factors: %s",
               LogUtil.logSafeQuote(iamUsername),
               accountId,
               applicability
            )
         );
         return false;
      } else if (deviceSettings.cooldownSettings().cooldownConfiguredAndRespected() && this.allDevicesInCooldown(applicability)) {
         ((HelpdeskLogger)this.helpdeskLogger.get())
            .log(
               iamUsername,
               String.format(
                  "IAM user ${affectedUser} with Airlock 2FA account %s does not have any applicable devices which are not in cooldown",
                  LogUtil.logSafeQuote(accountId)
               )
            );
         return false;
      } else {
         return true;
      }
   }

   private boolean allDevicesInCooldown(Airlock2FADeviceAndFactorApplicability applicability) {
      return applicability.devicesForSelection()
         .stream()
         .allMatch(
            d -> d.isInCooldownPeriod(
               ((Airlock2FAHasDeviceConditionSettings)this.settings.get()).deviceSettings().cooldownSettings().cooldownPeriod().orElse(Duration.ZERO)
            )
         );
   }

   private Optional<Airlock2FAAccountId> findAirlock2FAAccountId(String iamUsername) {
      return ((Airlock2FAUserRepository)this.repository.get()).findBy(UserId.userId(iamUsername)).map(Airlock2FAUser::getAccountId);
   }

   private Airlock2FADeviceAndFactorApplicability deviceAndFactorApplicability(Airlock2FAPreAuthenticationService.PreAuthenticationAnswer answer) {
      return Airlock2FADeviceAndFactorApplicability.builder()
         .allowedFactorsByConfig((Airlock2FAFactorSet)this.enabledFactors.get())
         .allowedFactorsByBackend(answer.getAllowedFactors())
         .devices(answer.getDevices())
         .payloadEncryptionSettings(((Airlock2FAHasDeviceConditionSettings)this.settings.get()).deviceSettings().payloadEncryptionSettings())
         .build();
   }

   private void logPreAuthenticationAnswer(Airlock2FAPreAuthenticationService.PreAuthenticationAnswer answer) {
      String userName = ((User)this.user.get()).getName();
      String status = answer.getUserStatus().map(UserStatus::getValue).orElse("UNKNOWN");
      ((SigningLogger)this.signingLogger.get())
         .log("Futurae pre-authentication for user: " + LogUtil.quote(userName) + ", returned: " + LogUtil.quote(answer.getResult().getValue() + "/" + status));
   }

   @Override
   public String displayName() {
      return "Has Airlock 2FA Device";
   }
}
