package com.airlock.iam.plugin.framework.application.service;

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.ConfigurationVariable;
import com.airlock.iam.plugin.api.config.LogTypes;
import com.airlock.iam.plugin.api.config.PluginConfiguration;
import com.airlock.iam.plugin.api.exception.InitializeException;
import com.airlock.iam.plugin.api.property.PropertyConfiguration;
import com.airlock.iam.plugin.framework.application.configuration.MutablePluginConfiguration;
import com.airlock.iam.plugin.framework.domain.model.ConfigHistoryMetaDataItem;
import com.airlock.iam.plugin.framework.domain.model.MedusaConfigurationElement;
import com.airlock.iam.plugin.framework.domain.model.Plugin;
import com.airlock.iam.plugin.framework.domain.model.PropertyIdentifier;
import com.airlock.iam.plugin.framework.domain.model.property.PluginListProperty;
import com.airlock.iam.plugin.framework.domain.model.property.PluginMapProperty;
import com.airlock.iam.plugin.framework.domain.model.property.PluginProperty;
import com.airlock.iam.plugin.framework.domain.model.property.Property;
import com.airlock.iam.plugin.framework.domain.model.property.SimpleListProperty;
import com.airlock.iam.plugin.framework.domain.model.property.SimpleProperty;
import com.airlock.iam.plugin.framework.misc.config.ConfigHistoryItem;
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.PropertyConfigurationVisitor;
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.frompersistence.ClassNameNormalizer;
import com.airlock.iam.plugin.framework.misc.frompersistence.TranslatedMedusaConfiguration;
import com.airlock.iam.plugin.impl.PropertyExternalFileUtils;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.TreeSet;
import org.apache.commons.lang3.builder.CompareToBuilder;

public class PCModelToPersistenceTranslation {
   private static final ELogger LOG = ELoggerFactory.getLogger(PCModelToPersistenceTranslation.class);
   public static final Comparator<Plugin> PLUGIN_SORTER = (p1, p2) -> new CompareToBuilder()
      .append(p1.getType().orElse(""), p2.getType().orElse(""))
      .append(p1.getUuid().orElse(""), p2.getUuid().orElse(""))
      .toComparison();

   public MedusaConfigurationElement translate(TranslatedMedusaConfiguration config) {
      return this.translate(config, new IncludeAllFilter());
   }

   public MedusaConfigurationElement translate(TranslatedMedusaConfiguration config, TranslationFilter filter) {
      PCModelToPersistenceTranslation.PCModelConversionResult results = this.convertPluginConfigurationToPluginsOfAllPlugins(config, filter);
      this.convertPropertiesForLocation(results, filter);
      return this.createMedusaConfigurationForLocation(config, results, filter);
   }

   private PCModelToPersistenceTranslation.PCModelConversionResult convertPluginConfigurationToPluginsOfAllPlugins(
      TranslatedMedusaConfiguration config, TranslationFilter filter
   ) {
      PCModelToPersistenceTranslation.PCModelConversionResult results = new PCModelToPersistenceTranslation.PCModelConversionResult();

      for (PluginConfiguration pc : config.getAllPluginConfigurations()) {
         if (this.shallBeTranslated(pc, filter)) {
            results.addResult(pc, this.convert(pc));
         }
      }

      return results;
   }

   private boolean shallBeTranslated(PluginConfiguration config, TranslationFilter filter) {
      return !config.isSynthetic() && filter.includePluginConfiguration(config);
   }

   private Plugin convert(PluginConfiguration config) {
      Plugin plugin = new Plugin();
      this.convertAttributes(config, plugin);
      return plugin;
   }

   private void convertAttributes(PluginConfiguration config, Plugin plugin) {
      plugin.setType(ClassNameNormalizer.normalizeClassName(config.getPluginClassName()));
      plugin.setName(config.getPluginDisplayName());
      plugin.setComment(config.getComment());
      plugin.setUuid(config.getUuid());
      if (config.hasParentConfiguration()) {
         plugin.setTemplate(Objects.requireNonNull(config.getParentConfiguration()).getPluginDisplayName());
      }
   }

   private void convertPropertiesForLocation(PCModelToPersistenceTranslation.PCModelConversionResult results, TranslationFilter filter) {
      for (PCModelToPersistenceTranslation.OnePCConversionResult result : results.getAllResults()) {
         if (this.shallBeIncludedIntoConfig(result.pluginConfig, filter)) {
            this.convertProperties(result, results, filter);
         }
      }
   }

