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

import com.airlock.iam.plugin.api.annotation.property.PluginProperty;
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.exception.ConfigurationException;
import com.airlock.iam.plugin.api.exception.InitializeException;
import com.airlock.iam.plugin.api.exception.PropertyTypeMismatchException;
import com.airlock.iam.plugin.api.property.PluginConfigurationProperties;
import com.airlock.iam.plugin.api.property.PropertyConfiguration;
import com.airlock.iam.plugin.api.valuereader.PluginPropertyValueReader;
import com.airlock.iam.plugin.framework.misc.config.GlobalConfigurationAccessor;
import com.airlock.iam.plugin.framework.misc.config.PluginConfigurationDictionary;
import com.airlock.iam.plugin.framework.misc.config.PluginConfigurationOwner;
import com.airlock.iam.plugin.framework.misc.config.PluginConfigurationParentChangedEvent;
import com.airlock.iam.plugin.framework.misc.config.PluginConfigurationPropertiesView;
import com.airlock.iam.plugin.framework.misc.config.PluginConfigurationPropertyChangedEvent;
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.config.properties.VisitableProperty;
import com.airlock.iam.plugin.framework.misc.ioc.PluginLoader;
import com.airlock.iam.plugin.impl.PluginDisplayNameReader;
import com.airlock.iam.plugin.impl.cache.PluginClassCache;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import jakarta.annotation.Nullable;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.UUID;
import java.util.regex.Pattern;
import lombok.NonNull;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;

public final class DefaultPluginConfiguration implements MutablePluginConfiguration {
   private static final Pattern CLASS_SEPARATOR = Pattern.compile("[$.]");
   private DefaultPluginConfiguration parentConfiguration;
   private String pluginClassName;
   private final Map<ConfigurationEnvironment, PluginConfigurationProperties> propertiesPerEnv = new HashMap<>();
   private Class<?> pluginClass;
   private String internalDisplayName;
   private PluginConfigurationOwner owner;
   private boolean synthetic;
   private String comment;
   private GlobalConfigurationAccessor globalConfigurationAccessor;
   private String uuid;

   public PluginConfigurationProperties getEnvironmentSpecificProperties(ConfigurationEnvironment env) {
      PluginConfigurationProperties properties = this.propertiesPerEnv.get(env);
      return properties == null ? new PluginConfigurationProperties() : properties;
   }

   public DefaultPluginConfiguration(@Nullable String displayName) {
      this.internalDisplayName = displayName;
      this.propertiesPerEnv.put(ConfigurationEnvironment.common(), new PluginConfigurationProperties());
   }

   public DefaultPluginConfiguration(@Nullable String displayName, String className) {
      this(displayName);
      Objects.requireNonNull(className, "class name is null");
      this.pluginClassName = className;
   }

   public DefaultPluginConfiguration(String displayName, PluginConfiguration parent) throws ConfigurationException {
      this(displayName, getClassName(parent));
      this.setParentConfiguration((MutablePluginConfiguration)parent);
   }

   private static String getClassName(PluginConfiguration parent) {
      Objects.requireNonNull(parent);
      return parent.getPluginClassName();
   }

   @Override
   public void visitInAllEnvironmentsAndContexts(PropertyConfigurationVisitor visitor) {
      for (ConfigurationEnvironment env : this.propertiesPerEnv.keySet()) {
         new PluginConfigurationPropertiesView(this, env)
            .inheritFromParent()
            .getProperties()
            .allProperties()
            .map(VisitableProperty.class::cast)
            .forEach(property -> property.accept(visitor));
      }
   }

   @Override
   public void visitLocallyInContext(PropertyConfigurationVisitor visitor, ConfigurationEnvironment env, ConfigurationContext context) {
      Preconditions.checkNotNull(visitor);
      Collection<PropertyConfiguration> localProperties = this.getDirectlyConfiguredProperties(env, context);
      localProperties.stream().map(VisitableProperty.class::cast).forEach(property -> property.accept(visitor));
   }

   @Override
   public void setPluginClassName(String pluginClassName) {
      this.pluginClassName = pluginClassName;
      this.pluginClass = null;
   }

   @Override
   public void setParentConfiguration(MutablePluginConfiguration parent) throws ConfigurationException {
      MutablePluginConfiguration oldParent = this.parentConfiguration;
      this.parentConfiguration = (DefaultPluginConfiguration)parent;
      if (this.parentConfiguration != null) {
         List<PluginConfiguration> visitedPluginConfigurations = new ArrayList<>();
         detectLoopInHierarchy(this, visitedPluginConfigurations);
         this.synthetic = false;
      }

      if (oldParent != this.parentConfiguration) {
         this.propagatePluginConfigurationParentChanged(new PluginConfigurationParentChangedEvent(this, oldParent, this.parentConfiguration));
      }
   }

