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

import com.airlock.iam.admin.domain.model.contextdata.AdminContextDataFactory;
import com.airlock.iam.admin.domain.service.access.AdminappAccessController;
import com.airlock.iam.admin.domain.service.contextdata.ContextDataCreateAdminService;
import com.airlock.iam.base.api.domain.model.email.EmailAddress;
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.GenericError;
import com.airlock.iam.base.api.domain.model.validation.ValidationResult;
import com.airlock.iam.base.api.domain.model.validation.ValidatorError;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.common.api.domain.model.contextdata.ContextData;
import com.airlock.iam.common.api.domain.model.contextdata.ContextDataItem;
import com.airlock.iam.common.api.domain.model.event.ContextDataChanged.ContextDataChange;
import com.airlock.iam.common.application.configuration.userprofile.UserProfileItemConfig.ProfileItemType;
import com.airlock.iam.common.domain.model.contextdata.ContextDataAccessController;
import com.airlock.iam.common.domain.model.contextdata.ContextDataFactory;
import com.airlock.iam.common.domain.model.contextdata.ContextDataValidator;
import com.airlock.iam.common.domain.model.event.ContextDataChangeEventFactory;
import com.airlock.iam.common.domain.model.event.EmailAddressChangesEventFactory;
import com.airlock.iam.common.domain.model.user.profileitems.StringProfileItemLogic;
import com.airlock.iam.common.domain.model.user.profileitems.UserProfileItemDetails;
import com.airlock.iam.common.domain.model.user.profileitems.UserProfileItems;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.log.api.signing.SigningLogger;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.annotation.Nullable;
import jakarta.inject.Inject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;

public final class ContextDataAdminServiceImpl implements ContextDataAdminService {
   private static final ELogger LOG = ELoggerFactory.getLogger(ContextDataAdminServiceImpl.class);
   private final AdminappAccessController accessController;
   private final UserProfileItems userProfileItems;
   private final SigningLogger signingLogger;
   private final ContextDataCreateAdminService contextDataCreateAdminService;
   private final EmailAddressChangesEventFactory emailEventFactory;
   private final ContextDataChangeEventFactory contextDataEventFactory;

   @Inject
   public ContextDataAdminServiceImpl(
      AdminappAccessController accessController,
      UserProfileItems userProfileItems,
      SigningLogger signingLogger,
      ContextDataCreateAdminService contextDataCreateAdminService,
      EmailAddressChangesEventFactory emailEventFactory,
      ContextDataChangeEventFactory contextDataEventFactory
   ) {
      this.accessController = accessController;
      this.userProfileItems = userProfileItems;
      this.signingLogger = signingLogger;
      this.contextDataCreateAdminService = contextDataCreateAdminService;
      this.emailEventFactory = emailEventFactory;
      this.contextDataEventFactory = contextDataEventFactory;
   }

   @Override
   public Map<String, Object> filterAndTransformContextData(Map<String, Object> persistentContextData) {
      ContextData contextData = this.contextDataFactory().fromPersistency(persistentContextData);
      return contextData.filter(ContextDataItem::isVisible).asKeyValueMap();
   }

   @Override
   public ValidationResult validateContextDataFilterNames(List<String> contextDataNames) {
      List<ValidatorError> validatorErrors = new ArrayList<>();

      for (String contextDataName : contextDataNames) {
         if (!this.isStringContextData(contextDataName) || !this.accessController.isContextDataColumnVisible(contextDataName)) {
            LOG.debug("Context-data name " + LogUtil.logSafeQuote(contextDataName) + " is not of type String or access is not allowed.");
            validatorErrors.add(ValidatorError.validatorError(contextDataName, GenericError.INVALID_VALUE));
         }
      }

      return validatorErrors.isEmpty() ? ValidationResult.valid() : ValidationResult.invalid(validatorErrors);
   }

   @Override
   public ValidationResult validateSortableContextDataNames(List<String> contextDataNames) {
      List<ValidatorError> validatorErrors = new ArrayList<>();

      for (String contextDataName : contextDataNames) {
         Optional<UserProfileItemDetails> sortableItem = this.userProfileItems.forContextData(contextDataName).filter(UserProfileItemDetails::isSortable);
         if (sortableItem.isEmpty() || !this.accessController.isContextDataColumnVisible(contextDataName)) {
            LOG.debug("Context-data name " + LogUtil.logSafeQuote(contextDataName) + " is not sortable or access is not allowed.");
            validatorErrors.add(ValidatorError.validatorError(contextDataName, GenericError.INVALID_VALUE));
         }
      }

      return validatorErrors.isEmpty() ? ValidationResult.valid() : ValidationResult.invalid(validatorErrors);
   }