   private void convertProperties(
      PCModelToPersistenceTranslation.OnePCConversionResult result, PCModelToPersistenceTranslation.PCModelConversionResult results, TranslationFilter filter
   ) {
      MutablePluginConfiguration config = result.getPluginConfig();
      List<Property> allProperties = this.convertAllProperties(results, config, filter);
      this.sortProperties(allProperties);

      for (Property property : allProperties) {
         result.getPlugin().addProperty(property);
      }
   }

   private List<Property> convertAllProperties(
      PCModelToPersistenceTranslation.PCModelConversionResult results, MutablePluginConfiguration config, TranslationFilter filter
   ) {
      List<Property> allProperties = Lists.newArrayList();

      for (ConfigurationEnvironment env : config.getLocallyUsedConfigurationEnvironments()) {
         for (ConfigurationContext context : config.getLocallyUsedConfigurationContexts(env)) {
            PCModelToPersistenceTranslation.PropertyConfigurationToPropertyConverter visitor = new PCModelToPersistenceTranslation.PropertyConfigurationToPropertyConverter(
               results, config, filter, env, context
            );
            config.visitLocallyInContext(visitor, env, context);
            allProperties.addAll(visitor.properties);
         }
      }

      return allProperties;
   }

   private void sortProperties(List<Property> allProperties) {
      allProperties.sort(new PCModelToPersistenceTranslation.PCModelPropertySorter());
   }

   private MedusaConfigurationElement createMedusaConfigurationForLocation(
      TranslatedMedusaConfiguration config, PCModelToPersistenceTranslation.PCModelConversionResult results, TranslationFilter filter
   ) {
      MedusaConfigurationElement element = new MedusaConfigurationElement();
      this.addAllPluginsToTopLevel(config, results, element, filter);
      element.sortToplevelPlugins(PLUGIN_SORTER);
      this.addConvertedHistoryToMedusaConfigurationElement(config, element);
      this.convertVersionAndType(config, element);
      this.addModificationsToMedusaConfigurationElement(config, element);
      return element;
   }

   private void addAllPluginsToTopLevel(
      TranslatedMedusaConfiguration config,
      PCModelToPersistenceTranslation.PCModelConversionResult results,
      MedusaConfigurationElement element,
      TranslationFilter filter
   ) {
      for (PluginConfiguration pc : config.getAllPluginConfigurations()) {
         if (this.shallBeIncludedIntoConfig(pc, filter)) {
            element.addToplevelPlugin(results.getBy(pc).getPlugin());
         }
      }
   }

   private boolean shallBeIncludedIntoConfig(PluginConfiguration config, TranslationFilter filter) {
      return this.shallBeTranslated(config, filter);
   }

   private void addConvertedHistoryToMedusaConfigurationElement(TranslatedMedusaConfiguration config, MedusaConfigurationElement element) {
      for (ConfigHistoryMetaDataItem historyItem : this.convertConfigurationHistoryToConfigurationHistoryMetaDataItems(config.getHistory())) {
         element.addHistoryItem(historyItem);
      }
   }

   private List<ConfigHistoryMetaDataItem> convertConfigurationHistoryToConfigurationHistoryMetaDataItems(List<ConfigHistoryItem> configHistory) {
      List<ConfigHistoryMetaDataItem> convertedHistory = Lists.newArrayList();

      for (ConfigHistoryItem historyItem : configHistory) {
         convertedHistory.add(this.convert(historyItem));
      }

      return convertedHistory;
   }

   private ConfigHistoryMetaDataItem convert(ConfigHistoryItem historyItem) {
      ConfigHistoryMetaDataItem convertedHistoryItem = new ConfigHistoryMetaDataItem();
      convertedHistoryItem.setAuthor(historyItem.getAuthor());
      convertedHistoryItem.setDate(historyItem.getDate());
      convertedHistoryItem.setDescription(historyItem.getComment());
      return convertedHistoryItem;
   }

   private void convertVersionAndType(TranslatedMedusaConfiguration config, MedusaConfigurationElement element) {
      if (config.getIamVersion() != null) {
         element.setIamVersion(config.getIamVersion().toString());
      }

      element.setSchemaVersion(config.getSchemaVersion());
      element.setDocumentTypeSnippet(config.isDocumentTypeSnippet());
   }

   private void addModificationsToMedusaConfigurationElement(TranslatedMedusaConfiguration config, MedusaConfigurationElement element) {
      config.getModifications().forEach(modification -> element.addModification(ConfigModificationItemToConfigModificationTransformer.transform(modification)));
   }

   public static final class OnePCConversionResult {
      private final MutablePluginConfiguration pluginConfig;
      private final Plugin converted;

