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

import com.airlock.iam.base.api.domain.model.exception.FailureException;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.base.misc.time.TimeProvider;
import com.airlock.iam.common.api.domain.model.authmethod.AuthenticationMethod;
import com.airlock.iam.common.api.domain.model.contextdata.ContextData;
import com.airlock.iam.common.api.domain.model.role.Role;
import com.airlock.iam.common.api.domain.model.user.PersistentUser;
import com.airlock.iam.common.api.domain.model.user.UserIdentity;
import com.airlock.iam.common.api.domain.model.user.validity.UserValidity;
import com.airlock.iam.common.domain.model.contextdata.ContextDataFactory;
import com.airlock.iam.common.domain.model.contextdata.ContextDataImpl;
import com.airlock.iam.common.domain.model.user.Lockable;
import com.airlock.iam.common.domain.model.user.LockableImpl;
import com.airlock.iam.common.domain.service.adapt.PersistentUserAdapter;
import com.airlock.iam.common.infrastructure.adapt.PersistentUserAdapterImpl;
import com.airlock.iam.log.api.signing.SigningLogger;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.inject.Provider;
import java.time.Instant;
import java.util.Collection;
import java.util.Collections;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.Set;
import java.util.stream.Collectors;

public final class UserPrincipalImpl implements UserPrincipal {
   private static final PersistentUserAdapter<Lockable> LOCKABLE_ADAPTER = PersistentUserAdapterImpl.create(LockableImpl.class);
   private Optional<PersistentUser> cachedUser = Optional.empty();
   private Optional<UserIdentity> userIdentity = Optional.empty();
   private final Provider<ContextDataFactory> contextDataFactory;
   private final Provider<MaxFailedAttempts> maxFailedAttempts;
   private final SigningLogger signingLogger;
   private final TimeProvider timeProvider;

   public UserPrincipalImpl(
      Provider<ContextDataFactory> contextDataFactory, Provider<MaxFailedAttempts> maxFailedAttempts, SigningLogger signingLogger, TimeProvider timeProvider
   ) {
      this.contextDataFactory = contextDataFactory;
      this.maxFailedAttempts = maxFailedAttempts;
      this.signingLogger = signingLogger;
      this.timeProvider = timeProvider;
   }

   @Override
   public String getName() {
      return this.userIdentity.<String>map(id -> id.id().getValue()).orElseThrow(() -> new FailureException("User not identified"));
   }

   @Override
   public Optional<UserIdentity> getIdentity() {
      return this.userIdentity;
   }

   @Override
   public PersistentUser user() {
      return this.cachedUser.orElseThrow();
   }

   @Override
   public Optional<PersistentUser> userOptional() {
      return this.cachedUser;
   }

   @Override
   public boolean exists() {
      return this.cachedUser.isPresent();
   }

   public void initialize(UserIdentity newUserIdentity, Optional<PersistentUser> user) {
      if (this.cachedUser.isPresent()) {
         throw new FailureException("User already cached.");
      }

      this.userIdentity = Optional.of(newUserIdentity);
      this.cachedUser = user;
   }

   @Override
   public boolean hasUserId(UserIdentity userId) {
      return this.exists() && userId.id().hasValue(this.cachedUser.orElseThrow().getName());
   }

   public void update(PersistentUser reloadedUser) {
      if (this.userIdentity.isEmpty()) {
         throw new FailureException("Cannot reload unidentified user.");
      }

      if (!this.userIdentity.get().id().hasValue(reloadedUser.getName())) {
         throw new FailureException(
            "Inconsistency between username %s and user ID %s."
               .formatted(LogUtil.logSafeQuote(reloadedUser.getName()), this.userIdentity.get().id().getValue())
         );
      }

      this.cachedUser = Optional.of(reloadedUser);
   }

   @Override
   public Optional<Lockable> asLockable() {
      return this.exists() ? Optional.of(LOCKABLE_ADAPTER.to(this.user())) : Optional.empty();
   }

   @Override
   public Optional<UserValidity> getValidity() {
      return this.exists() ? Optional.of(this.user().getUserValidity()) : Optional.empty();
   }

