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

import com.airlock.iam.admin.api.domain.model.administrator.CurrentAdministrator;
import com.airlock.iam.admin.application.service.authmethod.AuthenticationMethodService;
import com.airlock.iam.admin.application.service.settings.AdminappSettingsService;
import com.airlock.iam.admin.application.service.token.password.PasswordAdminService;
import com.airlock.iam.base.api.domain.model.exception.ServiceConfigurationException;
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.api.domain.model.validation.ValidationResult;
import com.airlock.iam.base.api.domain.model.validation.Validator;
import com.airlock.iam.base.api.domain.model.validation.ValidatorError;
import com.airlock.iam.base.misc.adapt.Adapter;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.base.misc.result.Result;
import com.airlock.iam.common.api.domain.model.contextdata.ContextData;
import com.airlock.iam.common.api.domain.model.password.Password;
import com.airlock.iam.common.api.domain.model.store.user.DeletingUserStore;
import com.airlock.iam.common.api.domain.model.store.user.IdentityChangingUserStore;
import com.airlock.iam.common.api.domain.model.store.user.InsertingUserStore;
import com.airlock.iam.common.api.domain.model.store.user.UserStore;
import com.airlock.iam.common.api.domain.model.store.user.exception.UserEntityNotFoundException;
import com.airlock.iam.common.api.domain.model.store.user.exception.UserEntityNotUniqueException;
import com.airlock.iam.common.api.domain.model.user.PersistentUser;
import com.airlock.iam.common.application.service.password.PasswordPolicyViolationException;
import com.airlock.iam.common.domain.model.event.UserEventFactory;
import com.airlock.iam.core.domain.model.user.UserIdentityGenerator;
import com.airlock.iam.core.misc.impl.persistency.SimplePersistentUserWithPassword;
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.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Map.Entry;
import java.util.stream.Collectors;

public class UserAdminService {
   private final Provider<UserStore> userStore;
   private final Provider<Optional<PasswordAdminService>> passwordService;
   private final Provider<UserIdentityGenerator> userIdentityGenerator;
   private final Provider<ContextDataAdminService> contextDataService;
   private final Provider<SigningLogger> signingLogger;
   private final Provider<Validator<Password>> passwordValidator;
   private final Provider<Validator<String>> usernameValidator;
   private final Provider<AuthenticationMethodService> authMethodService;
   private final Provider<Optional<IdentityChangingUserStore>> identityChangingUserStore;
   private final Provider<CurrentAdministrator> currentAdministrator;
   private final Provider<UserEventFactory> userEventFactory;
   private final Provider<AdminappSettingsService> adminappSettingsService;

   @Inject
   public UserAdminService(
      Provider<UserStore> userStore,
      Provider<Optional<PasswordAdminService>> passwordService,
      Provider<UserIdentityGenerator> userIdentityGenerator,
      Provider<ContextDataAdminService> contextDataService,
      Provider<SigningLogger> signingLogger,
      Provider<Validator<Password>> passwordValidator,
      Provider<Validator<String>> usernameValidator,
      Provider<AuthenticationMethodService> authMethodService,
      Provider<Optional<IdentityChangingUserStore>> identityChangingUserStore,
      Provider<CurrentAdministrator> currentAdministrator,
      Provider<UserEventFactory> userEventFactory,
      Provider<AdminappSettingsService> adminappSettingsService
   ) {
      this.userStore = userStore;
      this.passwordService = passwordService;
      this.userIdentityGenerator = userIdentityGenerator;
      this.contextDataService = contextDataService;
      this.signingLogger = signingLogger;
      this.passwordValidator = passwordValidator;
      this.usernameValidator = usernameValidator;
      this.authMethodService = authMethodService;
      this.identityChangingUserStore = identityChangingUserStore;
      this.currentAdministrator = currentAdministrator;
      this.userEventFactory = userEventFactory;
      this.adminappSettingsService = adminappSettingsService;
   }

   public UserAdminService.CreationResult create(UserAdminService.CreateUser userCreate) throws UserEntityNotUniqueException, PasswordPolicyViolationException {
      UserId userId = userCreate.getUserId()
         .orElse(
            UserId.userId(((UserIdentityGenerator)this.userIdentityGenerator.get()).generateIdentity((UserStore)this.userStore.get(), Collections.emptyMap()))
         );
      ValidationResult validation = this.validate(userCreate);
      if (!validation.isValid()) {
         throw new ViolationException("The data provided to create a new user are invalid: " + validation.getFailure());
      }

      ContextData contextData = ((ContextDataAdminService)this.contextDataService.get()).createContextData(userCreate.getContextData());
      this.insertUser(userId, contextData);
      this.setPassword(userId, userCreate.getPassword());
      ((AuthenticationMethodService)this.authMethodService.get()).addToCreatedUser(userId);
      ((UserEventFactory)this.userEventFactory.get()).publishUserCreatedEvent(userId, contextData);
      ((ContextDataAdminService)this.contextDataService.get())
         .publishContextDataChangeEvents(userId, Collections.emptyMap(), userCreate.getContextData(), userCreate.getContextData().keySet());
      ((SigningLogger)this.signingLogger.get()).log("Created a user with ID \"%s\"", new Object[]{userId});
      return new UserAdminService.CreationResult(userId.getValue());
   }

