package com.airlock.iam.admin.infrastructure.service.analytics;

import com.airlock.iam.admin.application.service.license.ConfigAnalyticsService;
import com.airlock.iam.admin.domain.model.analytics.ConfigAnalytics;
import com.airlock.iam.base.api.domain.model.exception.FailureException;
import com.airlock.iam.core.domain.model.http.digest.SHA256MessageDigestFactory;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.plugin.api.annotation.property.BooleanProperty;
import com.airlock.iam.plugin.api.annotation.property.EnumProperty;
import com.airlock.iam.plugin.api.annotation.property.StringListProperty;
import com.airlock.iam.plugin.api.annotation.property.StringProperty;
import com.airlock.iam.plugin.api.exception.InitializeException;
import com.airlock.iam.plugin.api.exception.PluginClassLoadingException;
import com.airlock.iam.plugin.framework.application.service.ConfigReader;
import com.airlock.iam.plugin.framework.application.service.ConfigWriter;
import com.airlock.iam.plugin.framework.domain.model.ConfigRoot;
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.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.infrastructure.io.YamlConfigReader;
import com.airlock.iam.plugin.framework.infrastructure.io.YamlConfigWriter;
import com.airlock.iam.plugin.framework.misc.ioc.PluginLoader;
import com.airlock.iam.plugin.impl.cache.PluginClassCache;
import com.google.common.base.Strings;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.SequencedMap;
import java.util.Map.Entry;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import org.apache.commons.codec.binary.Base64;

public class ConfigAnalyticsServiceImpl implements ConfigAnalyticsService {
   private static final ELogger LOG = ELoggerFactory.getLogger(ConfigAnalyticsServiceImpl.class);
   private static final List<String> ROOT_MODULES = Stream.of(ConfigRoot.values()).map(ConfigRoot::getIdentifier).toList();
   private static final String REDACTED_PLACEHOLDER = "[redacted]";
   private final Provider<ConfigProvider> configLoader;

   @Inject
   public ConfigAnalyticsServiceImpl(Provider<ConfigProvider> configLoader) {
      this.configLoader = configLoader;
   }

   @Override
   public ConfigAnalytics retrieveConfig() {
      MedusaConfigurationElement originalConfig = ((ConfigProvider)this.configLoader.get()).retrieveConfig();
      MedusaConfigurationElement config = this.configWithoutSensitiveData(originalConfig);
      Map<?, ?> configAsMap = asMap(config);
      return new ConfigAnalytics(configAsMap);
   }

   private MedusaConfigurationElement configWithoutSensitiveData(MedusaConfigurationElement originalConfig) {
      MedusaConfigurationElement processedConfig = new MedusaConfigurationElement();
      processedConfig.setIamVersion(originalConfig.getIamVersion().orElseThrow(() -> new FailureException("missing version in config")));
      processedConfig.setSchemaVersion(originalConfig.getSchemaVersion());
      StreamSupport.stream(originalConfig.allPluginsInArbitraryOrder().spliterator(), false)
         .map(this::pluginWithoutSensitiveData)
         .forEach(processedConfig::addToplevelPlugin);
      return processedConfig;
   }

   private Plugin pluginWithoutSensitiveData(Plugin plugin) {
      redactTopLevelPluginFields(plugin);
      redactProperties(plugin);
      return plugin;
   }

   private static void redactTopLevelPluginFields(Plugin plugin) {
      plugin.setComment(null);
      if (plugin.getUuid().isPresent()) {
         plugin.setUuid(hash(plugin.getUuid().get()));
      }

      if (plugin.getUuidRef().isPresent()) {
         plugin.setUuidRef(hash(plugin.getUuidRef().get()));
      }

      if (plugin.getName().isPresent()) {
         plugin.setName(hashDisplayName(plugin.getName().get()));
      }

      if (plugin.getTemplate().isPresent()) {
         plugin.setTemplate(hashDisplayName(plugin.getTemplate().get()));
      }
   }

   private static String hashDisplayName(String displayName) {
      return ROOT_MODULES.contains(displayName) ? displayName : hash(displayName);
   }

   private static String hash(String text) {
      MessageDigest messageDigest = new SHA256MessageDigestFactory().create();
      messageDigest.update(text.getBytes(StandardCharsets.UTF_8));
      return Base64.encodeBase64URLSafeString(messageDigest.digest()).substring(0, 16);
   }

   private static void redactProperties(Plugin plugin) {
      List<Property> redactedProperties = getRedactedProperties(plugin);
      plugin.getProperties().forEach(plugin::removeProperty);
      redactedProperties.forEach(plugin::addProperty);
   }

