package com.airlock.iam.admin.application.service.authmethod;

import com.airlock.iam.admin.api.domain.model.user.AuthenticationMethods;
import com.airlock.iam.admin.api.domain.model.user.ManagedUser;
import com.airlock.iam.admin.api.domain.service.user.ManagedUserRepository;
import com.airlock.iam.admin.domain.model.authmethod.AuthenticationMethod;
import com.airlock.iam.base.api.domain.model.exception.ViolationException;
import com.airlock.iam.base.api.domain.model.user.UserId;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.common.domain.model.event.AuthenticationMethodChangedEventFactory;
import com.airlock.iam.core.misc.impl.authen.SimpleAuthenticationMethod;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.elog.api.LogTypes;
import com.airlock.iam.log.api.signing.SigningLogger;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

public final class AuthenticationMethodService {
   public static final String ADDITIONAL_ACTIVE_AUTH_METHOD_KEYS = "additionalActiveAuthMethodKeys";
   private static final ELogger LOG = ELoggerFactory.getLogger(AuthenticationMethodService.class);
   private final Provider<List<AuthenticationMethod>> authenticationMethods;
   private final Provider<ManagedUserRepository> userRepository;
   private final Provider<AuthenticationMethodSettings> settings;
   private final Provider<AuthenticationMethodChangedEventFactory> eventFactory;
   private final Provider<SigningLogger> signingLogger;

   @Inject
   public AuthenticationMethodService(
      Provider<List<AuthenticationMethod>> authenticationMethods,
      Provider<ManagedUserRepository> userRepository,
      Provider<AuthenticationMethodSettings> settings,
      Provider<AuthenticationMethodChangedEventFactory> eventFactory,
      Provider<SigningLogger> signingLogger
   ) {
      this.userRepository = userRepository;
      this.authenticationMethods = authenticationMethods;
      this.settings = settings;
      this.eventFactory = eventFactory;
      this.signingLogger = signingLogger;
   }

   public boolean isAllowedAsActive(UserId userId, AuthenticationMethods.AuthenticationMethodId authMethod) {
      return this.getPossibleActiveAuthMethods(userId).stream().anyMatch(method -> method.getConfiguredId().equals(authMethod));
   }

   public boolean isValidAdditionalAuthMethodKey(String key) {
      return this.getAdditionalActiveAuthMethodKeys().contains(key);
   }

   public boolean isAllowedAsNext(UserId userId, AuthenticationMethods.AuthenticationMethodId... authMethods) {
      return this.isAllowedAsNext(userId, Set.of(authMethods));
   }

   public boolean isAllowedAsNext(UserId userId, Set<AuthenticationMethods.AuthenticationMethodId> authMethods) {
      if (authMethods.isEmpty()) {
         return false;
      } else {
         return !((AuthenticationMethodSettings)this.settings.get()).allowMultipleNextAuthMethods() && authMethods.size() > 1
            ? false
            : this.getPossibleNextAuthMethods(userId).stream().map(AuthenticationMethod::getConfiguredId).collect(Collectors.toSet()).containsAll(authMethods);
      }
   }

   public void setAuthMethod(UserId userId, AuthenticationMethods.AuthenticationMethodId authMethod) {
      if (!this.isAllowedAsActive(userId, authMethod)) {
         throw new ViolationException("Authentication method '" + authMethod + "' cannot be set as active method");
      }

      ManagedUser user = this.getUserRepository().getById(userId);
      AuthenticationMethods authMethods = user.getAuthenticationMethods();
      Optional<AuthenticationMethods.AuthenticationMethodId> oldAuthMethod = authMethods.getActiveAuthMethod();
      authMethods.setActiveAuthMehod(authMethod);
      this.getUserRepository().update(user);
      ((AuthenticationMethodChangedEventFactory)this.eventFactory.get())
         .publishAuthenticationMethodChanged(
            userId,
            authMethodOf(authMethod),
            (com.airlock.iam.common.api.domain.model.authmethod.AuthenticationMethod)oldAuthMethod.map(AuthenticationMethodService::authMethodOf).orElse(null)
         );
   }

   private static SimpleAuthenticationMethod authMethodOf(AuthenticationMethods.AuthenticationMethodId authMethodId) {
      return new SimpleAuthenticationMethod(authMethodId.getId(), authMethodId.getId());
   }

   public void deleteAuthMethod(UserId userId) {
      ManagedUser user = this.getUserRepository().getById(userId);
      Optional<AuthenticationMethods.AuthenticationMethodId> activeAuthMethod = user.getAuthenticationMethods().getActiveAuthMethod();
      if (activeAuthMethod.isPresent()) {
         user.getAuthenticationMethods().clearActiveAuthMethod();
         this.getUserRepository().update(user);
         ((AuthenticationMethodChangedEventFactory)this.eventFactory.get())
            .publishAuthenticationMethodChanged(userId, null, authMethodOf(activeAuthMethod.get()));
      }
   }