   private void insertUser(UserId userId, ContextData contextData) {
      PersistentUser user = new SimplePersistentUserWithPassword(userId.getValue(), null, null);
      user.putAllContextData(contextData.asKeyValueMap());
      ((InsertingUserStore)Adapter.adapt(this.userStore.get()).to(InsertingUserStore.class)).insert(user);
   }

   private void setPassword(UserId userId, Optional<Password> password) {
      if (password.isPresent() && ((Optional)this.passwordService.get()).isEmpty()) {
         throw new ServiceConfigurationException("a password service is required to set a password");
      }

      if (password.isPresent()) {
         ((PasswordAdminService)((Optional)this.passwordService.get()).get())
            .setPassword(PasswordAdminService.AdminSetPassword.builder().userId(userId.getValue()).newPassword(password.get().getValue()).build());
      }
   }

   public UserAdminService.AdminappUser loadUser(String userId) throws UserEntityNotFoundException {
      PersistentUser user = (PersistentUser)((UserStore)this.userStore.get()).getByIdentity(userId);
      return new UserAdminService.AdminappUser(
         user, ((ContextDataAdminService)this.contextDataService.get()).filterAndTransformContextData(user.getContextData())
      );
   }

   public void deleteUser(UserId userId) {
      if (!((AdminappSettingsService)this.adminappSettingsService.get()).isDeletionAllowed(userId)) {
         throw new ViolationException("The user has the same name as the administrator. IAM has been configured to prevent this deletion.");
      }

      DeletingUserStore deletingUserStore = (DeletingUserStore)ServiceConfigurationException.checkPresent(
         Adapter.adapt(this.userStore.get()).ifPossibleTo(DeletingUserStore.class),
         "Deleting a user requires a User Store of type " + DeletingUserStore.class.getName()
      );
      ((UserEventFactory)this.userEventFactory.get()).publishUserDeletedEvent(userId);
      deletingUserStore.delete(userId.getValue());
      ((SigningLogger)this.signingLogger.get())
         .log(
            "The user "
               + LogUtil.quote(userId)
               + " was successfully deleted by administrator "
               + LogUtil.quote(((CurrentAdministrator)this.currentAdministrator.get()).getName())
         );
   }

   public Result<List<String>, List<ValidatorError>> updateContextData(UserId userId, Map<String, Object> contextDataChange) {
      PersistentUser user = ((UserStore)this.userStore.get()).getByIdentity(userId);
      Map<String, Object> oldContextData = new HashMap<>(user.getContextData());
      ValidationResult result = ((ContextDataAdminService)this.contextDataService.get()).validateContextDataUpdate(user.getContextData(), contextDataChange);
      if (result.isValid()) {
         Map<String, Object> newContextData = ((ContextDataAdminService)this.contextDataService.get())
            .updateContextData(user.getContextData(), contextDataChange);
         user.putAllContextData(newContextData);
         ((UserStore)this.userStore.get()).update(user);
         List<String> changedDataKeys = contextDataChange.keySet()
            .stream()
            .filter(key -> !Objects.equals(oldContextData.get(key), contextDataChange.get(key)))
            .toList();
         ((ContextDataAdminService)this.contextDataService.get())
            .publishContextDataChangeEvents(userId, oldContextData, contextDataChange, contextDataChange.keySet());
         ((SigningLogger)this.signingLogger.get())
            .log(
               "Administrator %s updated context-data %s for user %s",
               new Object[]{LogUtil.quote(((CurrentAdministrator)this.currentAdministrator.get()).getName()), changedDataKeys, LogUtil.quote(userId)}
            );
         return Result.result(changedDataKeys);
      } else {
         return Result.failure(result.getErrors());
      }
   }

   public ValidationResult validate(UserAdminService.CreateUser createCommand) {
      ValidationResult result = ValidationResult.valid();
      if (createCommand.password.isPresent()) {
         Password password = createCommand.password.get();
         result.mergeWith(((Validator)this.passwordValidator.get()).validate(password));
      }

      if (createCommand.userId.isPresent()) {
         result.mergeWith(this.validate(createCommand.userId.get().getValue()));
      }

      result.mergeWith(
         ((ContextDataAdminService)this.contextDataService.get()).validateContextDataUpdate(Collections.emptyMap(), createCommand.getContextData())
      );
      return result;
   }