   private static List<Property> getRedactedProperties(Plugin plugin) {
      Optional<Class<?>> pluginClass = plugin.getType().flatMap(ConfigAnalyticsServiceImpl::getPluginClass);
      if (pluginClass.isEmpty()) {
         return Collections.emptyList();
      }

      Collection<Field> fields = collectAllFields(pluginClass.get());
      List<String> sensitiveFieldnames = fields.stream().filter(ConfigAnalyticsServiceImpl::isSensitive).map(Field::getName).toList();
      return plugin.getProperties().stream().map(property -> propertyWithoutSensitiveData(property, sensitiveFieldnames)).toList();
   }

   private static boolean isSensitive(Field field) {
      if (field.isAnnotationPresent(StringProperty.class)) {
         StringProperty property = Objects.requireNonNull(field.getAnnotation(StringProperty.class));
         return property.allowedValues().length == 0;
      } else if (field.isAnnotationPresent(StringListProperty.class)) {
         StringListProperty property = Objects.requireNonNull(field.getAnnotation(StringListProperty.class));
         return property.allowedValues().length == 0;
      } else {
         return !isSafeSimpleProperty(field);
      }
   }

   private static boolean isSafeSimpleProperty(Field field) {
      return field.isAnnotationPresent(BooleanProperty.class) || field.isAnnotationPresent(EnumProperty.class);
   }

   private static Property propertyWithoutSensitiveData(Property property, List<String> sensitiveSimpleProperties) {
      if (property instanceof SimpleProperty simpleProperty) {
         String value = simpleProperty.getValue().orElse(null);
         if (sensitiveSimpleProperties.contains(property.getName())) {
            value = replaceSensitive(value);
         }

         return new SimpleProperty(property.getName(), value, mapEnv(property), mapContext(property), mapVariable(property), null);
      } else if (property instanceof SimpleListProperty simpleListProperty) {
         List<String> values = simpleListProperty.getValues();
         if (sensitiveSimpleProperties.contains(property.getName())) {
            values = simpleListProperty.getValues().stream().map(ConfigAnalyticsServiceImpl::replaceSensitive).toList();
         }

         return new SimpleListProperty(property.getName(), values, mapEnv(property), mapContext(property), mapVariable(property));
      } else if (property instanceof PluginListProperty pluginListProperty) {
         return new PluginListProperty(property.getName(), pluginListProperty.getPlugins(), mapEnv(property), mapContext(property), mapVariable(property));
      } else if (property instanceof PluginMapProperty pluginMapProperty) {
         return new PluginMapProperty(property.getName(), redactKeys(pluginMapProperty), mapEnv(property), mapContext(property), mapVariable(property));
      } else if (property instanceof PluginProperty pluginProperty) {
         return new PluginProperty(property.getName(), pluginProperty.getValue().orElse(null), mapEnv(property), mapContext(property), mapVariable(property));
      } else {
         throw new FailureException("unknown property type: " + property.getClass().getName());
      }
   }

   private static SequencedMap<String, Plugin> redactKeys(PluginMapProperty pluginMapProperty) {
      LinkedHashMap<String, Plugin> result = new LinkedHashMap<>();
      List<Entry<String, Plugin>> entries = pluginMapProperty.getPluginMap().sequencedEntrySet().stream().toList();

      for (int i = 0; i < entries.size(); i++) {
         result.put("key" + i, entries.get(i).getValue());
      }

      return result;
   }

   private static String replaceSensitive(String value) {
      return Strings.isNullOrEmpty(value) ? value : "[redacted]";
   }

   private static String mapVariable(Property property) {
      return replaceSensitive(property.getVariable().orElse(null));
   }

   private static String mapContext(Property property) {
      return property.getContext().map(ConfigAnalyticsServiceImpl::hash).orElse(null);
   }

   private static String mapEnv(Property property) {
      return property.getEnvId().map(ConfigAnalyticsServiceImpl::hash).orElse(null);
   }

   private static Optional<Class<?>> getPluginClass(String className) {
      try {
         return Optional.of(PluginLoader.loadClass(className));
      } catch (InitializeException e) {
         LOG.info(e.getMessage());
         return Optional.empty();
      }
   }

   private static Collection<Field> collectAllFields(Class<?> pluginClass) {
      try {
         return PluginClassCache.collectAllFields(pluginClass, true);
      } catch (PluginClassLoadingException e) {
         LOG.info(e.getMessage());
         return Collections.emptyList();
      }
   }

   private static Map<?, ?> asMap(MedusaConfigurationElement config) {
      ByteArrayOutputStream yamlStream = new ByteArrayOutputStream();

      try {
         new YamlConfigWriter().write(config, yamlStream);
      } catch (ConfigWriter.ConfigWriterException e) {
         throw new FailureException(e);
      }

      return yamlToMap(yamlStream);
   }

   private static Map<?, ?> yamlToMap(ByteArrayOutputStream yaml) {
      try {
         return YamlConfigReader.readToMap(new ByteArrayInputStream(yaml.toByteArray()));
      } catch (ConfigReader.ConfigLoaderException e) {
         throw new FailureException(e);
      }
   }
}