   @Override
   public void removeParent() {
      this.parentConfiguration = null;
   }

   private static void detectLoopInHierarchy(PluginConfiguration configToCheck, List<PluginConfiguration> visitedPluginConfigurations) throws ConfigurationException {
      visitedPluginConfigurations.add(configToCheck);
      if (configToCheck.hasParentConfiguration()) {
         for (PluginConfiguration pc : visitedPluginConfigurations) {
            if (pc == configToCheck.getParentConfiguration()) {
               throw new ConfigurationException(
                  configToCheck,
                  ConfigurationEnvironment.common(),
                  ConfigurationContext.defaultContext(),
                  "Plugin configuration causes a a loop in the hierarchy."
               );
            }
         }

         detectLoopInHierarchy(configToCheck.getParentConfiguration(), visitedPluginConfigurations);
      }
   }

   public String getPluginClassName() {
      return this.pluginClassName == null && this.hasParentConfiguration() ? this.parentConfiguration.getPluginClassName() : this.pluginClassName;
   }

   public Class<?> getPluginClass() throws InitializeException {
      if (this.pluginClass != null) {
         return this.pluginClass;
      }

      String className = this.getPluginClassName();
      if (StringUtils.isBlank(className)) {
         throw new InitializeException("No class name set");
      }

      this.pluginClass = PluginLoader.loadClass(className);
      return this.pluginClass;
   }

   @Override
   public MutablePluginConfiguration getParentConfiguration() {
      return this.parentConfiguration;
   }

   public boolean hasParentConfiguration() {
      return this.parentConfiguration != null;
   }

   public Collection<PropertyConfiguration> getDirectlyConfiguredProperties(ConfigurationEnvironment env, ConfigurationContext context) {
      Objects.requireNonNull(context);
      synchronized (this) {
         return this.getEnvironmentSpecificProperties(env).getContextSpecificProperties(context);
      }
   }

   @Override
   public boolean removeProperty(String name, ConfigurationEnvironment env, ConfigurationContext context) {
      Objects.requireNonNull(name);
      Objects.requireNonNull(context);
      synchronized (this) {
         PluginConfigurationProperties propertiesInEnv = this.propertiesPerEnv.get(env);
         if (propertiesInEnv == null) {
            return false;
         }

         PropertyConfiguration removed = propertiesInEnv.removeProperty(name, context);
         if (!env.isCommonEnvironment() && !propertiesInEnv.hasProperties()) {
            this.propertiesPerEnv.remove(env);
         }

         if (removed != null) {
            this.synthetic = false;
            this.propagateEvent(new PluginConfigurationPropertyChangedEvent(this, env, context, removed, null));
            return true;
         } else {
            return false;
         }
      }
   }

   @Override
   public boolean removeProperty(String name) {
      return this.removeProperty(name, ConfigurationEnvironment.common(), ConfigurationContext.defaultContext());
   }

   private void propagateEvent(PluginConfigurationPropertyChangedEvent event) {
      if (this.getOwner() != null) {
         this.getOwner().handlePluginPropertyConfigurationChanged(event);
      }
   }

   private void propagatePluginConfigurationParentChanged(PluginConfigurationParentChangedEvent event) {
      if (this.getOwner() != null) {
         this.getOwner().handlePluginConfigurationParentChanged(event);
      }
   }

   public DefaultPluginConfiguration setProperty(PropertyConfiguration property) {
      return this.setProperty(property, ConfigurationEnvironment.common(), ConfigurationContext.defaultContext());
   }

   public DefaultPluginConfiguration setProperty(PropertyConfiguration property, ConfigurationEnvironment env, ConfigurationContext context) {
      Objects.requireNonNull(property);
      Objects.requireNonNull(context);
      synchronized (this) {
         PropertyConfiguration oldValue = this.getPropertyConfiguration(property.getName(), env, context);
         this.propertiesPerEnv.computeIfAbsent(env, var0 -> new PluginConfigurationProperties()).setProperty(property, context);
         if (this.synthetic) {
            this.synthetic = determineSyntheticStatus(property);
         }

         this.propagateEvent(new PluginConfigurationPropertyChangedEvent(this, env, context, oldValue, property));
         return this;
      }
   }

