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

import com.airlock.iam.base.api.domain.model.exception.EntityNotFoundException;
import com.airlock.iam.base.api.domain.model.user.UserId;
import com.airlock.iam.base.misc.collection.Pair;
import com.airlock.iam.common.domain.service.phone.PhoneNumberService;
import com.airlock.iam.core.domain.model.token.TokenUsage;
import com.airlock.iam.core.misc.authen.result.AuthenticationFailed;
import com.airlock.iam.core.misc.impl.token.MtanItem;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.inject.Inject;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Strings;

public class MtanTokens {
   private final LoginMtanTokenRepository repository;
   private final PhoneNumberService phoneNumberService;

   @Inject
   public MtanTokens(LoginMtanTokenRepository repository, PhoneNumberService phoneNumberService) {
      this.repository = repository;
      this.phoneNumberService = phoneNumberService;
   }

   public List<AuthenticationFailed.MtanChoiceToken> createChoicesForAuthentication(UserId userId) {
      return this.asChoices(userId, MtanItem::isActive);
   }

   public List<AuthenticationFailed.MtanChoiceToken> createChoicesForManagement(UserId userId) {
      return this.asChoices(userId, item -> StringUtils.isNotBlank(item.getMobilePhoneNumber()));
   }

   private List<AuthenticationFailed.MtanChoiceToken> asChoices(UserId userId, Predicate<MtanItem> filter) {
      List<MtanItem> mtanTokens = this.getMtanTokens(userId);
      Optional<Integer> lastUsedIndex = this.indexOfLastUsedActiveToken(mtanTokens);
      return this.asChoices(mtanTokens, lastUsedIndex, filter);
   }

   private List<AuthenticationFailed.MtanChoiceToken> asChoices(List<MtanItem> mtanTokens, Optional<Integer> lastUsedIndex, Predicate<MtanItem> filter) {
      return IntStream.range(0, mtanTokens.size())
         .mapToObj(i -> Pair.pair(i, mtanTokens.get(i)))
         .filter(pair -> filter.test(pair.getSecond()))
         .map(pair -> this.toChoiceToken(lastUsedIndex.orElse(0), (Pair<Integer, MtanItem>)pair))
         .collect(Collectors.toList());
   }

   public Optional<AuthenticationFailed.MtanChoiceToken> getMostRecentChoice(UserId userId) {
      List<MtanItem> mtanTokens = this.getMtanTokens(userId);
      Optional<Integer> lastUsedIndex = this.indexOfLastUsedActiveToken(mtanTokens);
      return lastUsedIndex.map(integer -> this.asChoices(mtanTokens, lastUsedIndex, MtanItem::isActive).get(integer));
   }

   private AuthenticationFailed.MtanChoiceToken toChoiceToken(int lastUsedIndex, Pair<Integer, MtanItem> pair) {
      Integer index = pair.getFirst();
      MtanItem mtan = pair.getSecond();
      boolean lastUsed = lastUsedIndex == index;
      String maskedPhoneNumber = this.phoneNumberService.maskPhoneNumber(mtan.getMobilePhoneNumber());
      String label = mtan.getLabel();
      return new AuthenticationFailed.MtanChoiceToken(mtan.getId(), lastUsed, maskedPhoneNumber, label);
   }

   private Optional<Integer> indexOfLastUsedActiveToken(List<MtanItem> mtanTokens) {
      return IntStream.range(0, mtanTokens.size())
         .mapToObj(i -> Pair.pair(i, mtanTokens.get(i)))
         .filter(pair -> this.isActiveWithTokenUsage(pair.getSecond()))
         .sorted((pair1, pair2) -> this.byMostRecentUsageDescending(pair1.getSecond(), pair2.getSecond()))
         .map(Pair::getFirst)
         .findFirst();
   }

   private boolean isActiveWithTokenUsage(MtanItem mtan) {
      return mtan.isActive() && mtan.getTokenUsage().map(TokenUsage::getMostRecentUsageDate).isPresent();
   }

   private int byMostRecentUsageDescending(MtanItem first, MtanItem second) {
      return Comparator.comparing(MtanTokens::getMostRecentUsageDate).compare(second, first);
   }

   private static Date getMostRecentUsageDate(MtanItem item) {
      return item.getTokenUsage().map(TokenUsage::getMostRecentUsageDate).orElseThrow(NullPointerException::new);
   }

   public Optional<AuthenticationFailed.MtanChoiceToken> findMtanTokenChoice(UserId userId, String id) {
      return this.createChoicesForAuthentication(userId).stream().filter(choice -> choice.getId().equals(id)).findFirst();
   }

   public MtanItem getMtanToken(UserId userId, String id) {
      return this.findMtanToken(userId, id).orElseThrow(() -> new EntityNotFoundException(String.format("Could not find mTAN token with ID '%s'", id)));
   }

   public Optional<MtanItem> findMtanToken(UserId userId, String id) {
      return this.getMtanTokens(userId).stream().filter(MtanItem::isActive).filter(item -> Strings.CS.equals(id, item.getId())).findFirst();
   }

   public void store(UserId userId, MtanItem item) {
      this.repository.store(userId, item);
   }

   public List<MtanItem> getMtanTokens(UserId userId) {
      return this.repository.getMtanTokens(userId);
   }

   @SuppressFBWarnings(justification = "generated code")
   public LoginMtanTokenRepository getRepository() {
      return this.repository;
   }

   @SuppressFBWarnings(justification = "generated code")
   public PhoneNumberService getPhoneNumberService() {
      return this.phoneNumberService;
   }
}
