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

import com.airlock.iam.base.api.domain.model.exception.EntityNotFoundException;
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.domain.model.cronto.ExternalCrontoDeviceId;
import com.airlock.iam.core.domain.model.cronto.CrontoOtp;
import com.airlock.iam.core.domain.model.cronto.challenge.CrontoActivation2Challenge;
import com.airlock.iam.core.domain.model.cronto.challenge.CrontoDeactivationChallenge;
import com.airlock.iam.core.misc.cronto.CrontoHandler;
import com.airlock.iam.core.misc.impl.cronto.CrontoActivationSession;
import com.airlock.iam.core.misc.impl.token.cronto.CrontoDeviceToken;
import com.airlock.iam.core.misc.impl.token.cronto.CrontoLetterToken;
import com.airlock.iam.core.misc.token.Token;
import com.airlock.iam.core.misc.util.cronto.CrontoUtils;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.flow.shared.domain.model.cronto.CompleteDeviceActivationResult;
import com.airlock.iam.flow.shared.domain.model.cronto.CrontoDeviceActivationChallenge;
import com.airlock.iam.flow.shared.domain.model.cronto.CrontoSelfManagementServiceSettings;
import com.airlock.iam.flow.shared.domain.model.cronto.StartDeviceActivationResult;
import com.airlock.iam.log.api.signing.SigningLogger;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Strings;

public final class CrontoSelfManagementServiceImpl implements CrontoSelfManagementService {
   private static final ELogger LOG = ELoggerFactory.getLogger(CrontoSelfManagementServiceImpl.class);
   private final Provider<CrontoDeviceManagementService> service;
   private final Provider<CrontoHandler> crontoHandler;
   private final Provider<CrontoSelfManagementServiceSettings> settings;
   private final SigningLogger signingLogger;

   @Inject
   public CrontoSelfManagementServiceImpl(
      Provider<CrontoDeviceManagementService> service,
      Provider<CrontoHandler> crontoHandler,
      Provider<CrontoSelfManagementServiceSettings> settings,
      SigningLogger signingLogger
   ) {
      this.crontoHandler = crontoHandler;
      this.service = service;
      this.settings = settings;
      this.signingLogger = signingLogger;
   }

   @Override
   public List<CrontoDeviceToken> getDevicesByUsername(UserId userId) {
      return ((CrontoDeviceManagementService)this.service.get())
         .getDevicesByUsername(userId)
         .stream()
         .sorted(Comparator.comparing(Token::getGenerationDate, Comparator.nullsFirst(Comparator.naturalOrder())))
         .toList();
   }

   @Override
   public CrontoDeviceToken getDevice(UserId userId, ExternalCrontoDeviceId externalId) {
      return ((CrontoDeviceManagementService)this.service.get()).getDeviceById(userId, externalId);
   }

   @Override
   public String getDeviceIdForLogging(UserId userId, ExternalCrontoDeviceId externalId) {
      try {
         return this.getDevice(userId, externalId).getDeviceIdForDisplay();
      } catch (EntityNotFoundException e) {
         return LogUtil.logSafeString(externalId.getId());
      }
   }

   @Override
   public boolean canActivateDeviceOnScreen(UserId userId, CrontoHandler.CrontoSelfRegistrationContext context) {
      return ((CrontoHandler)this.crontoHandler.get()).canUseOnScreenCryptogramForDeviceActivation(userId, context);
   }

   @Override
   public CrontoActivationSession startOnScreenActivation(UserId userId) {
      if (((CrontoHandler)this.crontoHandler.get()).hasValidAndUsableLetter(userId)) {
         CrontoLetterToken letterToken = ((CrontoHandler)this.crontoHandler.get()).getAllLettersForUser(userId).stream().findFirst().orElseThrow();
         return CrontoActivationSession.startWithOnScreenCryptogram(
            userId, ((CrontoHandler)this.crontoHandler.get()).getActivationChallengeFromLetter(letterToken), letterToken
         );
      } else {
         return CrontoActivationSession.startWithOnScreenCryptogram(userId, ((CrontoHandler)this.crontoHandler.get()).createActivationChallenge1(userId));
      }
   }