   public ValidationResult validate(String username) {
      ValidationResult result = ((Validator)this.usernameValidator.get()).validate(username);
      if (result.isNotValid()) {
         List<ValidatorError> usernameErrors = result.getErrors()
            .stream()
            .map(e -> ValidatorError.validatorErrorBuilder("username", e.getType()).parameters(e.getParameters()).build())
            .collect(Collectors.toList());
         return ValidationResult.invalid(usernameErrors);
      } else {
         return result;
      }
   }

   public void changeUsername(String currentUsername, String newUsername) {
      ServiceConfigurationException.checkPresent(
         (Optional)this.identityChangingUserStore.get(), "Changing a username requires a User Store of type " + IdentityChangingUserStore.class.getName()
      );
      ValidationResult validationResult = this.validate(newUsername);
      if (!validationResult.isValid()) {
         throw new ViolationException("The data provided to change a username are invalid: " + validationResult.getFailure());
      }

      ((IdentityChangingUserStore)((Optional)this.identityChangingUserStore.get()).orElseThrow()).changeIdentity(currentUsername, newUsername);
      ((SigningLogger)this.signingLogger.get()).log("username of user %s has been changed to %s", new Object[]{currentUsername, newUsername});
   }

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

   public record AdminappUser(PersistentUser persistentUser, Map<String, Object> filteredContextData) {
   }

   public static final class CreateUser {
      private final Optional<UserId> userId;
      private final Optional<Password> password;
      private final Map<String, Object> contextData;

      @SuppressFBWarnings(justification = "generated code")
      CreateUser(Optional<UserId> userId, Optional<Password> password, Map<String, Object> contextData) {
         this.userId = userId;
         this.password = password;
         this.contextData = contextData;
      }

      @SuppressFBWarnings(justification = "generated code")
      public static UserAdminService.CreateUser.CreateUserBuilder builder() {
         return new UserAdminService.CreateUser.CreateUserBuilder();
      }

