package com.airlock.iam.common.domain.service.credential;

import com.airlock.iam.base.api.domain.model.user.UserId;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.base.misc.time.TimeProvider;
import com.airlock.iam.common.api.domain.model.authentication.Authentee;
import com.airlock.iam.common.api.domain.model.authentication.AuthenticationResult;
import com.airlock.iam.common.api.domain.model.contextdata.ContextData;
import com.airlock.iam.common.api.domain.model.credential.RequestAuthentication;
import com.airlock.iam.common.api.domain.model.request.Request;
import com.airlock.iam.common.api.domain.model.role.Role;
import com.airlock.iam.common.api.domain.model.store.user.UserStore;
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.service.credential.RequestAuthenticationService;
import com.airlock.iam.common.domain.model.authentication.AuthenteeImpl;
import com.airlock.iam.common.domain.model.authentication.AuthenticationResultImpl;
import com.airlock.iam.common.domain.model.contextdata.ContextDataImpl;
import com.airlock.iam.common.domain.model.role.RolesUtil;
import com.airlock.iam.common.domain.model.user.TentativePrincipalInfo;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.log.api.signing.SigningLogger;
import com.google.common.collect.Sets;
import jakarta.annotation.Nonnull;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

public final class RequestAuthenticationServiceImpl implements RequestAuthenticationService {
   private static final ELogger LOG = ELoggerFactory.getLogger(RequestAuthenticationServiceImpl.class);
   private final Provider<RequestAuthentication> requestAuthentication;
   private final Provider<TentativePrincipalInfo> tentativePrincipalInfo;
   private final Provider<RequestAuthenticationSettings> settings;
   private final Provider<RequestAuthenticationLockService> lockService;
   private final Provider<TimeProvider> time;
   private final SigningLogger signingLogger;

   @Inject
   public RequestAuthenticationServiceImpl(
      Provider<RequestAuthentication> requestAuthentication,
      Provider<TentativePrincipalInfo> tentativePrincipalInfo,
      Provider<RequestAuthenticationSettings> settings,
      Provider<RequestAuthenticationLockService> lockService,
      Provider<TimeProvider> time,
      SigningLogger signingLogger
   ) {
      this.requestAuthentication = requestAuthentication;
      this.tentativePrincipalInfo = tentativePrincipalInfo;
      this.settings = settings;
      this.lockService = lockService;
      this.time = time;
      this.signingLogger = signingLogger;
   }

   @Override
   public Optional<AuthenticationResult> extractCredentialAndAuthenticate(Request request) {
      Optional<AuthenticationResult> optionalAuthResult = ((RequestAuthentication)this.requestAuthentication.get()).extractCredentialAndAuthenticate(request);
      if (optionalAuthResult.isEmpty()) {
         return Optional.empty();
      }

      AuthenticationResult authResult = optionalAuthResult.get();
      UserIdentity identity = authResult.userIdentity();
      ((TentativePrincipalInfo)this.tentativePrincipalInfo.get()).setTentativeIdentity(identity);
      if (authResult.authentee().isEmpty()) {
         this.updateUserWithFailedLoginAttempt(authResult, identity);
         this.signingLogger.log("Request authentication for user " + LogUtil.logSafeQuote(identity.id()) + " failed.");
         return Optional.of(authResult);
      }

      Authentee authentee = authResult.authentee().get();
      Optional<PersistentUser> user = ((RequestAuthenticationSettings)this.settings.get())
         .userStore()
         .flatMap(store -> store.findByIdentity(authentee.userIdentity().id()));
      authResult = this.validateAndEnhanceUser(authentee, user, authResult);
      if (authResult.authentee().isEmpty()) {
         this.signingLogger.log("Validity checks failed for user " + LogUtil.logSafeQuote(identity.id()) + " after request authentication.");
         return Optional.of(AuthenticationResultImpl.withIdentity(authResult, identity));
      }

      if (((RequestAuthenticationSettings)this.settings.get()).userStore().isPresent() && authResult.factorCheckPerformed()) {
         this.updateUserWithSuccessfulLoginAttempt(((RequestAuthenticationSettings)this.settings.get()).userStore().get(), user.orElseThrow());
      }

      UserIdentity finalIdentity = authResult.authentee().get().userIdentity();
      AuthenticationResultImpl authenticationResult = AuthenticationResultImpl.withIdentity(authResult, identity);
      this.signingLogger
         .log(
            "Successful request authentication for identity %s with granted roles %s.",
            new Object[]{LogUtil.logSafeQuote(finalIdentity.id()), LogUtil.logSafeQuote(this.listGrantedRoleNames(authenticationResult))}
         );
      return Optional.of(authenticationResult);
   }