   @Override
   public StartDeviceActivationResult verifyActivationOtp(CrontoActivationSession activationSession, CrontoOtp otp) {
      if (((CrontoHandler)this.crontoHandler.get()).hasReachedMaxiumNumberOfDevices(activationSession.getUserId())) {
         LOG.info("User " + LogUtil.quote(activationSession.getUserId()) + " reached device limit when starting to add new Cronto device");
         return StartDeviceActivationResult.failure(StartDeviceActivationResult.FailureReason.CRONTO_DEVICE_LIMIT_REACHED);
      } else {
         ((CrontoHandler)this.crontoHandler.get()).verifyActivationResponse1(activationSession, otp);
         if (!activationSession.isResponse1Valid()) {
            LOG.info("User " + LogUtil.quote(activationSession.getUserId()) + " entered wrong activation letter OTP when starting to add new Cronto device");
            return StartDeviceActivationResult.failure(StartDeviceActivationResult.FailureReason.CRONTO_OTP_WRONG);
         } else if (!((CrontoHandler)this.crontoHandler.get()).isPlatformAllowed(activationSession.getDeviceInfo(), Optional.empty())) {
            LOG.info("Illegal platform code: " + activationSession.getDeviceInfo().getPlatformCode());
            return StartDeviceActivationResult.failure(StartDeviceActivationResult.FailureReason.ILLEGAL_CRONTO_PLATFORM);
         } else {
            CrontoActivation2Challenge challenge = ((CrontoHandler)this.crontoHandler.get()).createActivationChallenge2(activationSession);
            CrontoDeviceActivationChallenge result = CrontoDeviceActivationChallenge.builder()
               .challengeImage(challenge.getCryptogramForScreen())
               .secureChannelChallenge(challenge.getRawChallengeString())
               .build();
            this.signingLogger
               .log("User " + LogUtil.quote(activationSession.getUserId()) + " successfully verified OTP from first activation challenge (without letter) ");
            return StartDeviceActivationResult.of(activationSession, result);
         }
      }
   }

   @Override
   public StartDeviceActivationResult startDeviceActivation(UserId userId, CrontoOtp otp) {
      CrontoActivationSession session = CrontoActivationSession.startWithLetter(userId);
      if (((CrontoHandler)this.crontoHandler.get()).hasReachedMaxiumNumberOfDevices(userId)) {
         LOG.info("User " + LogUtil.quote(userId) + " reached device limit when starting to add new Cronto device");
         return StartDeviceActivationResult.failure(StartDeviceActivationResult.FailureReason.CRONTO_DEVICE_LIMIT_REACHED);
      } else {
         ((CrontoHandler)this.crontoHandler.get()).verifyActivationResponse1(session, otp);
         if (!session.isResponse1Valid()) {
            LOG.info("User " + LogUtil.quote(userId) + " entered wrong activation letter OTP when starting to add new Cronto device");
            return StartDeviceActivationResult.failure(StartDeviceActivationResult.FailureReason.CRONTO_OTP_WRONG);
         } else {
            Optional<CrontoLetterToken> activationLetter = session.getLetterId()
               .flatMap(letterId -> ((CrontoHandler)this.crontoHandler.get()).loadLetterById(userId, letterId));
            if (activationLetter.isPresent() && !activationLetter.get().isValidNow()) {
               LOG.info("User " + LogUtil.quote(userId) + " tried to use an invalid Cronto activation letter when starting to add new Cronto device");
               return StartDeviceActivationResult.failure(StartDeviceActivationResult.FailureReason.CRONTO_LETTER_INVALID);
            } else if (activationLetter.isPresent() && !((CrontoHandler)this.crontoHandler.get()).isLetterUsable(activationLetter.get())) {
               LOG.info("User " + LogUtil.quote(userId) + " exceeded usage for a Cronto activation letter when starting to add new Cronto device");
               return StartDeviceActivationResult.failure(StartDeviceActivationResult.FailureReason.CRONTO_LETTER_USAGE_LIMIT_REACHED);
            } else if (!((CrontoHandler)this.crontoHandler.get()).isPlatformAllowed(session.getDeviceInfo(), activationLetter)) {
               LOG.info("Illegal platform code: " + session.getDeviceInfo().getPlatformCode());
               return StartDeviceActivationResult.failure(StartDeviceActivationResult.FailureReason.ILLEGAL_CRONTO_PLATFORM);
            } else {
               CrontoActivation2Challenge challenge = ((CrontoHandler)this.crontoHandler.get()).createActivationChallenge2(session);
               CrontoDeviceActivationChallenge result = CrontoDeviceActivationChallenge.builder()
                  .challengeImage(challenge.getCryptogramForScreen())
                  .secureChannelChallenge(challenge.getRawChallengeString())
                  .build();
               this.signingLogger.log("User " + LogUtil.quote(userId) + " successfully verified activation letter OTP when starting to add new Cronto device");
               return StartDeviceActivationResult.of(session, result);
            }
         }
      }
   }