   public Optional<AuthenticationMethod.AddAuthMethodErrorCode> addAuthenticationMethod(UserId userId, AuthenticationMethods.AuthenticationMethodId authMethod) {
      Optional<AuthenticationMethod> optionalAuthMethod = this.getPossibleNewAuthMethods(userId)
         .stream()
         .filter(method -> authMethod.equals(method.getConfiguredId()))
         .findFirst();
      if (optionalAuthMethod.isEmpty()) {
         return Optional.of(AuthenticationMethod.GenericAddAuthMethodErrorCodes.NO_SUCH_AUTH_METHOD);
      }

      Optional<AuthenticationMethod.AddAuthMethodErrorCode> errorCode = optionalAuthMethod.get().addToUser(userId);
      if (errorCode.isEmpty()) {
         ((SigningLogger)this.signingLogger.get()).log(String.format("Authentication method '%s' added to user '%s'", authMethod, userId));
      }

      return errorCode;
   }

   public void addToCreatedUser(UserId userId) {
      for (AuthenticationMethod authMethod : this.getAuthenticationMethods()) {
         authMethod.handleUserCreated(userId);
      }
   }

   public void deleteAdditionalAuthMethod(UserId userId, String key) {
      if (!this.isValidAdditionalAuthMethodKey(key)) {
         throw new ViolationException(String.format("%s is not a valid identifier of an additional authentication method.", key));
      }

      ManagedUser user = this.getUserRepository().getById(userId);
      user.getAuthenticationMethods().clearAdditionalAuthMethod(key);
      this.getUserRepository().update(user);
   }

   public void activateAdditionalAuthMethod(UserId userId, String key, AuthenticationMethods.AuthenticationMethodId authMethod) {
      if (!this.isValidAdditionalAuthMethodKey(key)) {
         throw new ViolationException(String.format("%s is not a valid identifier of an additional authentication method.", key));
      }

      if (!this.isAllowedAsActive(userId, authMethod)) {
         throw new ViolationException("Authentication method '" + authMethod + "' cannot be set as active authentication method");
      }

      ManagedUser user = this.getUserRepository().getById(userId);
      user.getAuthenticationMethods().setAdditionalAuthMethod(key, authMethod);
      this.getUserRepository().update(user);
   }

   public void migrateTo(UserId userId, Optional<Date> until, AuthenticationMethods.AuthenticationMethodId... nextAuthMethods) {
      this.migrateTo(userId, until, Arrays.stream(nextAuthMethods).collect(Collectors.toSet()));
   }

   public void migrateTo(UserId userId, Optional<Date> until, Set<AuthenticationMethods.AuthenticationMethodId> nextAuthMethods) {
      if (!this.isAllowedAsNext(userId, nextAuthMethods)) {
         throw new ViolationException("Authentication method '" + nextAuthMethods + "' cannot be set as next authentication method");
      }

      ManagedUser user = this.getUserRepository().getById(userId);
      user.getAuthenticationMethods().setMigration(nextAuthMethods, until);
      this.getUserRepository().update(user);
   }

   public void resetMigration(UserId userId) {
      ManagedUser user = this.getUserRepository().getById(userId);
      if (user.getAuthenticationMethods().hasMigration()) {
         user.getAuthenticationMethods().clearMigration();
         this.getUserRepository().update(user);
      }
   }

   public List<AuthenticationMethod> getPossibleActiveAuthMethods(UserId userId) {
      AuthenticationMethods authMethods = this.getUserRepository().getById(userId).getAuthenticationMethods();
      return this.getAuthenticationMethods()
         .stream()
         .filter(authMethod -> authMethods.hasActiveAuthMethod(authMethod.getConfiguredId()) || this.isAllowedAsActiveAuthMethod(authMethod, userId))
         .toList();
   }

   public List<AuthenticationMethod> getPossibleNextAuthMethods(UserId userId) {
      AuthenticationMethods authMethods = this.getUserRepository().getById(userId).getAuthenticationMethods();
      return this.getAuthenticationMethods()
         .stream()
         .filter(
            authMethod -> authMethods.hasActiveAuthMethod(authMethod.getConfiguredId()) && !authMethod.allowMigrationToSameToken()
               ? false
               : authMethod.isAllowedAsNextAuthMethod()
         )
         .toList();
   }

   public List<AuthenticationMethod> getAllPossibleNextAuthMethods() {
      return this.getAuthenticationMethods().stream().filter(AuthenticationMethod::isAllowedAsNextAuthMethod).toList();
   }