      @SuppressFBWarnings(justification = "generated code")
      public Optional<UserId> getUserId() {
         return this.userId;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Optional<Password> getPassword() {
         return this.password;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Map<String, Object> getContextData() {
         return this.contextData;
      }

      @SuppressFBWarnings(justification = "generated code")
      public static class CreateUserBuilder {
         @SuppressFBWarnings(justification = "generated code")
         private Optional<UserId> userId;
         @SuppressFBWarnings(justification = "generated code")
         private Optional<Password> password;
         @SuppressFBWarnings(justification = "generated code")
         private ArrayList<String> contextData$key;
         @SuppressFBWarnings(justification = "generated code")
         private ArrayList<Object> contextData$value;

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

         @SuppressFBWarnings(justification = "generated code")
         public UserAdminService.CreateUser.CreateUserBuilder userId(Optional<UserId> userId) {
            this.userId = userId;
            return this;
         }

         @SuppressFBWarnings(justification = "generated code")
         public UserAdminService.CreateUser.CreateUserBuilder password(Optional<Password> password) {
            this.password = password;
            return this;
         }

         @SuppressFBWarnings(justification = "generated code")
         public UserAdminService.CreateUser.CreateUserBuilder contextData(String contextDataKey, Object contextDataValue) {
            if (this.contextData$key == null) {
               this.contextData$key = new ArrayList<>();
               this.contextData$value = new ArrayList<>();
            }

            this.contextData$key.add(contextDataKey);
            this.contextData$value.add(contextDataValue);
            return this;
         }

         @SuppressFBWarnings(justification = "generated code")
         public UserAdminService.CreateUser.CreateUserBuilder contextData(Map<? extends String, ? extends Object> contextData) {
            if (contextData == null) {
               throw new NullPointerException("contextData cannot be null");
            }

            if (this.contextData$key == null) {
               this.contextData$key = new ArrayList<>();
               this.contextData$value = new ArrayList<>();
            }

            for (Entry<? extends String, ? extends Object> $lombokEntry : contextData.entrySet()) {
               this.contextData$key.add($lombokEntry.getKey());
               this.contextData$value.add($lombokEntry.getValue());
            }

            return this;
         }

         @SuppressFBWarnings(justification = "generated code")
         public UserAdminService.CreateUser.CreateUserBuilder clearContextData() {
            if (this.contextData$key != null) {
               this.contextData$key.clear();
               this.contextData$value.clear();
            }

            return this;
         }

         @SuppressFBWarnings(justification = "generated code")
         public UserAdminService.CreateUser build() {
            return new UserAdminService.CreateUser(
               this.userId,
               this.password,
               switch (this.contextData$key == null ? 0 : this.contextData$key.size()) {
                  case 0 -> Collections.emptyMap();
                  case 1 -> Collections.singletonMap(this.contextData$key.get(0), this.contextData$value.get(0));
                  default -> {
                     Map<String, Object> contextData = new LinkedHashMap<>(
                        this.contextData$key.size() < 1073741824 ? 1 + this.contextData$key.size() + (this.contextData$key.size() - 3) / 3 : Integer.MAX_VALUE
                     );

                     for (int $i = 0; $i < this.contextData$key.size(); $i++) {
                        contextData.put(this.contextData$key.get($i), this.contextData$value.get($i));
                     }

                     yield Collections.unmodifiableMap(contextData);
                  }
               }
            );
         }

         @SuppressFBWarnings(justification = "generated code")
         @Override
         public String toString() {
            return "UserAdminService.CreateUser.CreateUserBuilder(userId="
               + this.userId
               + ", password="
               + this.password
               + ", contextData$key="
               + this.contextData$key
               + ", contextData$value="
               + this.contextData$value
               + ")";
         }
      }
   }

   public record CreationResult(String username) {
   }

   @SuppressFBWarnings(justification = "generated code")
   public static class UserAdminServiceBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private Provider<UserStore> userStore;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<Optional<PasswordAdminService>> passwordService;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<UserIdentityGenerator> userIdentityGenerator;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<ContextDataAdminService> contextDataService;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<SigningLogger> signingLogger;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<Validator<Password>> passwordValidator;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<Validator<String>> usernameValidator;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<AuthenticationMethodService> authMethodService;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<Optional<IdentityChangingUserStore>> identityChangingUserStore;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<CurrentAdministrator> currentAdministrator;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<UserEventFactory> userEventFactory;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<AdminappSettingsService> adminappSettingsService;

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

      @SuppressFBWarnings(justification = "generated code")
      public UserAdminService.UserAdminServiceBuilder userStore(Provider<UserStore> userStore) {
         this.userStore = userStore;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UserAdminService.UserAdminServiceBuilder passwordService(Provider<Optional<PasswordAdminService>> passwordService) {
         this.passwordService = passwordService;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UserAdminService.UserAdminServiceBuilder userIdentityGenerator(Provider<UserIdentityGenerator> userIdentityGenerator) {
         this.userIdentityGenerator = userIdentityGenerator;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UserAdminService.UserAdminServiceBuilder contextDataService(Provider<ContextDataAdminService> contextDataService) {
         this.contextDataService = contextDataService;
         return this;
      }

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

      @SuppressFBWarnings(justification = "generated code")
      public UserAdminService.UserAdminServiceBuilder passwordValidator(Provider<Validator<Password>> passwordValidator) {
         this.passwordValidator = passwordValidator;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UserAdminService.UserAdminServiceBuilder usernameValidator(Provider<Validator<String>> usernameValidator) {
         this.usernameValidator = usernameValidator;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UserAdminService.UserAdminServiceBuilder authMethodService(Provider<AuthenticationMethodService> authMethodService) {
         this.authMethodService = authMethodService;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UserAdminService.UserAdminServiceBuilder identityChangingUserStore(Provider<Optional<IdentityChangingUserStore>> identityChangingUserStore) {
         this.identityChangingUserStore = identityChangingUserStore;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UserAdminService.UserAdminServiceBuilder currentAdministrator(Provider<CurrentAdministrator> currentAdministrator) {
         this.currentAdministrator = currentAdministrator;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UserAdminService.UserAdminServiceBuilder userEventFactory(Provider<UserEventFactory> userEventFactory) {
         this.userEventFactory = userEventFactory;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UserAdminService.UserAdminServiceBuilder adminappSettingsService(Provider<AdminappSettingsService> adminappSettingsService) {
         this.adminappSettingsService = adminappSettingsService;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public UserAdminService build() {
         return new UserAdminService(
            this.userStore,
            this.passwordService,
            this.userIdentityGenerator,
            this.contextDataService,
            this.signingLogger,
            this.passwordValidator,
            this.usernameValidator,
            this.authMethodService,
            this.identityChangingUserStore,
            this.currentAdministrator,
            this.userEventFactory,
            this.adminappSettingsService
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "UserAdminService.UserAdminServiceBuilder(userStore="
            + this.userStore
            + ", passwordService="
            + this.passwordService
            + ", userIdentityGenerator="
            + this.userIdentityGenerator
            + ", contextDataService="
            + this.contextDataService
            + ", signingLogger="
            + this.signingLogger
            + ", passwordValidator="
            + this.passwordValidator
            + ", usernameValidator="
            + this.usernameValidator
            + ", authMethodService="
            + this.authMethodService
            + ", identityChangingUserStore="
            + this.identityChangingUserStore
            + ", currentAdministrator="
            + this.currentAdministrator
            + ", userEventFactory="
            + this.userEventFactory
            + ", adminappSettingsService="
            + this.adminappSettingsService
            + ")";
      }
   }
}