   @Override
   public CompleteDeviceActivationResult completeDeviceActivation(String deviceName, CrontoOtp enteredCode, CrontoActivationSession activationSession) {
      ((CrontoHandler)this.crontoHandler.get()).verifyActivationResponse2(activationSession, enteredCode);
      if (!activationSession.isResponse2Valid()) {
         this.signingLogger.log("User " + LogUtil.quote(activationSession.getUserId()) + " submitted wrong device OTP when adding new Cronto device");
         return CompleteDeviceActivationResult.failure(CompleteDeviceActivationResult.FailureReason.CRONTO_OTP_WRONG);
      } else if (((CrontoHandler)this.crontoHandler.get()).getTokenByDeviceId(activationSession.getUserId(), activationSession.getDeviceInfo().getDeviceId())
         != null) {
         LOG.info("Device already exists.");
         return CompleteDeviceActivationResult.failure(CompleteDeviceActivationResult.FailureReason.CRONTO_DEVICE_ALREADY_EXISTS);
      } else {
         deviceName = CrontoUtils.cleanDeviceName(deviceName);
         if (StringUtils.isBlank(deviceName)) {
            LOG.info("Name of new Cronto device must not be blank");
            return CompleteDeviceActivationResult.failure(CompleteDeviceActivationResult.FailureReason.INVALID_CRONTO_LABEL);
         } else {
            CrontoDeviceToken device = ((CrontoHandler)this.crontoHandler.get()).addActivatedDevice(activationSession, deviceName);
            this.signingLogger.log("Successfully verified device OTP and added new Cronto device");
            return CompleteDeviceActivationResult.of(device);
         }
      }
   }

   @Override
   public Optional<CrontoDeactivationChallenge> deleteDevice(UserId userId, ExternalCrontoDeviceId id) {
      List<CrontoDeviceToken> devices = this.getDevicesByUsername(userId);
      if (this.findDeviceByExternalId(devices, id).isEmpty()) {
         throw new EntityNotFoundException("Cronto device with ID " + id + " not found");
      }

      if (devices.size() == 1) {
         throw new ViolationException("can't delete the last device of a user");
      }

      if (!((CrontoSelfManagementServiceSettings)this.settings.get()).isUserCanDisableAllDevices() && !this.existsEnabledDeviceOtherThan(devices, id)) {
         throw new ViolationException("can't delete the last enabled device of user " + userId);
      }

      CrontoDeactivationChallenge crontoChallenge = ((CrontoDeviceManagementService)this.service.get()).deleteDevice(userId, id);
      return ((CrontoSelfManagementServiceSettings)this.settings.get()).isProvideDeactivationChallenge() ? Optional.of(crontoChallenge) : Optional.empty();
   }

   @Override
   public void updateLabel(UserId userId, ExternalCrontoDeviceId id, String label) {
      if (StringUtils.isBlank(label)) {
         throw new ViolationException("label must not be blank");
      }

      ((CrontoDeviceManagementService)this.service.get()).setDeviceLabel(userId, id, label);
   }

   @Override
   public void enableDevice(UserId userId, ExternalCrontoDeviceId id) {
      ((CrontoDeviceManagementService)this.service.get()).setDeviceEnabled(userId, id, true);
   }

   @Override
   public void disableDevice(UserId userId, ExternalCrontoDeviceId id) {
      List<CrontoDeviceToken> devices = this.getDevicesByUsername(userId);
      Optional<CrontoDeviceToken> device = this.findDeviceByExternalId(devices, id);
      if (device.isEmpty()) {
         throw new EntityNotFoundException("Cronto device not found");
      }

      if (device.get().isActive()) {
         if (!((CrontoSelfManagementServiceSettings)this.settings.get()).isUserCanDisableAllDevices() && !this.existsEnabledDeviceOtherThan(devices, id)) {
            throw new ViolationException("can't disable the last enabled device of user " + userId);
         }

         ((CrontoDeviceManagementService)this.service.get()).setDeviceEnabled(userId, id, false);
      }
   }

   @Override
   public void allowPushNotifications(UserId userId, ExternalCrontoDeviceId id) {
      ((CrontoDeviceManagementService)this.service.get()).allowPushNotification(userId, id);
   }

   @Override
   public void disallowPushNotification(UserId userId, ExternalCrontoDeviceId id) {
      ((CrontoDeviceManagementService)this.service.get()).disallowPushNotification(userId, id);
   }

   private Optional<CrontoDeviceToken> findDeviceByExternalId(List<CrontoDeviceToken> devices, ExternalCrontoDeviceId id) {
      return devices.stream().filter(it -> it.getSerialId().equals(id.getId())).findFirst();
   }

   private boolean existsEnabledDeviceOtherThan(List<CrontoDeviceToken> devices, ExternalCrontoDeviceId id) {
      return devices.stream().anyMatch(device -> !Strings.CS.equals(device.getSerialId(), id.getId()) && device.isActive());
   }
}