   public List<AuthenticationMethod> getPossibleNewAuthMethods(UserId userId) {
      return this.getAuthenticationMethods().stream().filter(authMethod -> this.isAllowedAsNewAuthMethod(authMethod, userId)).toList();
   }

   public List<AuthenticationMethod> getVisibleAuthenticationMethods(UserId userId) {
      return this.getAuthenticationMethods().stream().filter(authMethod -> this.isVisible(authMethod, userId)).toList();
   }

   private boolean isAllowedAsActiveAuthMethod(AuthenticationMethod authMethod, UserId userId) {
      try {
         return authMethod.isAllowedAsActiveAuthMethodForUser(userId);
      } catch (RuntimeException e) {
         LOG.warn(
            LogTypes.SERVER_INFO_SYS,
            "Exception while determining if authentication method " + LogUtil.quote(authMethod.getConfiguredId()) + " is allowed as active",
            e
         );
         return false;
      }
   }

   private boolean isAllowedAsNewAuthMethod(AuthenticationMethod authMethod, UserId userId) {
      try {
         return authMethod.isAllowedAsNewAuthMethod(userId);
      } catch (RuntimeException e) {
         LOG.warn(
            LogTypes.SERVER_INFO_SYS,
            "Exception while determining if authentication method " + LogUtil.quote(authMethod.getConfiguredId()) + " is allowed as new",
            e
         );
         return false;
      }
   }

   private boolean isVisible(AuthenticationMethod authMethod, UserId userId) {
      try {
         return authMethod.isVisible(userId);
      } catch (RuntimeException e) {
         LOG.warn(LogTypes.SERVER_INFO_SYS, "Exception while determining visibility of authentication method " + LogUtil.quote(authMethod.getConfiguredId()), e);
         return true;
      }
   }

   public Map<String, String> authMethodIdToTokenType() {
      return this.getAuthenticationMethods()
         .stream()
         .collect(Collectors.toMap(authMethd -> authMethd.getConfiguredId().getId(), authMethod -> authMethod.getTokenType().getId()));
   }

   public boolean hasAuthenticationMethod(String id) {
      return this.getAuthenticationMethods().stream().anyMatch(method -> method.getTokenType().getId().equals(id));
   }

   public List<AuthenticationMethod> getGenericAuthenticationMethods() {
      return this.getAuthenticationMethods().stream().filter(AuthenticationMethod::isGeneric).toList();
   }

   public List<String> getAdditionalActiveAuthMethodKeys() {
      return ((AuthenticationMethodSettings)this.settings.get()).additionalAuthMethodProperties();
   }

   private ManagedUserRepository getUserRepository() {
      return (ManagedUserRepository)this.userRepository.get();
   }

   private List<AuthenticationMethod> getAuthenticationMethods() {
      return (List<AuthenticationMethod>)this.authenticationMethods.get();
   }

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

   @SuppressFBWarnings(justification = "generated code")
   public static class AuthenticationMethodServiceBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private Provider<List<AuthenticationMethod>> authenticationMethods;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<ManagedUserRepository> userRepository;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<AuthenticationMethodSettings> settings;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<AuthenticationMethodChangedEventFactory> eventFactory;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<SigningLogger> signingLogger;

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

      @SuppressFBWarnings(justification = "generated code")
      public AuthenticationMethodService.AuthenticationMethodServiceBuilder authenticationMethods(Provider<List<AuthenticationMethod>> authenticationMethods) {
         this.authenticationMethods = authenticationMethods;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public AuthenticationMethodService.AuthenticationMethodServiceBuilder userRepository(Provider<ManagedUserRepository> userRepository) {
         this.userRepository = userRepository;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public AuthenticationMethodService.AuthenticationMethodServiceBuilder settings(Provider<AuthenticationMethodSettings> settings) {
         this.settings = settings;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public AuthenticationMethodService.AuthenticationMethodServiceBuilder eventFactory(Provider<AuthenticationMethodChangedEventFactory> eventFactory) {
         this.eventFactory = eventFactory;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public AuthenticationMethodService.AuthenticationMethodServiceBuilder signingLogger(Provider<SigningLogger> signingLogger) {
         this.signingLogger = signingLogger;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public AuthenticationMethodService build() {
         return new AuthenticationMethodService(this.authenticationMethods, this.userRepository, this.settings, this.eventFactory, this.signingLogger);
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "AuthenticationMethodService.AuthenticationMethodServiceBuilder(authenticationMethods="
            + this.authenticationMethods
            + ", userRepository="
            + this.userRepository
            + ", settings="
            + this.settings
            + ", eventFactory="
            + this.eventFactory
            + ", signingLogger="
            + this.signingLogger
            + ")";
      }
   }
}