      public OnePCConversionResult(PluginConfiguration pluginConfig, Plugin converted) {
         this.pluginConfig = (MutablePluginConfiguration)Preconditions.checkNotNull((MutablePluginConfiguration)pluginConfig);
         this.converted = (Plugin)Preconditions.checkNotNull(converted);
      }

      public MutablePluginConfiguration getPluginConfig() {
         return this.pluginConfig;
      }

      public Plugin getPlugin() {
         return this.converted;
      }

      private Plugin createDirectReference() {
         Plugin directReference = new Plugin();
         directReference.setUuidRef(this.converted.getUuid().orElseThrow());
         return directReference;
      }
   }

   public static final class PCModelConversionResult {
      private final Map<PluginConfiguration, PCModelToPersistenceTranslation.OnePCConversionResult> results = Maps.newIdentityHashMap();

      public void addResult(PluginConfiguration config, Plugin plugin) {
         this.results.put(config, new PCModelToPersistenceTranslation.OnePCConversionResult(config, plugin));
      }

      public PCModelToPersistenceTranslation.OnePCConversionResult getBy(PluginConfiguration pluginConfig) {
         PCModelToPersistenceTranslation.OnePCConversionResult result = this.results.get(pluginConfig);
         Preconditions.checkState(result != null, "Given PluginConfiguration was not converted");
         return result;
      }

      public boolean contains(PluginConfiguration pluginConfig) {
         return this.results.containsKey(pluginConfig);
      }

      public Collection<PCModelToPersistenceTranslation.OnePCConversionResult> getAllResults() {
         return this.results.values();
      }
   }

   public static final class PCModelPropertySorter implements Comparator<Property> {
      public int compare(Property o1, Property o2) {
         return new CompareToBuilder()
            .append(o1.getName(), o2.getName())
            .append(o1.getEnvId().orElse(""), o2.getEnvId().orElse(""))
            .append(o1.getContext().orElse(""), o2.getContext().orElse(""))
            .toComparison();
      }
   }

   private static final class PropertyConfigurationToPropertyConverter implements PropertyConfigurationVisitor {
      private final List<Property> properties = Lists.newArrayList();
      private final PCModelToPersistenceTranslation.PCModelConversionResult results;
      private final PluginConfiguration pluginConfiguration;
      private final TranslationFilter translationFilter;
      private final ConfigurationEnvironment env;
      private final ConfigurationContext context;

      @Override
      public void visit(PluginMapPropertyConfiguration prop) {
         if (this.isIncludedByFilter(prop)) {
            PluginMapProperty map = new PluginMapProperty(
               prop.getName(), this.getEnvironmentIdentifier(), this.getContextIdentifier(), prop.getVariable().map(ConfigurationVariable::format)
            );
            Map<String, PluginConfiguration> mapConfig = prop.getValue();
            if (mapConfig != null) {
               for (String key : new TreeSet<>(mapConfig.keySet())) {
                  map.putPlugin(key, this.referencePluginForPC(mapConfig.get(key)));
               }
            }

            this.properties.add(map);
         }
      }

      @Override
      public void visit(PluginListPropertyConfiguration prop) {
         if (this.isIncludedByFilter(prop)) {
            PluginListProperty list = new PluginListProperty(
               prop.getName(), this.getEnvironmentIdentifier(), this.getContextIdentifier(), prop.getVariable().map(ConfigurationVariable::format)
            );
            if (prop.getValue() != null) {
               for (PluginConfiguration config : prop.getValue()) {
                  list.addPlugin(this.referencePluginForPC(config));
               }
            }

            this.properties.add(list);
         }
      }

      @Override
      public void visit(SimpleListPropertyConfiguration prop) {
         if (this.isIncludedByFilter(prop)) {
            SimpleListProperty list = new SimpleListProperty(
               prop.getName(), this.getEnvironmentIdentifier(), this.getContextIdentifier(), prop.getVariable().map(ConfigurationVariable::format)
            );
            if (prop.getValue() != null) {
               for (String value : prop.getValue()) {
                  list.addValue(value);
               }
            }

            this.properties.add(list);
         }
      }

      @Override
      public void visit(SimplePropertyConfiguration prop) {
         if (this.isIncludedByFilter(prop)) {
            try {
               SimpleProperty sp = new SimpleProperty(
                  prop.getName(), this.getEnvironmentIdentifier(), this.getContextIdentifier(), prop.getVariable().map(ConfigurationVariable::format)
               );
               sp.setValue(this.determineValue(prop));
               if (prop.getExternalStorageId().isPresent()) {
                  sp.setSecureExternalStorageId(prop.getExternalStorageId().get());
               }

               this.properties.add(sp);
            } catch (InitializeException e) {
               PCModelToPersistenceTranslation.LOG.error(LogTypes.CONFIGURATION_ERROR, "Error scrambling value of field {1}.", new Object[]{prop.getName()}, e);
            }
         }
      }

