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

import com.airlock.iam.base.api.domain.model.user.UserId;
import com.airlock.iam.common.api.domain.model.cronto.CrontoDeviceId;
import com.airlock.iam.common.api.domain.model.user.CurrentLocale;
import com.airlock.iam.core.domain.model.cronto.CrontoCheckingSettings;
import com.airlock.iam.core.misc.impl.authen.CrontoUsedDeviceInfo;
import com.airlock.iam.core.misc.impl.authen.StringResourceProvider;
import com.airlock.iam.core.misc.impl.authen.cronto.CrontoPushDeviceChoice;
import com.airlock.iam.core.misc.impl.authen.cronto.DeviceSelectionInformation;
import com.airlock.iam.core.misc.impl.cronto.CrontoChallengeResult;
import com.airlock.iam.core.misc.impl.token.cronto.CrontoDeviceToken;
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 jakarta.inject.Inject;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Strings;

public final class CrontoMethodAndDeviceSelectionHandler {
   private static final ELogger LOG = ELoggerFactory.getLogger(CrontoMethodAndDeviceSelectionHandler.class);
   private final CrontoCheckingSettings settings;
   private final StringResourceProvider stringResourceProvider;
   private final CurrentLocale locale;

   @Inject
   public CrontoMethodAndDeviceSelectionHandler(CrontoCheckingSettings settings, StringResourceProvider stringResourceProvider, CurrentLocale locale) {
      this.settings = settings;
      this.stringResourceProvider = stringResourceProvider;
      this.locale = locale;
   }

   public DeviceSelectionInformation createDeviceSelectionInformation(
      Optional<CrontoUsedDeviceInfo> requestedDeviceInfo, UserId userId, Optional<String> selectedDeviceId
   ) {
      if (selectedDeviceId.isPresent()) {
         Optional<CrontoDeviceToken> selectedDevice = this.getSelectedPushDevice(userId, selectedDeviceId.get());
         if (selectedDevice.isPresent()) {
            return DeviceSelectionInformation.createSelectionWithSinglePushDevice(selectedDevice.get());
         }

         LOG.info("Invalid choice: the user with username '" + userId + "' does not have a push-enabled device with id '" + selectedDeviceId.get() + "'");
         return this.createDeviceSelectionInfo(requestedDeviceInfo, userId);
      } else {
         return this.createDeviceSelectionInfo(requestedDeviceInfo, userId);
      }
   }

   public List<CrontoDeviceToken> getPushEnabledDevices(UserId userId) {
      return this.settings.getCrontoHandler().getPushAppHandler() == null
         ? Collections.emptyList()
         : this.settings.getCrontoHandler().getPushAppHandler().getPushEnabledDevices(userId);
   }

   public boolean hasActivePushDevice(UserId userId) {
      return !this.getPushEnabledDevices(userId).isEmpty();
   }

   private DeviceSelectionInformation createDeviceSelectionInfo(Optional<CrontoUsedDeviceInfo> deviceInfo, UserId userId) {
      if (!this.settings.getCrontoHandler().isPushNotificationEnabled()) {
         return DeviceSelectionInformation.createNonPushSelection(this.determineAuthMethodForNonPushSelection(deviceInfo));
      } else {
         return deviceInfo.isPresent() && deviceInfo.get().getMethod() != CrontoChallengeResult.CrontoMethod.PUSH
            ? DeviceSelectionInformation.createNonPushSelection(this.determineAuthMethodForNonPushSelection(deviceInfo))
            : this.createDeviceSelectionInfoFromAvailableDevices(deviceInfo, userId);
      }
   }

   private DeviceSelectionInformation createDeviceSelectionInfoFromAvailableDevices(Optional<CrontoUsedDeviceInfo> requestedDeviceInfo, UserId userId) {
      List<CrontoDeviceToken> enabledDevices = this.getPushEnabledDevices(userId);
      if (requestedDeviceInfo.isPresent()) {
         CrontoDeviceId requestedId = requestedDeviceInfo.get().getDeviceId();
         Optional<CrontoDeviceToken> selected = enabledDevices.stream().filter(dev -> this.equalDeviceId(dev, requestedId)).findFirst();
         if (selected.isPresent()) {
            return DeviceSelectionInformation.createSelectionWithSinglePushDevice(selected.get());
         }
      }

      if (this.settings.isPushToAllDevices() && enabledDevices.size() > 1) {
         return DeviceSelectionInformation.createPushToAllChoiceSelection(enabledDevices);
      } else {
         List<CrontoDeviceToken> nonPushEnabledDevices = this.getNonPushEnabledDevices(userId);
         if (this.userChoiceRequired(enabledDevices)) {
            return DeviceSelectionInformation.createUserChoiceSelection(this.createCrontoPushDeviceChoices(enabledDevices, nonPushEnabledDevices));
         } else {
            return !enabledDevices.isEmpty()
               ? DeviceSelectionInformation.createSelectionWithSinglePushDevice(enabledDevices.getFirst())
               : DeviceSelectionInformation.createNonPushSelection(this.determineAuthMethodForNonPushSelection(requestedDeviceInfo));
         }
      }
   }

