package com.airlock.iam.plugin.config.manipulator;

import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.elog.api.EDefaultLogType;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.plugin.api.config.ConfigurationContext;
import com.airlock.iam.plugin.api.config.ConfigurationEnvironment;
import com.airlock.iam.plugin.api.config.PluginConfiguration;
import com.airlock.iam.plugin.api.property.PropertyConfiguration;
import com.airlock.iam.plugin.framework.application.configuration.MutablePluginConfiguration;
import com.airlock.iam.plugin.framework.misc.config.PluginConfigurationDictionary;
import com.airlock.iam.plugin.framework.misc.config.modification.AbstractConfigModificationItem;
import com.airlock.iam.plugin.framework.misc.config.modification.ConfigModificationItem;
import com.airlock.iam.plugin.framework.misc.config.modification.connect.ConfigConnectModificationItem;
import com.airlock.iam.plugin.framework.misc.config.modification.connect.ConfigConnectPluginModificationItem;
import com.airlock.iam.plugin.framework.misc.config.modification.connect.ConfigConnectToListModificationItem;
import com.airlock.iam.plugin.framework.misc.config.modification.connect.ConfigConnectToMapModificationItem;
import com.airlock.iam.plugin.framework.misc.config.modification.connect.ConfigInsertionPosition;
import com.airlock.iam.plugin.framework.misc.config.modification.property.ConfigDeletionTarget;
import com.airlock.iam.plugin.framework.misc.config.modification.property.ConfigInsertSimpleListElementModificationItem;
import com.airlock.iam.plugin.framework.misc.config.modification.property.ConfigRemoveListElementModificationItem;
import com.airlock.iam.plugin.framework.misc.config.modification.property.ConfigRemovePropertyModificationItem;
import com.airlock.iam.plugin.framework.misc.config.modification.property.ConfigSetSimplePropertyModificationItem;
import com.airlock.iam.plugin.framework.misc.config.properties.PluginListPropertyConfiguration;
import com.airlock.iam.plugin.framework.misc.config.properties.PluginMapPropertyConfiguration;
import com.airlock.iam.plugin.framework.misc.config.properties.PluginPropertyConfiguration;
import com.airlock.iam.plugin.framework.misc.config.properties.SimpleListPropertyConfiguration;
import com.airlock.iam.plugin.framework.misc.config.properties.SimplePropertyConfiguration;
import com.airlock.iam.plugin.framework.misc.shortnames.ShortNameMappingFactory;
import com.airlock.iam.plugin.framework.misc.util.PluginConfigurationDictionaryUtils;
import com.airlock.iam.system.config.ConfigFormat;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.function.ToIntFunction;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class ConfigManipulationService {
   private static final ELogger LOG = ELoggerFactory.getLogger(ConfigManipulationService.class);
   private static final Map<Class<? extends AbstractConfigModificationItem>, String> MODIFICATION_ITEM_TYPE_TO_IDENTIFIER = Map.of(
      ConfigConnectPluginModificationItem.class,
      "Connect Plugin",
      ConfigConnectToListModificationItem.class,
      "Connect Plugin To List",
      ConfigConnectToMapModificationItem.class,
      "Connect Plugin To Map",
      ConfigRemovePropertyModificationItem.class,
      "Remove Property",
      ConfigSetSimplePropertyModificationItem.class,
      "Set Simple Property",
      ConfigInsertSimpleListElementModificationItem.class,
      "Insert Simple List Element",
      ConfigRemoveListElementModificationItem.class,
      "Remove List Element"
   );
   private final PluginConfigurationDictionary configDictionary;
   private final String configSource;

   public ConfigManipulationService(Path configFile) {
      this.configDictionary = PluginConfigurationDictionaryUtils.loadDictionaryFromFile(
         configFile, ConfigFormat.YAML_FORMAT, Optional::empty, ConfigurationEnvironment::common
      );
      this.configSource = String.valueOf(configFile.getFileName());
   }

   public ConfigManipulationService(PluginConfigurationDictionary configDictionary, String configSource) {
      this.configDictionary = configDictionary;
      this.configSource = configSource;
   }

   public SnippetImportResult importConfigurationSnippet(Path snippetFile, OnDisplayNameConflict onDisplayNameConflict, Optional<String> idSuffix) {
      PluginConfigurationDictionary snippetDictionary = PluginConfigurationDictionaryUtils.loadDictionaryFromFile(
         snippetFile, ConfigFormat.YAML_FORMAT, this.configDictionary::getSensitiveValuesProvider, this.configDictionary.getEnvironment()
      );
      this.validateAndResolveIdConflictsInSnippet(idSuffix, snippetDictionary);
      this.validateAndResolveDisplayNameConflictsInSnippet(onDisplayNameConflict, snippetDictionary);
      this.mergePluginTreeRootsFromSnippetIntoConfiguration(snippetFile, snippetDictionary);
      return this.applyModificationsFromSnippet(snippetFile, snippetDictionary.getModifications());
   }

   private void validateAndResolveIdConflictsInSnippet(Optional<String> idSuffix, PluginConfigurationDictionary snippetDictionary) {
      if (idSuffix.isPresent()) {
         Map<String, String> originalToSuffixedUuids = this.addSuffixToAllPluginIds(snippetDictionary, idSuffix.get());
         this.addSuffixToAllModificationIds(snippetDictionary.getModifications(), originalToSuffixedUuids);
      }

      this.checkForIdConflicts(this.configDictionary, snippetDictionary, idSuffix.isPresent());
   }

   private Map<String, String> addSuffixToAllPluginIds(PluginConfigurationDictionary snippetDictionary, String suffix) {
      Map<String, String> originalToSuffixedUuid = new LinkedHashMap<>();
      snippetDictionary.getAllPluginConfigurations().stream().filter(Predicate.not(PluginConfiguration::isSynthetic)).forEach(config -> {
         String oldUuid = config.getUuid();
         String newUuid = oldUuid + "-" + suffix;
         config.setUuid(newUuid);
         originalToSuffixedUuid.put(oldUuid, newUuid);
      });
      return originalToSuffixedUuid;
   }

   private void addSuffixToAllModificationIds(List<ConfigModificationItem> modifications, Map<String, String> originalToSuffixedUuids) {
      for (ConfigModificationItem modification : modifications) {
         if (originalToSuffixedUuids.containsKey(modification.targetId())) {
            modification.setTargetId(originalToSuffixedUuids.get(modification.targetId()));
         }

         if (modification instanceof ConfigConnectModificationItem connectModification && originalToSuffixedUuids.containsKey(connectModification.sourceId())) {
            connectModification.setSourceId(originalToSuffixedUuids.get(connectModification.sourceId()));
         }
      }
   }

   private void checkForIdConflicts(PluginConfigurationDictionary configDictionary, PluginConfigurationDictionary snippetDictionary, boolean hasIdSuffix) {
      Set<String> configIds = new HashSet<>(configDictionary.getAllPluginConfigurations().stream().map(PluginConfiguration::getUuid).toList());
      Set<String> snippetIds = snippetDictionary.getAllPluginConfigurations().stream().<String>map(PluginConfiguration::getUuid).collect(Collectors.toSet());
      configIds.retainAll(snippetIds);
      if (!configIds.isEmpty()) {
         if (hasIdSuffix) {
            throw new DuplicateIdsDuringSnippetImportException(configIds, "Please specify another unique suffix.");
         } else {
            throw new DuplicateIdsDuringSnippetImportException(
               configIds, "To automatically append a suffix to every ID in the snippet, specify a unique suffix using --id-suffix."
            );
         }
      }
   }

   private void validateAndResolveDisplayNameConflictsInSnippet(OnDisplayNameConflict onDisplayNameConflict, PluginConfigurationDictionary snippetDictionary) {
      if (onDisplayNameConflict == OnDisplayNameConflict.FAIL) {
         this.checkForDisplayNameConflicts(this.configDictionary, snippetDictionary);
      } else {
         this.resolveDisplayNameConflicts(this.configDictionary, snippetDictionary);
      }
   }

   private void checkForDisplayNameConflicts(PluginConfigurationDictionary configDictionary, PluginConfigurationDictionary snippetDictionary) {
      Set<String> configDisplayNames = new HashSet<>(this.streamOfDisplayNames(configDictionary).toList());
      Set<String> snippetDisplayNames = this.streamOfDisplayNames(snippetDictionary).collect(Collectors.toSet());
      configDisplayNames.retainAll(snippetDisplayNames);
      if (!configDisplayNames.isEmpty()) {
         throw new DuplicateDisplayNamesDuringSnippetImportException(
            configDisplayNames, "To automatically rename conflicting display names, specify --on-display-name-conflict=rename."
         );
      }
   }

   private Stream<String> streamOfDisplayNames(PluginConfigurationDictionary configDictionary) {
      return configDictionary.getAllPluginConfigurations().stream().<String>map(PluginConfiguration::getPluginDisplayName).filter(Objects::nonNull);
   }

   private void resolveDisplayNameConflicts(PluginConfigurationDictionary configDictionary, PluginConfigurationDictionary snippetDictionary) {
      Set<String> configDisplayNames = new HashSet<>(this.streamOfDisplayNames(configDictionary).toList());
      snippetDictionary.getAllPluginConfigurations()
         .stream()
         .filter(c -> c.getPluginDisplayName() != null)
         .forEach(mutablePluginConfiguration -> checkAndResolveDisplayNameConflict(mutablePluginConfiguration, configDisplayNames));
   }

   private static void checkAndResolveDisplayNameConflict(MutablePluginConfiguration pluginConfiguration, Set<String> existingDisplayNames) {
      if (existingDisplayNames.contains(pluginConfiguration.getPluginDisplayName())) {
         int i = 1;

         String displayNameCandidate;
         do {
            displayNameCandidate = pluginConfiguration.getPluginDisplayName() + "-" + i++;
         } while (existingDisplayNames.contains(displayNameCandidate));

         LOG.info(
            "Renaming display name of plugin from snippet from %s to %s due to a conflict with an existing display name"
               .formatted(LogUtil.logSafeQuote(pluginConfiguration.getPluginDisplayName()), LogUtil.logSafeQuote(displayNameCandidate))
         );
         pluginConfiguration.setPluginDisplayName(displayNameCandidate);
      }

      existingDisplayNames.add(pluginConfiguration.getPluginDisplayName());
   }

   private void mergePluginTreeRootsFromSnippetIntoConfiguration(Path snippetFile, PluginConfigurationDictionary snippetDictionary) {
      LOG.info(
         String.format(
            "Merging %d plugin tree roots from %s into %s.", snippetDictionary.getConfigTreeRoots().size(), snippetFile.getFileName(), this.configSource
         )
      );
      snippetDictionary.getConfigTreeRoots().forEach(this.configDictionary::addConfigTreeRootPluginConfiguration);
   }

   private SnippetImportResult applyModificationsFromSnippet(Path snippetFile, List<ConfigModificationItem> modifications) {
      if (modifications.isEmpty()) {
         LOG.info("Snippet import completed successfully.");
         return new SnippetImportResult(this.configDictionary, List.of());
      }

      LOG.info(String.format("Applying configuration modifications from snippet %s.", snippetFile.getFileName()));
      SnippetImportResult result = new SnippetImportResult(this.configDictionary, this.applyConfigModifications(modifications));
      if (result.hasFailures()) {
         LOG.warn(
            EDefaultLogType.GENERIC_LOG_TYPE,
            String.format(
               "Snippet import aborted due to failures while applying modifications. All changes have been rolled back. Summary: VALID=%d SKIPPED=%s FAILED=%d",
               result.getValidModificationsCount(),
               result.getSkippedModificationCount(),
               result.getFailureModificationCount()
            )
         );
      } else {
         LOG.info(
            String.format(
               "Snippet import completed successfully including modifications. Summary: VALID=%d SKIPPED=%s",
               result.getValidModificationsCount(),
               result.getSkippedModificationCount()
            )
         );
      }

      this.logModificationResults(result.modificationResults());
      return result;
   }

   private void logModificationResults(List<ModificationResult> modificationResults) {
      for (ModificationResult modificationResult : modificationResults) {
         String logMessage = String.format("%s: %s - %s", modificationResult.status().name(), modificationResult.identifier(), modificationResult.message());
         if (modificationResult.isFailed()) {
            LOG.warn(EDefaultLogType.GENERIC_LOG_TYPE, logMessage);
         } else {
            LOG.info(logMessage);
         }
      }
   }

   private List<ModificationResult> applyConfigModifications(List<ConfigModificationItem> modifications) {
      return modifications.stream().map(this::applyModification).toList();
   }

   private ModificationResult applyModification(ConfigModificationItem modification) {
      String modificationIdentifier = MODIFICATION_ITEM_TYPE_TO_IDENTIFIER.get(modification.getClass());

      try {
         this.actionForModification(modification).run();
         return new ModificationResult(
            ModificationStatus.VALID, modificationIdentifier, this.successMessageForModification(modification, PropertyInfo.toPropertyInfo(modification))
         );
      } catch (SkippableConfigManipulationException e) {
         return modification.failOnError().orElse(true)
            ? new ModificationResult(ModificationStatus.FAILED, modificationIdentifier, e.getMessage())
            : new ModificationResult(ModificationStatus.SKIPPED, modificationIdentifier, e.getMessage());
      } catch (UnskippableConfigManipulationException e) {
         return new ModificationResult(ModificationStatus.FAILED, modificationIdentifier, e.getMessage());
      }
   }

   private Runnable actionForModification(ConfigModificationItem modification) {
      this.validateTargetType(modification);

      return switch (modification) {
         case ConfigConnectPluginModificationItem pluginModification -> () -> this.connect(ConnectInfo.toConnectInfo(pluginModification));
         case ConfigConnectToMapModificationItem toMapModification -> () -> this.connectToMap(
            ConnectInfo.toConnectInfo(toMapModification), toMapModification.key()
         );
         case ConfigConnectToListModificationItem toListModification -> this.connectToPluginList(toListModification);
         case ConfigRemovePropertyModificationItem removePropertyItem -> () -> this.removeProperty(PropertyInfo.toPropertyInfo(removePropertyItem));
         case ConfigSetSimplePropertyModificationItem setSimplePropertyItem -> () -> this.setSimpleProperty(
            PropertyInfo.toPropertyInfo(setSimplePropertyItem), setSimplePropertyItem.value()
         );
         case ConfigInsertSimpleListElementModificationItem insertSimpleListElementItem -> this.insertIntoSimpleList(insertSimpleListElementItem);
         case ConfigRemoveListElementModificationItem removeListElementItem -> this.removeListElement(removeListElementItem);
         default -> throw new IllegalStateException("Unexpected modification: " + modification.getClass().getSimpleName());
      };
   }

   private void validateTargetType(ConfigModificationItem modification) {
      MutablePluginConfiguration plugin = this.configDictionary.getByUuid(modification.targetId());
      if (plugin != null && !modification.targetType().equals(plugin.getPluginClassName())) {
         throw new UnskippableConfigManipulationException(
            "The referenced plugin with id '%s' has type '%s', but the specified modification type is '%s'."
               .formatted(
                  modification.targetId(),
                  Optional.ofNullable(this.configDictionary.getConfigVersion())
                     .flatMap(ShortNameMappingFactory.INSTANCE::getMapping)
                     .flatMap(mapping -> mapping.retrieveShortNameForType(plugin.getPluginClassName()))
                     .orElse(plugin.getPluginClassName()),
                  modification.targetType()
               )
         );
      }
   }

   private String successMessageForModification(ConfigModificationItem modification, PropertyInfo propertyInfo) {
      return switch (modification) {
         case ConfigConnectPluginModificationItem pluginMod -> "Successfully connected plugin '%s' to property '%s' of plugin '%s' (context: %s, environment: %s)."
            .formatted(pluginMod.sourceId(), pluginMod.targetProperty(), pluginMod.targetId(), propertyInfo.context(), propertyInfo.environment());
         case ConfigConnectToMapModificationItem toMapMod -> "Successfully connected plugin '%s' to map property '%s' with key '%s' of plugin '%s' (context: %s, environment: %s)."
            .formatted(toMapMod.sourceId(), toMapMod.targetProperty(), toMapMod.key(), toMapMod.targetId(), propertyInfo.context(), propertyInfo.environment());
         case ConfigRemovePropertyModificationItem removeProperty -> "Successfully removed property '%s' of plugin '%s' (context: %s, environment: %s)."
            .formatted(removeProperty.targetProperty(), removeProperty.targetId(), propertyInfo.context(), propertyInfo.environment());
         case ConfigSetSimplePropertyModificationItem setSimpleMod -> "Successfully set simple property '%s' of plugin '%s' to '%s' (context: %s, environment: %s)."
            .formatted(
               setSimpleMod.targetProperty(), setSimpleMod.targetId(), setSimpleMod.value().orElse(null), propertyInfo.context(), propertyInfo.environment()
            );
         case ConfigConnectToListModificationItem toListMod -> this.successMessageForConnectToListModification(toListMod, propertyInfo);
         case ConfigInsertSimpleListElementModificationItem insertSimpleListMod -> this.successMessageForInsertSimpleListElementModification(
            insertSimpleListMod, propertyInfo
         );
         case ConfigRemoveListElementModificationItem removeListElementMod -> this.successMessageForRemoveListElementModification(
            removeListElementMod, propertyInfo
         );
         default -> throw new IllegalStateException("Unexpected modification: " + modification.getClass().getSimpleName());
      };
   }

   private String successMessageForConnectToListModification(ConfigConnectToListModificationItem toListMod, PropertyInfo propertyInfo) {
      return switch (toListMod.position()) {
         case ConfigInsertionPosition.ConfigAtIndex index -> "Successfully connected plugin '%s' to list property '%s' of plugin '%s' at index %d (context: %s, environment: %s)."
            .formatted(
               toListMod.sourceId(), toListMod.targetProperty(), toListMod.targetId(), index.index(), propertyInfo.context(), propertyInfo.environment()
            );
         case ConfigInsertionPosition.ConfigAfter after -> "Successfully connected plugin '%s' to list property '%s' of plugin '%s' after plugin '%s' (context: %s, environment: %s)."
            .formatted(
               toListMod.sourceId(), toListMod.targetProperty(), toListMod.targetId(), after.plugin(), propertyInfo.context(), propertyInfo.environment()
            );
         case ConfigInsertionPosition.ConfigBefore before -> "Successfully connected plugin '%s' to list property '%s' of plugin '%s' before plugin '%s' (context: %s, environment: %s)."
            .formatted(
               toListMod.sourceId(), toListMod.targetProperty(), toListMod.targetId(), before.plugin(), propertyInfo.context(), propertyInfo.environment()
            );
         case ConfigInsertionPosition.ConfigAtEnd ignored -> "Successfully connected plugin '%s' to the end of list property '%s' of plugin '%s' (context: %s, environment: %s)."
            .formatted(toListMod.sourceId(), toListMod.targetProperty(), toListMod.targetId(), propertyInfo.context(), propertyInfo.environment());
         default -> throw new MatchException(null, null);
      };
   }

   private String successMessageForInsertSimpleListElementModification(
      ConfigInsertSimpleListElementModificationItem insertElementMod, PropertyInfo propertyInfo
   ) {
      return switch (insertElementMod.position()) {
         case ConfigInsertionPosition.ConfigAtIndex index -> "Successfully set value '%s' at index %d of simple list property '%s' of plugin '%s' (context: %s, environment: %s)."
            .formatted(
               insertElementMod.value(),
               index.index(),
               insertElementMod.targetProperty(),
               insertElementMod.targetId(),
               propertyInfo.context(),
               propertyInfo.environment()
            );
         case ConfigInsertionPosition.ConfigAtEnd ignored -> "Successfully set value '%s' at the end of simple list property '%s' of plugin '%s' (context: %s, environment: %s)."
            .formatted(
               insertElementMod.value(), insertElementMod.targetProperty(), insertElementMod.targetId(), propertyInfo.context(), propertyInfo.environment()
            );
         default -> throw new IllegalStateException("Unexpected insertion position: " + insertElementMod.position().getClass().getSimpleName());
      };
   }

   private String successMessageForRemoveListElementModification(ConfigRemoveListElementModificationItem removeElementMod, PropertyInfo propertyInfo) {
      return switch (removeElementMod.deletionTarget()) {
         case ConfigDeletionTarget.ConfigByIndex byIndex -> "Successfully removed list element at index %d from list property '%s' of plugin '%s' (context: %s, environment: %s)."
            .formatted(byIndex.index(), removeElementMod.targetProperty(), removeElementMod.targetId(), propertyInfo.context(), propertyInfo.environment());
         case ConfigDeletionTarget.ConfigByValue byValue -> "Successfully removed list element with value '%s' from list property '%s' of plugin '%s' (context: %s, environment: %s)."
            .formatted(byValue.value(), removeElementMod.targetProperty(), removeElementMod.targetId(), propertyInfo.context(), propertyInfo.environment());
         default -> throw new MatchException(null, null);
      };
   }

   public PluginConfigurationDictionary connect(ConnectInfo c) {
      LOG.info(
         String.format(
            "Connecting plugin '%s' to property '%s' of plugin '%s' (context: %s, environment: %s). Config source: %s.",
            c.sourcePluginId(),
            c.destinationProperty(),
            c.destinationPluginId(),
            c.context(),
            c.environment(),
            this.configSource
         )
      );
      MutablePluginConfiguration destinationPlugin = this.getByIdentifierOrThrowSkippableException(
         this.configDictionary, c.destinationPluginId(), "Cannot connect plugins due to invalid destination."
      );
      MutablePluginConfiguration sourcePlugin = this.getByIdentifierOrThrowSkippableException(
         this.configDictionary, c.sourcePluginId(), "Cannot connect plugins due to invalid source."
      );
      PropertyConfiguration propertyConfiguration = getLocallyDefinedProperty(destinationPlugin, c.destinationProperty(), c.environment(), c.context());
      if (propertyConfiguration == null) {
         propertyConfiguration = new PluginPropertyConfiguration(c.destinationProperty(), sourcePlugin);
      }

      if (propertyConfiguration instanceof PluginPropertyConfiguration pluginPropertyConfiguration) {
         destinationPlugin.setProperty(
            new PluginPropertyConfiguration(c.destinationProperty(), sourcePlugin, pluginPropertyConfiguration.getVariable()), c.environment(), c.context()
         );
         return this.configDictionary;
      } else {
         throw new UnskippableConfigManipulationException(
            String.format(
               "Cannot connect to plugin property. Destination property '%s' is not of type plugin property in plugin '%s' (context: %s, environment: %s).",
               c.destinationProperty(),
               c.destinationPluginId(),
               c.context(),
               c.environment()
            )
         );
      }
   }

   public PluginConfigurationDictionary connectToMap(ConnectInfo c, String key) {
      LOG.info(
         String.format(
            "Connecting plugin '%s' to map property '%s' with key '%s' of plugin '%s' (context: %s, environment: %s). Config source: %s.",
            c.sourcePluginId(),
            c.destinationProperty(),
            key,
            c.destinationPluginId(),
            c.context(),
            c.environment(),
            this.configSource
         )
      );
      MutablePluginConfiguration destinationPlugin = this.getByIdentifierOrThrowSkippableException(
         this.configDictionary, c.destinationPluginId(), "Cannot connect to plugin map property due to invalid destination."
      );
      MutablePluginConfiguration sourcePlugin = this.getByIdentifierOrThrowSkippableException(
         this.configDictionary, c.sourcePluginId(), "Cannot connect to plugin map property due to invalid source."
      );
      PropertyConfiguration propertyConfiguration = getLocallyDefinedProperty(destinationPlugin, c.destinationProperty(), c.environment(), c.context());
      if (propertyConfiguration == null) {
         propertyConfiguration = new PluginMapPropertyConfiguration(c.destinationProperty());
      }

      if (propertyConfiguration instanceof PluginMapPropertyConfiguration pluginMapPropertyConfig) {
         Optional<Map<String, PluginConfiguration>> existingMap = Optional.ofNullable(pluginMapPropertyConfig.getValue());
         Map<String, PluginConfiguration> newMap = (Map<String, PluginConfiguration>)existingMap.map(LinkedHashMap::new).orElseGet(LinkedHashMap::new);
         newMap.put(key, sourcePlugin);
         destinationPlugin.setProperty(
            new PluginMapPropertyConfiguration(c.destinationProperty(), newMap, pluginMapPropertyConfig.getVariable()), c.environment(), c.context()
         );
         return this.configDictionary;
      } else {
         throw new UnskippableConfigManipulationException(
            String.format(
               "Cannot connect to plugin map property. Destination property '%s' is not a map in plugin '%s' (context: %s, environment: %s).",
               c.destinationProperty(),
               c.destinationPluginId(),
               c.context(),
               c.environment()
            )
         );
      }
   }

   private Runnable connectToPluginList(ConfigConnectToListModificationItem toListModification) {
      return switch (toListModification.position()) {
         case ConfigInsertionPosition.ConfigAtIndex index -> () -> this.insertAtIndex(ConnectInfo.toConnectInfo(toListModification), index.index());
         case ConfigInsertionPosition.ConfigAfter after -> () -> this.insertAfter(ConnectInfo.toConnectInfo(toListModification), after.plugin());
         case ConfigInsertionPosition.ConfigBefore before -> () -> this.insertBefore(ConnectInfo.toConnectInfo(toListModification), before.plugin());
         case ConfigInsertionPosition.ConfigAtEnd ignored -> () -> this.appendToList(ConnectInfo.toConnectInfo(toListModification));
         default -> throw new MatchException(null, null);
      };
   }

   public PluginConfigurationDictionary appendToList(ConnectInfo c) {
      LOG.info(
         String.format(
            "Connecting plugin '%s' to the end of list property '%s' of plugin '%s' (context: %s, environment: %s). Config source: %s.",
            c.sourcePluginId(),
            c.destinationProperty(),
            c.destinationPluginId(),
            c.context(),
            c.environment(),
            this.configSource
         )
      );
      ToIntFunction<List<PluginConfiguration>> indexFinder = List::size;
      return this.insertIntoPluginList(c, indexFinder);
   }

   public PluginConfigurationDictionary insertAtIndex(ConnectInfo c, int index) {
      LOG.info(
         String.format(
            "Connecting plugin '%s' to list property '%s' of plugin '%s' at index %d (context: %s, environment: %s). Config source: %s.",
            c.sourcePluginId(),
            c.destinationProperty(),
            c.destinationPluginId(),
            index,
            c.context(),
            c.environment(),
            this.configSource
         )
      );
      ToIntFunction<List<PluginConfiguration>> indexFinder = pluginList -> indexWithinBoundsForInsertion(c, pluginList, index);
      return this.insertIntoPluginList(c, indexFinder);
   }

   public PluginConfigurationDictionary insertBefore(ConnectInfo c, String otherPluginId) {
      LOG.info(
         String.format(
            "Connecting plugin '%s' to list property '%s' of plugin '%s' before plugin '%s' (context: %s, environment: %s). Config source: %s.",
            c.sourcePluginId(),
            c.destinationProperty(),
            c.destinationPluginId(),
            otherPluginId,
            c.context(),
            c.environment(),
            this.configSource
         )
      );
      ToIntFunction<List<PluginConfiguration>> indexFinder = pluginList -> indexOfOtherPlugin(c, pluginList, otherPluginId);
      return this.insertIntoPluginList(c, indexFinder);
   }

   public PluginConfigurationDictionary insertAfter(ConnectInfo c, String otherPluginId) {
      LOG.info(
         String.format(
            "Connecting plugin '%s' to list property '%s' of plugin '%s' after plugin '%s' (context: %s, environment: %s). Config source: %s.",
            c.sourcePluginId(),
            c.destinationProperty(),
            c.destinationPluginId(),
            otherPluginId,
            c.context(),
            c.environment(),
            this.configSource
         )
      );
      ToIntFunction<List<PluginConfiguration>> indexFinder = pluginList -> indexOfOtherPlugin(c, pluginList, otherPluginId) + 1;
      return this.insertIntoPluginList(c, indexFinder);
   }

   private static int indexWithinBoundsForInsertion(ConnectInfo c, List<PluginConfiguration> pluginList, int index) {
      if (index >= 0 && index <= pluginList.size()) {
         return index;
      } else {
         throw new UnskippableConfigManipulationException(
            String.format(
               "Cannot connect to plugin list property. Index %d is out of bounds for destination property '%s' in plugin '%s' (context: %s, environment: %s).",
               index,
               c.destinationProperty(),
               c.destinationPluginId(),
               c.context(),
               c.environment()
            )
         );
      }
   }

   private static int indexOfOtherPlugin(ConnectInfo c, List<PluginConfiguration> pluginList, String otherPluginId) {
      Optional<PluginConfiguration> otherPlugin = pluginList.stream().filter(p -> p.getUuid() != null && p.getUuid().equals(otherPluginId)).findFirst();
      if (otherPlugin.isEmpty()) {
         throw new SkippableConfigManipulationException(
            String.format(
               "Cannot connect to plugin list property. Referenced plugin '%s' not found in list property '%s' of plugin '%s' (context: %s, environment: %s).",
               otherPluginId,
               c.destinationProperty(),
               c.destinationPluginId(),
               c.context(),
               c.environment()
            )
         );
      } else {
         return pluginList.indexOf(otherPlugin.get());
      }
   }

   private PluginConfigurationDictionary insertIntoPluginList(ConnectInfo c, ToIntFunction<List<PluginConfiguration>> indexFinder) {
      MutablePluginConfiguration sourcePlugin = this.getByIdentifierOrThrowSkippableException(
         this.configDictionary, c.sourcePluginId(), "Cannot connect to plugin list property due to invalid source."
      );
      MutablePluginConfiguration destinationPlugin = this.getByIdentifierOrThrowSkippableException(
         this.configDictionary, c.destinationPluginId(), "Cannot connect to plugin list property due to invalid destination."
      );
      PropertyConfiguration propertyConfiguration = getLocallyDefinedProperty(destinationPlugin, c.destinationProperty(), c.environment(), c.context());
      if (propertyConfiguration == null) {
         propertyConfiguration = new PluginListPropertyConfiguration(c.destinationProperty());
      }

      if (propertyConfiguration instanceof PluginListPropertyConfiguration pluginListPropertyConfig) {
         Optional<List<PluginConfiguration>> existingList = Optional.ofNullable(pluginListPropertyConfig.getValue());
         boolean alreadyContainedInList = existingList.stream()
            .flatMap(Collection::stream)
            .<String>map(PluginConfiguration::getUuid)
            .anyMatch(uuid -> uuid.equals(sourcePlugin.getUuid()));
         if (alreadyContainedInList) {
            throw new SkippableConfigManipulationException(
               String.format(
                  "Cannot connect to plugin list property. Source plugin '%s' is already contained in destination list '%s' of plugin '%s' (context: %s, environment: %s).",
                  c.sourcePluginId(),
                  c.destinationProperty(),
                  c.destinationPluginId(),
                  c.context(),
                  c.environment()
               )
            );
         }

         ArrayList<PluginConfiguration> newList = (ArrayList<PluginConfiguration>)existingList.map(ArrayList::new).orElseGet(ArrayList::new);
         int index = indexFinder.applyAsInt(newList);
         newList.add(index, sourcePlugin);
         destinationPlugin.setProperty(
            new PluginListPropertyConfiguration(pluginListPropertyConfig.getName(), newList, pluginListPropertyConfig.getVariable()),
            c.environment(),
            c.context()
         );
         return this.configDictionary;
      } else {
         throw new UnskippableConfigManipulationException(
            String.format(
               "Cannot connect to plugin list property. Destination property '%s' is not a list in plugin '%s' (context: %s, environment: %s).",
               c.destinationProperty(),
               c.destinationPluginId(),
               c.context(),
               c.environment()
            )
         );
      }
   }

   public PluginConfigurationDictionary setSimpleProperty(PropertyInfo p, Optional<String> value) {
      LOG.info(
         String.format(
            "Setting simple property '%s' of plugin '%s' to '%s' (context: %s, environment: %s). Config source: %s.",
            p.property(),
            p.pluginId(),
            value.orElse(null),
            p.context(),
            p.environment(),
            this.configSource
         )
      );
      MutablePluginConfiguration destinationPlugin = this.getByIdentifierOrThrowSkippableException(
         this.configDictionary, p.pluginId(), "Cannot set simple property."
      );
      PropertyConfiguration propertyConfiguration = getLocallyDefinedProperty(destinationPlugin, p.property(), p.environment(), p.context());
      if (propertyConfiguration != null && !(propertyConfiguration instanceof SimplePropertyConfiguration)) {
         throw new UnskippableConfigManipulationException(
            String.format(
               "Cannot set simple property. Property '%s' is not of type simple property in plugin '%s' (context: %s, environment: %s).",
               p.property(),
               p.pluginId(),
               p.context(),
               p.environment()
            )
         );
      }

      SimplePropertyConfiguration newProperty = new SimplePropertyConfiguration(
         p.property(), value.orElse(null), Optional.ofNullable(propertyConfiguration).flatMap(PropertyConfiguration::getVariable)
      );
      destinationPlugin.setProperty(newProperty, p.environment(), p.context());
      return this.configDictionary;
   }

   public PluginConfigurationDictionary removeProperty(PropertyInfo p) {
      LOG.info(
         String.format(
            "Removing property '%s' of plugin '%s' (context: %s, environment: %s). Config source: %s.",
            p.property(),
            p.pluginId(),
            p.context(),
            p.environment(),
            this.configSource
         )
      );
      MutablePluginConfiguration destinationPlugin = this.getByIdentifierOrThrowSkippableException(
         this.configDictionary, p.pluginId(), "Cannot remove property."
      );
      PropertyConfiguration propertyConfiguration = getLocallyDefinedProperty(destinationPlugin, p.property(), p.environment(), p.context());
      if (propertyConfiguration == null) {
         throw new SkippableConfigManipulationException(
            String.format(
               "Cannot remove property. Property '%s' not found in plugin '%s' (context: %s, environment: %s).",
               p.property(),
               p.pluginId(),
               p.context(),
               p.environment()
            )
         );
      } else {
         boolean removed = destinationPlugin.removeProperty(p.property(), p.environment(), p.context());
         if (!removed) {
            throw new IllegalStateException("Unexpected failure to remove property.");
         } else {
            return this.configDictionary;
         }
      }
   }

   private Runnable insertIntoSimpleList(ConfigInsertSimpleListElementModificationItem modificationItem) {
      return switch (modificationItem.position()) {
         case ConfigInsertionPosition.ConfigAtIndex index -> () -> this.insertSimpleListElementAtIndex(
            PropertyInfo.toPropertyInfo(modificationItem), modificationItem.value(), index.index()
         );
         case ConfigInsertionPosition.ConfigAtEnd ignored -> () -> this.appendSimpleListElement(
            PropertyInfo.toPropertyInfo(modificationItem), modificationItem.value()
         );
         default -> throw new IllegalStateException("Unexpected insertion position: " + modificationItem.position().getClass().getSimpleName());
      };
   }

   public PluginConfigurationDictionary appendSimpleListElement(PropertyInfo p, String value) {
      LOG.info(
         String.format(
            "Inserting simple list element '%s' to the end of list property '%s' of plugin '%s' (context: %s, environment: %s). Config source: %s.",
            value,
            p.property(),
            p.pluginId(),
            p.context(),
            p.environment(),
            this.configSource
         )
      );
      ToIntFunction<List<String>> indexFinder = List::size;
      return this.insertIntoSimpleList(p, value, indexFinder);
   }

   public PluginConfigurationDictionary insertSimpleListElementAtIndex(PropertyInfo p, String value, int index) {
      LOG.info(
         String.format(
            "Inserting simple list element '%s' to list property '%s' of plugin '%s' at index %d (context: %s, environment: %s). Config source: %s.",
            value,
            p.property(),
            p.pluginId(),
            index,
            p.context(),
            p.environment(),
            this.configSource
         )
      );
      ToIntFunction<List<String>> indexFinder = simpleList -> indexWithinBoundsForInsertion(p, simpleList, index);
      return this.insertIntoSimpleList(p, value, indexFinder);
   }

   private static int indexWithinBoundsForInsertion(PropertyInfo p, List<String> simpleList, int index) {
      if (index >= 0 && index <= simpleList.size()) {
         return index;
      } else {
         throw new UnskippableConfigManipulationException(
            String.format(
               "Cannot insert simple list element. Index %d is out of bounds for property '%s' in plugin '%s' (context: %s, environment: %s).",
               index,
               p.property(),
               p.pluginId(),
               p.context(),
               p.environment()
            )
         );
      }
   }

   private PluginConfigurationDictionary insertIntoSimpleList(PropertyInfo p, String value, ToIntFunction<List<String>> indexFinder) {
      MutablePluginConfiguration destinationPlugin = this.getByIdentifierOrThrowSkippableException(
         this.configDictionary, p.pluginId(), "Cannot insert simple list element."
      );
      PropertyConfiguration propertyConfiguration = getLocallyDefinedProperty(destinationPlugin, p.property(), p.environment(), p.context());
      if (propertyConfiguration == null) {
         propertyConfiguration = new SimpleListPropertyConfiguration(p.property());
      }

      if (propertyConfiguration instanceof SimpleListPropertyConfiguration simpleListPropertyConfiguration) {
         Optional<List<String>> existingList = Optional.ofNullable(simpleListPropertyConfiguration.getValue());
         List newList = (List)existingList.map(ArrayList::new).orElseGet(ArrayList::new);
         int index = indexFinder.applyAsInt(newList);
         newList.add(index, value);
         destinationPlugin.setProperty(
            new SimpleListPropertyConfiguration(p.property(), newList, simpleListPropertyConfiguration.getVariable()), p.environment(), p.context()
         );
         return this.configDictionary;
      } else {
         throw new UnskippableConfigManipulationException(
            String.format(
               "Cannot insert simple list element. Property '%s' is not of type simple list property in plugin '%s' (context: %s, environment: %s).",
               p.property(),
               p.pluginId(),
               p.context(),
               p.environment()
            )
         );
      }
   }

   private Runnable removeListElement(ConfigRemoveListElementModificationItem modificationItem) {
      return switch (modificationItem.deletionTarget()) {
         case ConfigDeletionTarget.ConfigByIndex byIndex -> () -> this.removeListElementAtIndex(PropertyInfo.toPropertyInfo(modificationItem), byIndex.index());
         case ConfigDeletionTarget.ConfigByValue byValue -> () -> this.removeListElementByValueOrUuid(
            PropertyInfo.toPropertyInfo(modificationItem), byValue.value()
         );
         default -> throw new MatchException(null, null);
      };
   }

   public PluginConfigurationDictionary removeListElementAtIndex(PropertyInfo p, int index) {
      MutablePluginConfiguration destinationPlugin = this.getByIdentifierOrThrowSkippableException(
         this.configDictionary, p.pluginId(), "Cannot remove list element."
      );
      PropertyConfiguration propertyConfiguration = getLocallyDefinedProperty(destinationPlugin, p.property(), p.environment(), p.context());
      if (propertyConfiguration == null) {
         throw new SkippableConfigManipulationException(
            "Cannot remove list element. Property '%s' does not exist in plugin '%s' (context: %s, environment: %s)"
               .formatted(p.property(), p.pluginId(), p.context(), p.environment())
         );
      }

      return switch (propertyConfiguration) {
         case SimpleListPropertyConfiguration simpleList -> this.removeSimpleListElementAtIndex(p, destinationPlugin, simpleList, index);
         case PluginListPropertyConfiguration pluginList -> this.removePluginListElementAtIndex(p, destinationPlugin, pluginList, index);
         default -> throw new UnskippableConfigManipulationException(
            "Cannot remove list element. Property '%s' is not of type list in plugin '%s' (context: %s, environment: %s)"
               .formatted(p.property(), p.pluginId(), p.context(), p.environment())
         );
      };
   }

   public PluginConfigurationDictionary removeListElementByValueOrUuid(PropertyInfo p, String valueOrUuid) {
      MutablePluginConfiguration destinationPlugin = this.getByIdentifierOrThrowSkippableException(
         this.configDictionary, p.pluginId(), "Cannot remove list element."
      );
      PropertyConfiguration propertyConfiguration = getLocallyDefinedProperty(destinationPlugin, p.property(), p.environment(), p.context());
      if (propertyConfiguration == null) {
         throw new SkippableConfigManipulationException(
            "Cannot remove list element. Property '%s' does not exist in plugin '%s' (context: %s, environment: %s)"
               .formatted(p.property(), p.pluginId(), p.context(), p.environment())
         );
      }

      return switch (propertyConfiguration) {
         case SimpleListPropertyConfiguration simpleList -> this.removeSimpleListElementWithValue(p, destinationPlugin, simpleList, valueOrUuid);
         case PluginListPropertyConfiguration pluginList -> this.removePluginListElementWithUuid(p, destinationPlugin, pluginList, valueOrUuid);
         default -> throw new UnskippableConfigManipulationException(
            "Cannot remove list element. Property '%s' is not of type list in plugin '%s' (context: %s, environment: %s)"
               .formatted(p.property(), p.pluginId(), p.context(), p.environment())
         );
      };
   }

   private PluginConfigurationDictionary removeSimpleListElementAtIndex(
      PropertyInfo p, MutablePluginConfiguration destinationPlugin, SimpleListPropertyConfiguration simpleListProperty, int index
   ) {
      LOG.info(
         String.format(
            "Removing simple list element at index %s of list property '%s' of plugin '%s' at index %d (context: %s, environment: %s). Config source: %s.",
            index,
            p.property(),
            p.pluginId(),
            index,
            p.context(),
            p.environment(),
            this.configSource
         )
      );
      ToIntFunction<List<String>> indexFinder = simpleList -> this.indexWithinBoundsForDeletion(p, simpleList, index);
      return this.deleteFromSimpleList(simpleListProperty, destinationPlugin, indexFinder, p);
   }

   private PluginConfigurationDictionary removePluginListElementAtIndex(
      PropertyInfo p, MutablePluginConfiguration destinationPlugin, PluginListPropertyConfiguration pluginListProperty, int index
   ) {
      LOG.info(
         String.format(
            "Removing plugin list element at index %s of list property '%s' of plugin '%s' at index %d (context: %s, environment: %s). Config source: %s.",
            index,
            p.property(),
            p.pluginId(),
            index,
            p.context(),
            p.environment(),
            this.configSource
         )
      );
      ToIntFunction<List<PluginConfiguration>> indexFinder = pluginList -> this.indexWithinBoundsForDeletion(p, pluginList, index);
      return this.deleteFromPluginList(pluginListProperty, destinationPlugin, indexFinder, p);
   }

   private PluginConfigurationDictionary removeSimpleListElementWithValue(
      PropertyInfo p, MutablePluginConfiguration destinationPlugin, SimpleListPropertyConfiguration simpleListProperty, String value
   ) {
      LOG.info(
         String.format(
            "Removing simple list element with value '%s' of list property '%s' of plugin '%s' (context: %s, environment: %s). Config source: %s.",
            value,
            p.property(),
            p.pluginId(),
            p.context(),
            p.environment(),
            this.configSource
         )
      );
      ToIntFunction<List<String>> indexFinder = simpleList -> indexOfValue(p, simpleList, value);
      return this.deleteFromSimpleList(simpleListProperty, destinationPlugin, indexFinder, p);
   }

   private PluginConfigurationDictionary removePluginListElementWithUuid(
      PropertyInfo p, MutablePluginConfiguration destinationPlugin, PluginListPropertyConfiguration pluginListProperty, String uuid
   ) {
      LOG.info(
         String.format(
            "Removing plugin list element with ID '%s' of list property '%s' of plugin '%s' (context: %s, environment: %s). Config source: %s.",
            uuid,
            p.property(),
            p.pluginId(),
            p.context(),
            p.environment(),
            this.configSource
         )
      );
      ToIntFunction<List<PluginConfiguration>> indexFinder = pluginList -> indexOfPlugin(p, pluginList, uuid);
      return this.deleteFromPluginList(pluginListProperty, destinationPlugin, indexFinder, p);
   }

   private PluginConfigurationDictionary deleteFromSimpleList(
      SimpleListPropertyConfiguration simpleListProperty, MutablePluginConfiguration destinationPlugin, ToIntFunction<List<String>> indexFinder, PropertyInfo p
   ) {
      Optional<List<String>> existingList = Optional.ofNullable(simpleListProperty.getValue());
      List<String> newList = (List<String>)existingList.map(ArrayList::new).orElseGet(ArrayList::new);
      int index = indexFinder.applyAsInt(newList);
      newList.remove(index);
      destinationPlugin.setProperty(new SimpleListPropertyConfiguration(p.property(), newList, simpleListProperty.getVariable()), p.environment(), p.context());
      return this.configDictionary;
   }

   private PluginConfigurationDictionary deleteFromPluginList(
      PluginListPropertyConfiguration pluginListProperty,
      MutablePluginConfiguration destinationPlugin,
      ToIntFunction<List<PluginConfiguration>> indexFinder,
      PropertyInfo p
   ) {
      Optional<List<PluginConfiguration>> existingList = Optional.ofNullable(pluginListProperty.getValue());
      List<PluginConfiguration> newList = (List<PluginConfiguration>)existingList.map(ArrayList::new).orElseGet(ArrayList::new);
      int index = indexFinder.applyAsInt(newList);
      newList.remove(index);
      destinationPlugin.setProperty(
         new PluginListPropertyConfiguration(pluginListProperty.getName(), newList, pluginListProperty.getVariable()), p.environment(), p.context()
      );
      return this.configDictionary;
   }

   private int indexWithinBoundsForDeletion(PropertyInfo p, List<?> list, int index) {
      int listSize = list.size();
      if (listSize == 0) {
         throw new SkippableConfigManipulationException(
            String.format(
               "Cannot remove list element. List property '%s' in plugin '%s' is empty (context: %s, environment: %s).",
               p.property(),
               p.pluginId(),
               p.context(),
               p.environment()
            )
         );
      } else if (index >= 0 && index < listSize) {
         return index;
      } else {
         throw new UnskippableConfigManipulationException(
            String.format(
               "Cannot delete list element. Index %d is out of bounds for property '%s' in plugin '%s' (context: %s, environment: %s).",
               index,
               p.property(),
               p.pluginId(),
               p.context(),
               p.environment()
            )
         );
      }
   }

   private static int indexOfPlugin(PropertyInfo p, List<PluginConfiguration> pluginList, String otherPluginId) {
      Optional<PluginConfiguration> otherPlugin = pluginList.stream()
         .filter(plugin -> plugin.getUuid() != null && plugin.getUuid().equals(otherPluginId))
         .findFirst();
      if (otherPlugin.isEmpty()) {
         throw new SkippableConfigManipulationException(
            String.format(
               "Cannot delete plugin list element. Referenced plugin '%s' not found in list property '%s' of plugin '%s' (context: %s, environment: %s).",
               otherPluginId,
               p.property(),
               p.pluginId(),
               p.context(),
               p.environment()
            )
         );
      } else {
         return pluginList.indexOf(otherPlugin.get());
      }
   }

   private static int indexOfValue(PropertyInfo p, List<String> simpleList, String value) {
      int index = simpleList.indexOf(value);
      if (index == -1) {
         throw new SkippableConfigManipulationException(
            String.format(
               "Cannot delete simple list element. Value '%s' not found in list property '%s' of plugin '%s' (context: %s, environment: %s).",
               value,
               p.property(),
               p.pluginId(),
               p.context(),
               p.environment()
            )
         );
      } else {
         return index;
      }
   }

   private MutablePluginConfiguration getByIdentifierOrThrowSkippableException(
      PluginConfigurationDictionary configDictionary, String pluginId, String errorPrefix
   ) {
      MutablePluginConfiguration plugin = configDictionary.getByUuid(pluginId);
      if (plugin == null) {
         throw new SkippableConfigManipulationException("%s Plugin with id '%s' does not exist.".formatted(errorPrefix, pluginId));
      } else {
         return plugin;
      }
   }

   private static PropertyConfiguration getLocallyDefinedProperty(
      PluginConfiguration plugin, String property, ConfigurationEnvironment environment, ConfigurationContext context
   ) {
      return plugin.isPropertyDefinedLocallyInEnvironmentAndContext(property, environment, context)
         ? plugin.getPropertyConfiguration(property, environment, context)
         : null;
   }
}