   @Override
   public void increaseFailedLogins(AuthenticationMethod authMethod) {
      if (this.exists()) {
         if (!((MaxFailedAttempts)this.maxFailedAttempts.get()).isExceededBy(this.user().getAllFailedLogins())) {
            Instant now = this.timeProvider.getCurrentInstant();
            this.user().getAllFailedLogins().incrementFailedLoginCountForMethod(authMethod, now);
            this.signingLogger
               .log(
                  "Increased failed attempts counter '%s' for user '%s' to '%s'.",
                  new Object[]{authMethod.getId(), this.getName(), this.user().getAllFailedLogins().getFailedLoginCountForMethod(authMethod)}
               );
         }

         this.lockUserIfMaxFailedAttemptsExceeded();
      }
   }

   @Override
   public void resetFailedLogins(Collection<AuthenticationMethod> methods) {
      if (this.exists() && !methods.isEmpty()) {
         this.user().getAllFailedLogins().resetFailedLoginCountersForMethods(methods);
         this.signingLogger
            .log(
               "Reset failed attempt counters '%s' for user '%s'.",
               new Object[]{methods.stream().map(AuthenticationMethod::getId).collect(Collectors.joining(", ")), this.getName()}
            );
      }
   }

   @Override
   public OptionalInt getRemainingAttempts(AuthenticationMethod authMethod) {
      if (this.exists() && !((MaxFailedAttempts)this.maxFailedAttempts.get()).getFailedAttemptsLimit().isEmpty()) {
         int maxFailedLimit = ((MaxFailedAttempts)this.maxFailedAttempts.get()).getFailedAttemptsLimit().getAsInt();
         int failedAttempts = this.user().getAllFailedLogins().getFailedLoginCountForMethod(authMethod);
         return OptionalInt.of(Math.max(maxFailedLimit - failedAttempts + 1, 0));
      } else {
         return OptionalInt.empty();
      }
   }

   private void lockUserIfMaxFailedAttemptsExceeded() {
      ((MaxFailedAttempts)this.maxFailedAttempts.get()).lockIfExceeded(this.user().getAllFailedLogins(), this.asLockable().get());
   }

   @Override
   public ContextData getContextData() {
      return this.userOptional()
         .map(user -> ((ContextDataFactory)this.contextDataFactory.get()).fromPersistency(user.getContextData()))
         .orElseGet(ContextDataImpl::emptyContextData);
   }

   @Override
   public void setContextData(ContextData newContextData) {
      this.user().clearContextData();
      this.user().putAllContextData(newContextData.asKeyValueMap());
   }

   @Override
   public Set<Role> getRoles() {
      return this.userOptional().map(PersistentUser::getRoles).orElseGet(Collections::emptySet);
   }

   @Override
   public boolean hasName() {
      return this.cachedUser.isPresent();
   }

   @SuppressFBWarnings(justification = "generated code")
   public static UserPrincipalImpl.UserPrincipalImplBuilder builder() {
      return new UserPrincipalImpl.UserPrincipalImplBuilder();
   }

   @SuppressFBWarnings(justification = "generated code")
   public static class UserPrincipalImplBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private Provider<ContextDataFactory> contextDataFactory;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<MaxFailedAttempts> maxFailedAttempts;
      @SuppressFBWarnings(justification = "generated code")
      private SigningLogger signingLogger;
      @SuppressFBWarnings(justification = "generated code")
      private TimeProvider timeProvider;

      @SuppressFBWarnings(justification = "generated code")
      UserPrincipalImplBuilder() {
      }

      @SuppressFBWarnings(justification = "generated code")
      public UserPrincipalImpl.UserPrincipalImplBuilder contextDataFactory(Provider<ContextDataFactory> contextDataFactory) {
         this.contextDataFactory = contextDataFactory;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UserPrincipalImpl.UserPrincipalImplBuilder maxFailedAttempts(Provider<MaxFailedAttempts> maxFailedAttempts) {
         this.maxFailedAttempts = maxFailedAttempts;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UserPrincipalImpl.UserPrincipalImplBuilder signingLogger(SigningLogger signingLogger) {
         this.signingLogger = signingLogger;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UserPrincipalImpl.UserPrincipalImplBuilder timeProvider(TimeProvider timeProvider) {
         this.timeProvider = timeProvider;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UserPrincipalImpl build() {
         return new UserPrincipalImpl(this.contextDataFactory, this.maxFailedAttempts, this.signingLogger, this.timeProvider);
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "UserPrincipalImpl.UserPrincipalImplBuilder(contextDataFactory="
            + this.contextDataFactory
            + ", maxFailedAttempts="
            + this.maxFailedAttempts
            + ", signingLogger="
            + this.signingLogger
            + ", timeProvider="
            + this.timeProvider
            + ")";
      }
   }
}