   private Optional<CrontoChallengeResult.CrontoMethod> determineAuthMethodForNonPushSelection(Optional<CrontoUsedDeviceInfo> requestedDeviceInfo) {
      return this.settings.getCrontoHandler().isFallbackToOfflineValidation()
         ? Optional.of(CrontoChallengeResult.CrontoMethod.SCAN_TAN)
         : requestedDeviceInfo.map(CrontoUsedDeviceInfo::getMethod);
   }

   private boolean userChoiceRequired(List<CrontoDeviceToken> pushEnabledDevices) {
      return pushEnabledDevices.size() > 1 || this.settings.isPushSelectionForSingleDevice() && pushEnabledDevices.size() == 1;
   }

   private Optional<CrontoDeviceToken> getSelectedPushDevice(UserId userId, String choiceId) {
      for (CrontoDeviceToken device : this.getPushEnabledDevices(userId)) {
         if (device.getDeviceId().getInternalId().equals(choiceId)) {
            return Optional.of(device);
         }
      }

      return Optional.empty();
   }

   private boolean equalDeviceId(CrontoDeviceToken dev, CrontoDeviceId requestedId) {
      return Strings.CS.equals(dev.getDeviceId().getInternalId(), requestedId.getInternalId());
   }

   private List<CrontoDeviceToken> getNonPushEnabledDevices(UserId userId) {
      return this.settings
         .getCrontoHandler()
         .getAllDevicesForUser(userId)
         .stream()
         .filter(Token::isActive)
         .filter(d -> !d.canReceivePushNotification())
         .collect(Collectors.toList());
   }

   private List<CrontoPushDeviceChoice> createCrontoPushDeviceChoices(List<CrontoDeviceToken> pushEnabledDevices, List<CrontoDeviceToken> nonPushEnabledDevices) {
      Optional<CrontoDeviceToken> lastUsedDevice = getLastUsedDevice(pushEnabledDevices, nonPushEnabledDevices);
      return pushEnabledDevices.stream().map(dev -> this.createCrontoPushDeviceChoice(lastUsedDevice, dev)).toList();
   }

   private CrontoPushDeviceChoice createCrontoPushDeviceChoice(Optional<CrontoDeviceToken> lastUsedDevice, CrontoDeviceToken device) {
      String label = this.customDeviceNameAndPlatformName(device);
      boolean isPreselected = lastUsedDevice.isPresent() && lastUsedDevice.get().equals(device);
      return new CrontoPushDeviceChoice(device.getDeviceId().getInternalId(), label, isPreselected);
   }

   private static Optional<CrontoDeviceToken> getLastUsedDevice(List<CrontoDeviceToken> pushDevices, List<CrontoDeviceToken> nonPushDevices) {
      List<CrontoDeviceToken> devicesHavingLatestUsageDateSet = Stream.concat(pushDevices.stream(), nonPushDevices.stream())
         .filter(d -> d.getLatestUsageDate() != null)
         .toList();
      return devicesHavingLatestUsageDateSet.isEmpty()
         ? Optional.empty()
         : Optional.of(Collections.max(devicesHavingLatestUsageDateSet, Comparator.comparing(Token::getLatestUsageDate)));
   }

   private String customDeviceNameAndPlatformName(CrontoDeviceToken device) {
      StringBuilder label = new StringBuilder();
      String strippedCustomName = StringUtils.stripToEmpty(device.getCustomName());
      label.append(StringUtils.isNotEmpty(strippedCustomName) ? strippedCustomName : device.getDeviceId().getDisplayId());
      if (device.getPlatformType().isPresent()) {
         label.append(" (")
            .append(CrontoUtils.getLanguageDependentPlatformName(device.getPlatformType().get(), this.stringResourceProvider, this.locale.get()))
            .append(')');
      }

      return label.toString();
   }
}