   private static boolean determineSyntheticStatus(PropertyConfiguration configuration) {
      if (!(configuration instanceof PluginPropertyConfiguration)) {
         return false;
      }

      PluginConfiguration value = ((PluginPropertyConfiguration)configuration).getValue();
      return value != null && value.isSynthetic();
   }

   @Nullable
   public PropertyConfiguration getPropertyConfiguration(String propertyName, ConfigurationEnvironment env, ConfigurationContext context) {
      synchronized (this) {
         PropertyConfiguration result = this.getPropertyConfigurationWithFixedEnvironmentAndContext(propertyName, env, context);
         if (result == null && !env.isCommonEnvironment()) {
            result = this.getPropertyConfigurationWithFixedEnvironmentAndContext(propertyName, ConfigurationEnvironment.common(), context);
         }

         return result == null && !context.isDefaultContext()
            ? this.getPropertyConfiguration(propertyName, env, ConfigurationContext.defaultContext())
            : result;
      }
   }

   @Nullable
   public String getSimplePropertyConfigurationValue(String propertyName, ConfigurationEnvironment env, ConfigurationContext context) throws PropertyTypeMismatchException {
      return (String)this.getTypedPropertyValue(SimplePropertyConfiguration.class, propertyName, env, context);
   }

   @Nullable
   @Override
   public MutablePluginConfiguration getPluginPropertyConfigurationValue(String propertyName, ConfigurationEnvironment env, ConfigurationContext context) throws PropertyTypeMismatchException {
      return (MutablePluginConfiguration)this.getTypedPropertyValue(PluginPropertyConfiguration.class, propertyName, env, context);
   }

   @Nullable
   public List<PluginConfiguration> getPluginListPropertyConfigurationValue(String propertyName, ConfigurationEnvironment env, ConfigurationContext context) throws PropertyTypeMismatchException {
      return (List<PluginConfiguration>)this.getTypedPropertyValue(PluginListPropertyConfiguration.class, propertyName, env, context);
   }

   @Nullable
   public Map<String, PluginConfiguration> getPluginMapPropertyConfigurationValue(
      String propertyName, ConfigurationEnvironment env, ConfigurationContext context
   ) throws PropertyTypeMismatchException {
      return (Map<String, PluginConfiguration>)this.getTypedPropertyValue(PluginMapPropertyConfiguration.class, propertyName, env, context);
   }

   @Nullable
   public List<String> getSimpleListPropertyConfigurationValue(String propertyName, ConfigurationEnvironment env, ConfigurationContext context) throws PropertyTypeMismatchException {
      return (List<String>)this.getTypedPropertyValue(SimpleListPropertyConfiguration.class, propertyName, env, context);
   }

   private Object getTypedPropertyValue(Class<?> targetPropertyType, String propertyName, ConfigurationEnvironment env, ConfigurationContext context) throws PropertyTypeMismatchException {
      PropertyConfiguration p = this.getPropertyConfiguration(propertyName, env, context);
      if (p == null) {
         return null;
      } else if (targetPropertyType.isAssignableFrom(p.getClass())) {
         return p.getValue();
      } else {
         throw new PropertyTypeMismatchException(
            propertyName,
            this,
            env,
            context,
            "Type mismatch: Property configuration for property \""
               + propertyName
               + "\": has type "
               + p.getClass().getSimpleName()
               + ". Tried to use it as "
               + targetPropertyType.getSimpleName()
               + "."
         );
      }
   }

   private PropertyConfiguration getPropertyConfigurationWithFixedEnvironmentAndContext(
      String propertyName, ConfigurationEnvironment env, ConfigurationContext context
   ) {
      Objects.requireNonNull(propertyName);
      Objects.requireNonNull(context);
      synchronized (this) {
         PropertyConfiguration result = this.getPropertyWithFixedEnvironmentAndContextNoAncestors(propertyName, env, context);
         return result == null && this.hasParentConfiguration()
            ? this.parentConfiguration.getPropertyConfigurationWithFixedEnvironmentAndContext(propertyName, env, context)
            : result;
      }
   }

   private PropertyConfiguration getPropertyWithFixedEnvironmentAndContextNoAncestors(
      String propertyName, ConfigurationEnvironment env, ConfigurationContext context
   ) {
      synchronized (this) {
         return this.getEnvironmentSpecificProperties(env).getProperty(propertyName, context);
      }
   }

   @Override
   public int hashCode() {
      return System.identityHashCode(this);
   }

   @Override
   public boolean equals(Object obj) {
      return this == obj;
   }

   public String getPluginDisplayName() {
      return this.internalDisplayName;
   }