   private boolean isStringContextData(String contextDataName) {
      return this.userProfileItems.forContextData(contextDataName).filter(item -> isStringOrList(item.getType())).isPresent();
   }

   private static boolean isStringOrList(ProfileItemType type) {
      return ProfileItemType.STRING == type || ProfileItemType.LIST == type;
   }

   @Override
   public Map<String, Object> updateContextData(Map<String, Object> persistentData, Map<String, Object> changedData) {
      ContextData existingContextData = this.contextDataFactory().fromPersistency(persistentData);
      ContextData newContextData = this.contextDataFactory().fromInput(changedData);
      this.validate(existingContextData, newContextData);
      Map<String, Object> result = new HashMap<>(persistentData);
      result.putAll(newContextData.asKeyValueMap());
      return result;
   }

   @Override
   public void publishContextDataChangeEvents(UserId userId, Map<String, Object> oldContextData, Map<String, Object> newContextData, Set<String> affectedFields) {
      Set<ContextDataChange> contextDataChanges = new HashSet<>();

      for (String field : affectedFields) {
         Object oldValue = oldContextData.get(field);
         Object newValue = newContextData.get(field);
         if (!Objects.equals(oldValue, newValue)) {
            contextDataChanges.add(new ContextDataChange(field, oldValue, newValue));
            Optional<UserProfileItemDetails> profileItem = this.userProfileItems.forContextData(field);
            if (profileItem.isPresent() && profileItem.get().getLogic() instanceof StringProfileItemLogic stringItem && stringItem.isEmailAddress()) {
               this.publishEmailChangeEvents(userId, oldValue, newValue);
            }
         }
      }

      if (!contextDataChanges.isEmpty()) {
         this.contextDataEventFactory.publishContextDataChanged(userId, contextDataChanges);
      }
   }

   private void publishEmailChangeEvents(UserId userId, @Nullable Object oldValue, @Nullable Object newValue) {
      Optional<EmailAddress> oldAddress = toEmailAddress(oldValue);
      Optional<EmailAddress> newAddress = toEmailAddress(newValue);
      if (isChange(oldValue, newValue) && oldAddress.isPresent() && newAddress.isPresent()) {
         this.emailEventFactory.publishEmailAddressChangedEvent(userId, oldAddress.get(), newAddress.get());
      } else if (isAddition(oldValue, newValue) && newAddress.isPresent()) {
         this.emailEventFactory.publishEmailAddressAddedEvent(userId, newAddress.get());
      } else if (isDeletion(oldValue, newValue) && oldAddress.isPresent()) {
         this.emailEventFactory.publishEmailAddressDeletedEvent(userId, oldAddress.get());
      }
   }

   private static boolean isChange(Object oldValue, Object newValue) {
      return !isNullOrEmpty(oldValue) && !isNullOrEmpty(newValue) ? !Objects.equals(oldValue, newValue) : false;
   }

   private static boolean isAddition(Object oldValue, Object newValue) {
      return isNullOrEmpty(oldValue) && !isNullOrEmpty(newValue);
   }

   private static boolean isDeletion(Object oldValue, Object newValue) {
      return !isNullOrEmpty(oldValue) && isNullOrEmpty(newValue);
   }

   private static boolean isNullOrEmpty(Object value) {
      return value == null || value instanceof String stringValue && StringUtils.isEmpty(stringValue);
   }

   private static Optional<EmailAddress> toEmailAddress(@Nullable Object value) {
      return Optional.ofNullable(value)
         .filter(String.class::isInstance)
         .map(String.class::cast)
         .map(StringUtils::trimToNull)
         .flatMap(EmailAddress::emailAddress);
   }

   @Override
   public ContextData createContextData(Map<String, Object> createContextData) {
      ContextData existingContextData = this.contextDataFactory().fromPersistency(new HashMap());
      Map<String, Object> contextData = new HashMap<>(createContextData);
      contextData.putAll(this.contextDataCreateAdminService.createContextData());
      ContextData newContextData = this.contextDataFactory().fromInput(contextData);
      this.validate(existingContextData, newContextData);
      return newContextData;
   }

