package com.airlock.iam.common.domain.model.templock;

import com.airlock.iam.base.api.domain.model.plugin.LogicForConfig;
import com.airlock.iam.base.misc.time.TimeProvider;
import com.airlock.iam.common.api.domain.model.authentication.failed.FailedLoginsInfo;
import com.airlock.iam.common.api.domain.model.locking.TemporaryLockingStrategy;
import com.airlock.iam.common.api.domain.model.user.PersistentUser;
import com.airlock.iam.common.application.configuration.templock.TemporaryLockingConfig;
import jakarta.inject.Inject;
import java.time.Duration;
import java.time.Instant;
import java.util.Collection;
import java.util.Comparator;
import java.util.Optional;

@LogicForConfig(TemporaryLockingConfig.class)
public class TemporaryLockingImpl implements TemporaryLocking {
   private final TemporaryLockingStrategy temporaryLockingStrategy;
   private final TimeProvider timeProvider;

   @Inject
   public TemporaryLockingImpl(TemporaryLockingStrategy temporaryLockingStrategy, TimeProvider timeProvider) {
      this.temporaryLockingStrategy = temporaryLockingStrategy;
      this.timeProvider = timeProvider;
   }

   @Override
   public Optional<TemporaryLocking.TemporaryLock> checkTemporaryLock(PersistentUser user) {
      Collection<FailedLoginsInfo> failedLoginsOfAllMethods = user.getAllFailedLogins().getFailedLoginsByAuthMethod().values();
      return failedLoginsOfAllMethods.stream()
         .map(failedLoginsOfMethod -> calculateLockoutExpiry(failedLoginsOfMethod, this.temporaryLockingStrategy))
         .filter(Optional::isPresent)
         .map(Optional::get)
         .filter(this::isInTheFuture)
         .max(Comparator.naturalOrder())
         .map(TemporaryLocking.TemporaryLock::new);
   }

   private boolean isInTheFuture(Instant expiry) {
      return expiry.isAfter(this.timeProvider.getCurrentInstant());
   }

   private static Optional<Instant> calculateLockoutExpiry(FailedLoginsInfo failedLoginsInfo, TemporaryLockingStrategy strategy) {
      int count = failedLoginsInfo.getCount();
      if (count > 0) {
         Duration duration = strategy.calculateLockoutDuration(count);
         Optional<Instant> latestAttempt = failedLoginsInfo.getLatest();
         return latestAttempt.map(it -> calculateLockoutExpiry(it, duration));
      } else {
         return Optional.empty();
      }
   }

   private static Instant calculateLockoutExpiry(Instant latestAttempt, Duration lockout) {
      try {
         return Instant.ofEpochMilli(Math.addExact(latestAttempt.toEpochMilli(), lockout.toMillis()));
      } catch (ArithmeticException e) {
         return Instant.ofEpochMilli(Long.MAX_VALUE);
      }
   }
}