   @Override
   public void setPluginDisplayName(String displayName) {
      this.internalDisplayName = displayName;
      if (displayName != null) {
         this.synthetic = false;
         this.propagateEvent(
            new PluginConfigurationPropertyChangedEvent(this, ConfigurationEnvironment.common(), ConfigurationContext.defaultContext(), null, null)
         );
      }
   }

   public boolean isOfPluginClass(Class<?> targetPluginClass) throws ConfigurationException {
      try {
         return targetPluginClass.isAssignableFrom(this.getPluginClass());
      } catch (InitializeException e) {
         throw new ConfigurationException(
            this,
            ConfigurationEnvironment.common(),
            ConfigurationContext.defaultContext(),
            "Unable to load plugin class \"" + this.getPluginClassName() + "\"",
            e
         );
      }
   }

   public String getComment() {
      return this.comment;
   }

   @Override
   public void setComment(String comment) {
      this.comment = comment;
      this.synthetic = false;
      this.propagateEvent(
         new PluginConfigurationPropertyChangedEvent(this, ConfigurationEnvironment.common(), ConfigurationContext.defaultContext(), null, null)
      );
   }

   public boolean hasProperties() {
      for (PluginConfigurationProperties pluginConfigurationProperties : this.propertiesPerEnv.values()) {
         if (pluginConfigurationProperties.hasProperties()) {
            return true;
         }
      }

      return false;
   }

   public boolean isPropertyConfigured(String propertyName, ConfigurationEnvironment env, ConfigurationContext context) {
      return this.getPropertyConfigurationWithFixedEnvironmentAndContext(propertyName, env, context) != null
            && !this.hasSyntheticPluginPropertyConfigValue(propertyName, env, context)
         || this.getPropertyConfigurationWithFixedEnvironmentAndContext(propertyName, ConfigurationEnvironment.common(), context) != null
            && !this.hasSyntheticPluginPropertyConfigValue(propertyName, ConfigurationEnvironment.common(), context);
   }

   @VisibleForTesting
   boolean hasSyntheticPluginPropertyConfigValue(String propertyName, ConfigurationEnvironment env, ConfigurationContext context) {
      PropertyConfiguration pp = this.getPropertyConfigurationWithFixedEnvironmentAndContext(propertyName, env, context);
      if (!(pp instanceof PluginPropertyConfiguration)) {
         return false;
      }

      PluginConfiguration pc = (PluginConfiguration)pp.getValue();
      return pc != null && pc.isSynthetic();
   }

   public boolean isPropertyDefinedForEnvironmentAndContext(String propertyName, ConfigurationEnvironment env, ConfigurationContext context) {
      return this.getPropertyConfigurationWithFixedEnvironmentAndContext(propertyName, env, context) != null;
   }

   public boolean isPropertyDefinedForContext(String propertyName, ConfigurationEnvironment env, ConfigurationContext context) {
      return this.getPropertyConfigurationWithFixedEnvironmentAndContext(propertyName, env, context) != null
         || this.getPropertyConfigurationWithFixedEnvironmentAndContext(propertyName, ConfigurationEnvironment.common(), context) != null;
   }

   public boolean isPropertyConfigurationFromAncestor(String propertyName, ConfigurationEnvironment env, ConfigurationContext context) {
      PropertyConfiguration result = this.getPropertyWithFixedEnvironmentAndContextNoAncestors(propertyName, env, context);
      return result == null
         && this.hasParentConfiguration()
         && this.parentConfiguration.getPropertyConfigurationWithFixedEnvironmentAndContext(propertyName, env, context) != null;
   }

   public boolean isPropertyDefinedLocallyInEnvironmentAndContext(String propertyName, ConfigurationEnvironment env, ConfigurationContext context) {
      return this.getPropertyWithFixedEnvironmentAndContextNoAncestors(propertyName, env, context) != null;
   }

   public SortedSet<ConfigurationContext> getUsedConfigurationContexts(ConfigurationEnvironment env) {
      SortedSet<ConfigurationContext> res = new TreeSet<>();
      this.fillUsedConfigurationContexts(res, env);
      res.add(ConfigurationContext.defaultContext());
      return res;
   }

   public SortedSet<ConfigurationContext> getLocallyUsedConfigurationContexts(ConfigurationEnvironment env) {
      return new TreeSet<>(this.getEnvironmentSpecificProperties(env).getAllConfigurationContexts());
   }

   public SortedSet<ConfigurationEnvironment> getLocallyUsedConfigurationEnvironments() {
      return new TreeSet<>(this.propertiesPerEnv.keySet());
   }