      @Override
      public void visit(PluginPropertyConfiguration pluginPropertyConfig) {
         if (this.isIncludedByFilter(pluginPropertyConfig)) {
            if (this.hasSyntheticValue(pluginPropertyConfig) && this.hasParent(pluginPropertyConfig)) {
               PluginProperty pluginProperty = this.convert(new PluginPropertyConfiguration(pluginPropertyConfig.getName(), null, Optional.empty()));
               this.properties.add(pluginProperty);
            } else if (this.hasNonSyntheticValue(pluginPropertyConfig) || this.hasNullValue(pluginPropertyConfig)) {
               PluginProperty pluginProperty = this.convert(pluginPropertyConfig);
               this.properties.add(pluginProperty);
            }
         }
      }

      private String determineValue(SimplePropertyConfiguration prop) throws InitializeException {
         if (!prop.isScrambled()) {
            return prop.getValue();
         } else {
            return prop.getOriginalScrambledValue().isPresent()
               ? prop.getOriginalScrambledValue().get()
               : PropertyExternalFileUtils.scrambleString(prop.getValue());
         }
      }

      private boolean hasSyntheticValue(PluginPropertyConfiguration pluginPropertyConfig) {
         return pluginPropertyConfig.getValue() != null && pluginPropertyConfig.getValue().isSynthetic();
      }

      private boolean hasParent(PluginPropertyConfiguration pluginPropertyConfig) {
         return this.pluginConfiguration.hasParentConfiguration()
            || this.isPropertyDefinedForSpecificEnvironmentAndContext(pluginPropertyConfig)
               && (!this.context.isDefaultContext() || !this.env.isCommonEnvironment());
      }

      private boolean isPropertyDefinedForSpecificEnvironmentAndContext(PluginPropertyConfiguration pluginPropertyConfig) {
         return this.pluginConfiguration.isPropertyDefinedForEnvironmentAndContext(pluginPropertyConfig.getName(), this.env, this.context);
      }

      private boolean hasNullValue(PluginPropertyConfiguration pluginPropertyConfiguration) {
         return pluginPropertyConfiguration.getValue() == null;
      }

      private boolean hasNonSyntheticValue(PluginPropertyConfiguration pluginPropertyConfiguration) {
         return pluginPropertyConfiguration.getValue() != null && !pluginPropertyConfiguration.getValue().isSynthetic();
      }

      private PluginProperty convert(PluginPropertyConfiguration pluginPropertyConfig) {
         PluginProperty pluginProperty = new PluginProperty(
            pluginPropertyConfig.getName(),
            this.getEnvironmentIdentifier(),
            this.getContextIdentifier(),
            pluginPropertyConfig.getVariable().map(ConfigurationVariable::format)
         );
         this.setConfiguredPluginOnPluginPropertyIfItIsNotSynthetic(pluginPropertyConfig, pluginProperty);
         return pluginProperty;
      }

      private void setConfiguredPluginOnPluginPropertyIfItIsNotSynthetic(PluginPropertyConfiguration prop, PluginProperty pluginProp) {
         if (this.hasNonSyntheticValue(prop)) {
            pluginProp.setPlugin(this.referencePluginForPC(prop.getValue()));
         }
      }

      private Plugin referencePluginForPC(PluginConfiguration config) {
         return !this.results.contains(config) ? new Plugin() : this.results.getBy(config).createDirectReference();
      }

      private Optional<String> getContextIdentifier() {
         return Optional.ofNullable(this.context.getIdentifier());
      }

      private Optional<String> getEnvironmentIdentifier() {
         return this.env.getIdentifier();
      }

      private boolean isIncludedByFilter(PropertyConfiguration prop) {
         return this.translationFilter.includePropertyFilter(this.pluginConfiguration, PropertyIdentifier.by(prop.getName(), this.env, this.context));
      }

      @SuppressFBWarnings(justification = "generated code")
      public PropertyConfigurationToPropertyConverter(
         PCModelToPersistenceTranslation.PCModelConversionResult results,
         PluginConfiguration pluginConfiguration,
         TranslationFilter translationFilter,
         ConfigurationEnvironment env,
         ConfigurationContext context
      ) {
         this.results = results;
         this.pluginConfiguration = pluginConfiguration;
         this.translationFilter = translationFilter;
         this.env = env;
         this.context = context;
      }
   }
}