   private void validate(ContextData existingContextData, ContextData newContextData) {
      ValidationResult result = this.contextDataValidator().validate(existingContextData, newContextData, false);
      if (!result.isValid()) {
         this.signingLogger.log("Update of context data failed due to validation errors.");
         throw new ViolationException("The context data changes are not valid. " + result.getFailure());
      }
   }

   @Override
   public ValidationResult validateContextDataUpdate(Map<String, Object> persistentData, Map<String, Object> changedData) {
      ContextData existingContextData = this.contextDataFactory().fromPersistency(persistentData);
      ContextData newContextData = this.contextDataFactory().fromInput(changedData);
      return this.contextDataValidator().validate(existingContextData, newContextData, false);
   }

   private ContextDataValidator contextDataValidator() {
      Set<String> requiredProfileItems = this.userProfileItems
         .getRequiredProfileItems()
         .stream()
         .<String>map(UserProfileItemDetails::getName)
         .collect(Collectors.toSet());
      return new ContextDataValidator(requiredProfileItems);
   }

   private ContextDataFactory contextDataFactory() {
      return new AdminContextDataFactory(this.userProfileItems, new ContextDataAdminServiceImpl.ContextDataAdminappAccessController(this.accessController));
   }

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

   @SuppressFBWarnings(justification = "generated code")
   public static class ContextDataAdminServiceImplBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private AdminappAccessController accessController;
      @SuppressFBWarnings(justification = "generated code")
      private UserProfileItems userProfileItems;
      @SuppressFBWarnings(justification = "generated code")
      private SigningLogger signingLogger;
      @SuppressFBWarnings(justification = "generated code")
      private ContextDataCreateAdminService contextDataCreateAdminService;
      @SuppressFBWarnings(justification = "generated code")
      private EmailAddressChangesEventFactory emailEventFactory;
      @SuppressFBWarnings(justification = "generated code")
      private ContextDataChangeEventFactory contextDataEventFactory;

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

      @SuppressFBWarnings(justification = "generated code")
      public ContextDataAdminServiceImpl.ContextDataAdminServiceImplBuilder accessController(AdminappAccessController accessController) {
         this.accessController = accessController;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public ContextDataAdminServiceImpl.ContextDataAdminServiceImplBuilder userProfileItems(UserProfileItems userProfileItems) {
         this.userProfileItems = userProfileItems;
         return this;
      }

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

      @SuppressFBWarnings(justification = "generated code")
      public ContextDataAdminServiceImpl.ContextDataAdminServiceImplBuilder contextDataCreateAdminService(
         ContextDataCreateAdminService contextDataCreateAdminService
      ) {
         this.contextDataCreateAdminService = contextDataCreateAdminService;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public ContextDataAdminServiceImpl.ContextDataAdminServiceImplBuilder emailEventFactory(EmailAddressChangesEventFactory emailEventFactory) {
         this.emailEventFactory = emailEventFactory;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public ContextDataAdminServiceImpl.ContextDataAdminServiceImplBuilder contextDataEventFactory(ContextDataChangeEventFactory contextDataEventFactory) {
         this.contextDataEventFactory = contextDataEventFactory;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public ContextDataAdminServiceImpl build() {
         return new ContextDataAdminServiceImpl(
            this.accessController,
            this.userProfileItems,
            this.signingLogger,
            this.contextDataCreateAdminService,
            this.emailEventFactory,
            this.contextDataEventFactory
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "ContextDataAdminServiceImpl.ContextDataAdminServiceImplBuilder(accessController="
            + this.accessController
            + ", userProfileItems="
            + this.userProfileItems
            + ", signingLogger="
            + this.signingLogger
            + ", contextDataCreateAdminService="
            + this.contextDataCreateAdminService
            + ", emailEventFactory="
            + this.emailEventFactory
            + ", contextDataEventFactory="
            + this.contextDataEventFactory
            + ")";
      }
   }

   private static final class ContextDataAdminappAccessController implements ContextDataAccessController {
      private AdminappAccessController accessController;

      public boolean isEditable(String key) {
         return this.accessController.isContextDataColumnEditable(key);
      }

      public boolean isVisible(String key) {
         return this.accessController.isContextDataColumnVisible(key);
      }

      @SuppressFBWarnings(justification = "generated code")
      public ContextDataAdminappAccessController(AdminappAccessController accessController) {
         this.accessController = accessController;
      }
   }
}