   @Override
   public void addLocallyUsedConfigurationContextsInAllEnvironments(Set<ConfigurationContext> set) {
      for (ConfigurationEnvironment env : this.propertiesPerEnv.keySet()) {
         set.addAll(this.getEnvironmentSpecificProperties(env).getAllConfigurationContexts());
      }
   }

   void fillUsedConfigurationContexts(Set<ConfigurationContext> usedContexts, ConfigurationEnvironment env) {
      synchronized (this) {
         PluginConfigurationProperties pluginConfigurationProperties = new PluginConfigurationPropertiesView(this, env)
            .inheritFromParent()
            .inheritFromCommonEnvironment()
            .getProperties();
         usedContexts.addAll(pluginConfigurationProperties.getAllConfigurationContexts());
         pluginConfigurationProperties.allProperties()
            .map(VisitableProperty.class::cast)
            .forEach(property -> property.accept(new ContextCollector(usedContexts, env)));
      }
   }

   public String getPluginClassDisplayName() {
      try {
         return PluginLoader.getDisplayName(this.getPluginClass());
      } catch (InitializeException e) {
         String[] parts = CLASS_SEPARATOR.split(this.getPluginClassName());
         return PluginDisplayNameReader.pluginDisplayName(parts[parts.length - 1]);
      }
   }

   public String getClassNameAndDisplayName() {
      return this.getPluginClassDisplayName() + (StringUtils.isNotBlank(this.internalDisplayName) ? " - " + this.internalDisplayName : "");
   }

   public boolean isParentOf(PluginConfiguration pc) {
      return pc != null && pc.getParentConfiguration() != null && pc.getParentConfiguration() == this;
   }

   @Override
   public void removeAllPropertiesForContext(ConfigurationContext context) {
      Objects.requireNonNull(context);
      Validate.isTrue(!context.isDefaultContext(), "Cannot remove all default context properties.", new Object[0]);
      this.propertiesPerEnv.values().forEach(p -> p.removeAllPropertiesInContext(context));
   }

   public boolean isSynthetic() {
      return this.synthetic;
   }

   public DefaultPluginConfiguration setSynthetic(boolean syntheticFlag) {
      this.synthetic = syntheticFlag;
      this.propagateEvent(
         new PluginConfigurationPropertyChangedEvent(this, ConfigurationEnvironment.common(), ConfigurationContext.defaultContext(), null, null)
      );
      return this;
   }

   public boolean hasPluginDisplayName() {
      return this.internalDisplayName != null;
   }

   @Override
   public void setGlobalConfiguration(GlobalConfigurationAccessor accessor) {
      this.globalConfigurationAccessor = accessor;
   }

   public MutablePluginConfiguration getGlobalPluginOrDefault(String id, ConfigurationEnvironment env) {
      Optional<PluginConfigurationDictionary.GlobalDescription> description = this.globalConfigurationAccessor.getGlobalPluginDescription(id, env);
      if (description.isEmpty()) {
         return null;
      } else if (description.get().plugin() != null) {
         return description.get().plugin();
      } else {
         return description.get().defaultPlugin() != null ? description.get().defaultPlugin() : null;
      }
   }

   public boolean isGlobalConfiguration(ConfigurationEnvironment env) {
      return this.globalConfigurationAccessor.isGlobalConfig(this, env);
   }

   public boolean hasGlobalReference() throws InitializeException {
      for (Field field : PluginClassCache.collectAllAnnotatedFields(this.getPluginClass(), PluginProperty.class)) {
         if (PluginPropertyValueReader.hasGlobalReference(field.getAnnotation(PluginProperty.class))) {
            return true;
         }
      }

      return false;
   }

   public String getUuid() {
      if (this.uuid == null) {
         this.uuid = UUID.randomUUID().toString();
      }

      return this.uuid;
   }

   @Override
   public void setUuid(@NonNull String aUuid) {
      if (aUuid == null) {
         throw new NullPointerException("aUuid is marked non-null but is null");
      }

      this.uuid = aUuid;
      this.synthetic = false;
   }

   public MutablePluginConfiguration createSyntheticPlugin(Class<?> pluginClass) {
      return this.owner == null
         ? new DefaultPluginConfiguration(null, pluginClass.getName()).setSynthetic(true)
         : this.owner.createSyntheticPlugin(pluginClass);
   }

   @Override
   public String toString() {
      return this.getClassNameAndDisplayName();
   }

   @Override
   public void setOwner(PluginConfigurationOwner owner) {
      this.owner = owner;
   }

   public PluginConfigurationOwner getOwner() {
      return this.owner;
   }
}