   private void updateUserWithFailedLoginAttempt(AuthenticationResult authResult, UserIdentity identity) {
      if (((RequestAuthenticationSettings)this.settings.get()).userStore().isPresent() && authResult.factorCheckPerformed()) {
         UserStore userStore = ((RequestAuthenticationSettings)this.settings.get()).userStore().get();
         Optional<PersistentUser> user = userStore.findByIdentity(identity.id());
         if (user.isPresent()) {
            user.get().setLatestLoginAttempt(((TimeProvider)this.time.get()).getCurrentTime());
            this.increaseFailedCounter(user.get(), identity.id());
            userStore.update(user.get());
         }
      }
   }

   @Nonnull
   private String listGrantedRoleNames(AuthenticationResultImpl authenticationResult) {
      return RolesUtil.roleNamesToCommaSeparatedString(authenticationResult.authentee().orElseThrow().roles());
   }

   private void increaseFailedCounter(PersistentUser user, UserId userId) {
      user.increaseFailedLogins();
      if (((RequestAuthenticationSettings)this.settings.get()).maxFailedAttempts().isPresent()
         && user.getFailedLogins() > ((RequestAuthenticationSettings)this.settings.get()).maxFailedAttempts().getAsInt()) {
         ((RequestAuthenticationLockService)this.lockService.get()).lockUser(userId);
      }
   }

   private void updateUserWithSuccessfulLoginAttempt(UserStore userStore, PersistentUser user) {
      user.successfullyLoggedIn(((TimeProvider)this.time.get()).getCurrentTime());
      userStore.update(user);
   }

   private AuthenticationResult validateAndEnhanceUser(Authentee authUser, Optional<PersistentUser> user, AuthenticationResult authResult) {
      if (((RequestAuthenticationSettings)this.settings.get()).userStore().isEmpty()) {
         return AuthenticationResultImpl.withUser(authResult, Optional.of(this.processRoles(authUser)));
      } else if (user.isEmpty()) {
         LOG.info("User " + LogUtil.logSafeQuote(authUser.userIdentity().id()) + " not found in user store.");
         return AuthenticationResultImpl.withUser(authResult, Optional.empty());
      } else if (user.get().isLocked()) {
         LOG.info("User " + LogUtil.logSafeQuote(authUser.userIdentity().id()) + " is locked.");
         return AuthenticationResultImpl.withoutRetryAndWithoutAuthneticatedUser(authResult);
      } else if (!user.get().getUserValidity().isValid()) {
         LOG.info("User " + LogUtil.logSafeQuote(authUser.userIdentity().id()) + " is not valid.");
         return AuthenticationResultImpl.withoutRetryAndWithoutAuthneticatedUser(authResult);
      } else {
         return AuthenticationResultImpl.withUser(authResult, Optional.of(this.processRoles(mergeUser(user.get(), authUser))));
      }
   }

   private Authentee processRoles(Authentee authUser) {
      Set<Role> newRoles = new HashSet<>(authUser.roles());
      newRoles.addAll(((RequestAuthenticationSettings)this.settings.get()).staticRoles());
      Set<String> blockList = ((RequestAuthenticationSettings)this.settings.get()).rolesBlocklist();
      newRoles = newRoles.stream().filter(role -> !blockList.contains(role.getRoleName())).collect(Collectors.toSet());
      return new AuthenteeImpl(authUser.userIdentity(), newRoles, authUser.contextData());
   }

   private static Authentee mergeUser(PersistentUser persistentUser, Authentee authUser) {
      UserIdentity identity = new UserIdentity(UserId.userId(persistentUser.getName()), authUser.userIdentity().providedUsername());
      ContextData persistentData = new ContextDataImpl(persistentUser.getContextData());
      return new AuthenteeImpl(identity, Sets.union(authUser.roles(), persistentUser.getRoles()), persistentData.mergeWith(authUser.contextData()));
   }
}
