package com.airlock.iam.plugin.configeditor.data;

import com.airlock.iam.base.api.domain.model.exception.FailureException;
import com.airlock.iam.plugin.api.annotation.property.BooleanProperty;
import com.airlock.iam.plugin.api.annotation.property.DoubleProperty;
import com.airlock.iam.plugin.api.annotation.property.EnumProperty;
import com.airlock.iam.plugin.api.annotation.property.FileProperty;
import com.airlock.iam.plugin.api.annotation.property.IntegerProperty;
import com.airlock.iam.plugin.api.annotation.property.LongProperty;
import com.airlock.iam.plugin.api.annotation.property.PluginListProperty;
import com.airlock.iam.plugin.api.annotation.property.PluginMapProperty;
import com.airlock.iam.plugin.api.annotation.property.PluginProperty;
import com.airlock.iam.plugin.api.annotation.property.RegexListProperty;
import com.airlock.iam.plugin.api.annotation.property.RegexProperty;
import com.airlock.iam.plugin.api.annotation.property.StringListProperty;
import com.airlock.iam.plugin.api.annotation.property.StringProperty;
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.exception.InitializeException;
import com.airlock.iam.plugin.api.exception.PropertyConfigurationException;
import com.airlock.iam.plugin.api.exception.PropertyConstraintViolationException;
import com.airlock.iam.plugin.api.property.PropertyConfiguration;
import com.airlock.iam.plugin.api.valuereader.PluginPropertyValueReader;
import com.airlock.iam.plugin.configeditor.ConfigEditorApplication;
import com.airlock.iam.plugin.configeditor.rendering.VaadinBooleanPropertyRenderer;
import com.airlock.iam.plugin.configeditor.rendering.VaadinDoublePropertyRenderer;
import com.airlock.iam.plugin.configeditor.rendering.VaadinEnumPropertyRenderer;
import com.airlock.iam.plugin.configeditor.rendering.VaadinFilePropertyRenderer;
import com.airlock.iam.plugin.configeditor.rendering.VaadinIntegerPropertyRenderer;
import com.airlock.iam.plugin.configeditor.rendering.VaadinLongPropertyRenderer;
import com.airlock.iam.plugin.configeditor.rendering.VaadinPluginListPropertyRenderer;
import com.airlock.iam.plugin.configeditor.rendering.VaadinPluginMapPropertyRenderer;
import com.airlock.iam.plugin.configeditor.rendering.VaadinPluginPropertyRenderer;
import com.airlock.iam.plugin.configeditor.rendering.VaadinPropertyRenderer;
import com.airlock.iam.plugin.configeditor.rendering.VaadinRegexListPropertyRenderer;
import com.airlock.iam.plugin.configeditor.rendering.VaadinRegexPropertyRenderer;
import com.airlock.iam.plugin.configeditor.rendering.VaadinStringListPropertyRenderer;
import com.airlock.iam.plugin.configeditor.rendering.VaadinStringPropertyRenderer;
import com.airlock.iam.plugin.framework.application.configuration.MutablePluginConfiguration;
import com.airlock.iam.plugin.framework.application.service.license.LicenseManager;
import com.airlock.iam.plugin.framework.misc.processor.FieldProcessor;
import com.airlock.iam.plugin.framework.misc.processor.PasswordPropertyProcessor;
import com.airlock.iam.plugin.framework.misc.processor.PluginListPropertyProcessor;
import com.airlock.iam.plugin.framework.misc.processor.PluginMapPropertyProcessor;
import com.airlock.iam.plugin.framework.misc.processor.PluginPropertyProcessor;
import com.airlock.iam.plugin.framework.misc.processor.PropertyProcessor;
import com.airlock.iam.plugin.framework.misc.processor.PropertyType;
import com.google.common.collect.ImmutableMap;
import com.vaadin.ui.UI;
import jakarta.annotation.Nullable;
import java.io.File;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.Map.Entry;
import java.util.regex.Pattern;

public class PropertyAdapter<T extends Annotation> {
   private static final Map<Class<? extends Annotation>, VaadinPropertyRenderer> RENDERERS = ImmutableMap.builder()
      .put(StringProperty.class, new VaadinStringPropertyRenderer())
      .put(StringListProperty.class, new VaadinStringListPropertyRenderer())
      .put(RegexProperty.class, new VaadinRegexPropertyRenderer())
      .put(RegexListProperty.class, new VaadinRegexListPropertyRenderer())
      .put(IntegerProperty.class, new VaadinIntegerPropertyRenderer())
      .put(LongProperty.class, new VaadinLongPropertyRenderer())
      .put(DoubleProperty.class, new VaadinDoublePropertyRenderer())
      .put(BooleanProperty.class, new VaadinBooleanPropertyRenderer())
      .put(EnumProperty.class, new VaadinEnumPropertyRenderer())
      .put(FileProperty.class, new VaadinFilePropertyRenderer())
      .put(PluginProperty.class, new VaadinPluginPropertyRenderer())
      .put(PluginListProperty.class, new VaadinPluginListPropertyRenderer())
      .put(PluginMapProperty.class, new VaadinPluginMapPropertyRenderer())
      .build();
   private final Field configField;
   private final PropertyProcessor propertyProcessor;
   private final MutablePluginConfiguration pluginConfiguration;
   private final ConfigurationEnvironment environment;
   private final ConfigurationContext configurationContext;
   private final VaadinPropertyRenderer propertyRenderer;
   private Class<?> configFieldType;

   public PropertyAdapter(
      Field configField,
      PropertyProcessor propertyProcessor,
      MutablePluginConfiguration pluginConfiguration,
      ConfigurationEnvironment environment,
      ConfigurationContext configurationContext
   ) {
      this.configField = configField;
      this.propertyProcessor = propertyProcessor;
      this.pluginConfiguration = pluginConfiguration;
      this.environment = environment;
      this.configurationContext = configurationContext;
      this.determineModelType(configField);
      this.propertyRenderer = this.findRenderer(configField, propertyProcessor);
   }

   private void determineModelType(Field field) {
      if (field.getType().isPrimitive()) {
         this.configFieldType = convertPrimitiveType(field.getType());
      } else {
         this.configFieldType = this.convertTypeToString(field.getType());
      }
   }

   private VaadinPropertyRenderer findRenderer(Field field, FieldProcessor processor) {
      Annotation annotation = processor.getAnnotation(field);

      for (Entry<Class<? extends Annotation>, VaadinPropertyRenderer> entry : RENDERERS.entrySet()) {
         Class<? extends Annotation> annotationClass = entry.getKey();
         if (annotationClass.isAssignableFrom(annotation.getClass())) {
            return entry.getValue();
         }
      }

      throw new IllegalStateException("unknown field type '" + field.getType().getName() + "' with annotation '" + annotation.annotationType() + "'");
   }

   public final boolean isMandatory() {
      return this.propertyProcessor.isMandatory(this.configField);
   }

   private String renderValidatonErrorMessage(PropertyConfigurationException e) {
      return e.getPropertyNames() + ": " + e.getDescription();
   }

   public ConfigurableProperty.ValueStatus getValueStatus() {
      if (this.isInheritedFromParent()) {
         return ConfigurableProperty.ValueStatus.INHERITED_FROM_PARENT;
      } else if (this.isInheritedFromCommonEnvironment()) {
         return ConfigurableProperty.ValueStatus.INHERITED_FROM_COMMON_ENVIRONMENT;
      } else if (this.isInheritedFromDefaultContext()) {
         return ConfigurableProperty.ValueStatus.INHERITED_FROM_DEFAULTCONTEXT;
      } else if (this.isDefinedLocally()) {
         return ConfigurableProperty.ValueStatus.DEFINED_LOCALLY;
      } else if (this.isDefaultDefinedLocally()) {
         return ConfigurableProperty.ValueStatus.DEFAULT_DEFINED_LOCALLY;
      } else if (this.isValueFromDefault()) {
         return ConfigurableProperty.ValueStatus.DEFAULT;
      } else if (this.isUndefined()) {
         return ConfigurableProperty.ValueStatus.UNDEFINED;
      } else {
         throw new IllegalStateException(String.format("Configuration field '%s' has an unknown status.", this.configField.getName()));
      }
   }

   private boolean isDefinedLocally() {
      return this.getPropertyType() == PropertyType.PLUGIN
         ? this.hasProperty() && (this.hasPropertyValue() || !this.hasDefaultValue() && !this.isGlobal()) && !this.isSyntheticPluginProperty()
            || this.isLocalGlobalReference()
         : this.hasProperty() && (this.hasPropertyValue() || !this.hasDefaultValue());
   }

   private boolean isLocalGlobalReference() {
      return this.isPropertyDefinedLocallyInEnvironmentAndContext() && !this.hasPropertyValue() && this.isGlobal();
   }

   public boolean isPropertyDefinedLocallyInEnvironmentAndContext() {
      return this.pluginConfiguration.isPropertyDefinedLocallyInEnvironmentAndContext(this.configField.getName(), this.environment, this.configurationContext);
   }

   private boolean isDefaultDefinedLocally() {
      return this.getPropertyType() == PropertyType.PLUGIN
         ? this.hasInheritance() && this.isSyntheticPluginProperty()
         : this.hasProperty() && !this.hasPropertyValue() && this.hasDefaultValue();
   }

   public boolean isValueFromDefault() {
      return this.getPropertyType() == PropertyType.PLUGIN
         ? this.isSyntheticPluginProperty()
         : (!this.hasProperty() || !this.hasPropertyValue()) && this.hasDefaultValue() && !this.isInherited();
   }

   public boolean isValueFromGlobalReference() {
      return this.getPropertyType() == PropertyType.PLUGIN && (!this.hasProperty() || !this.hasPropertyValue()) && this.isGlobal();
   }

   private boolean isSyntheticPluginProperty() {
      return this.getPropertyType() == PropertyType.PLUGIN
         && this.hasProperty()
         && this.hasPropertyValue()
         && ((MutablePluginConfiguration)this.getPropertyConfiguration().getValue()).isSynthetic();
   }

   private boolean hasProperty() {
      return this.getPropertyConfiguration() != null;
   }

   private boolean hasPropertyValue() {
      return this.getPropertyConfiguration().getValue() != null;
   }

   public boolean hasVariable() {
      return this.hasProperty() && this.getPropertyConfiguration().hasVariable();
   }

   public Optional<ConfigurationVariable> getVariable() {
      return Optional.ofNullable(this.getPropertyConfiguration()).flatMap(PropertyConfiguration::getVariable);
   }

   public void setVariable(ConfigurationVariable variable) {
      this.getPropertyConfiguration().setVariable(variable);
   }

   public void unsetVariable() {
      this.getPropertyConfiguration().unsetVariable();
   }

   private boolean isInheritedFromParent() {
      return !this.pluginConfiguration.hasParentConfiguration()
         ? false
         : this.pluginConfiguration.isPropertyConfigurationFromAncestor(this.configField.getName(), this.environment, this.configurationContext)
            || !this.hasProperty();
   }

   private boolean isInheritedFromCommonEnvironment() {
      if (this.environment.isCommonEnvironment()) {
         return false;
      } else {
         return !this.hasProperty()
            ? true
            : !this.pluginConfiguration.isPropertyDefinedForEnvironmentAndContext(this.configField.getName(), this.environment, this.configurationContext)
               && this.pluginConfiguration
                  .isPropertyDefinedForEnvironmentAndContext(this.configField.getName(), ConfigurationEnvironment.common(), this.configurationContext);
      }
   }

   private boolean isInheritedFromDefaultContext() {
      return this.configurationContext.isDefaultContext()
         ? false
         : !this.pluginConfiguration.isPropertyDefinedForContext(this.configField.getName(), this.environment, this.configurationContext)
            || !this.hasProperty();
   }

   private boolean isUndefined() {
      return this.getPropertyType() == PropertyType.PLUGIN
         ? !this.isInherited() && (!this.hasProperty() || this.isGlobal())
         : !this.hasProperty() && !this.isInherited();
   }

   private boolean isGlobal() {
      return PluginPropertyValueReader.hasGlobalReference(this.getAnnotation(PluginProperty.class));
   }

   boolean isInherited() {
      return this.isInheritedFromParent() || this.isInheritedFromCommonEnvironment() || this.isInheritedFromDefaultContext();
   }

   public void setValue(Object newValue) {
      if (this.isEmptyValue(newValue)) {
         if (this.hasInheritance()) {
            this.setValueInConfiguration(null);
         } else {
            this.removeProperty();
         }
      } else {
         this.setValueInConfiguration(newValue);
      }
   }

   private boolean isEmptyValue(Object newValue) {
      return newValue == null || newValue.toString().isEmpty();
   }

   boolean hasInheritance() {
      return !this.configurationContext.isDefaultContext() || !this.environment.isCommonEnvironment() || this.pluginConfiguration.hasParentConfiguration();
   }

   public void setValueInConfiguration(Object newValue) {
      try {
         this.propertyProcessor
            .setValueInConfiguration(this.pluginConfiguration, this.configField, this.environment, this.configurationContext, this.getVariable(), newValue);
      } catch (PropertyConstraintViolationException e) {
         throw new FailureException(this.renderValidatonErrorMessage(e));
      }
   }

   public Optional<Object> getValue() {
      PropertyConfiguration propertyConfiguration = this.getPropertyConfiguration();
      if (propertyConfiguration != null) {
         return this.propertyProcessor instanceof PasswordPropertyProcessor
            ? Optional.ofNullable(this.propertyProcessor.translateFromConfigToModel(this.configField, propertyConfiguration))
            : Optional.ofNullable(this.propertyProcessor.translateFromConfigToModel(this.configField, propertyConfiguration.getValue()));
      } else {
         return Optional.empty();
      }
   }

   private PropertyConfiguration getPropertyConfiguration() {
      return this.pluginConfiguration.getPropertyConfiguration(this.configField.getName(), this.environment, this.configurationContext);
   }

   public Optional<?> getDefaultValue() {
      return this.getPropertyType() == PropertyType.PLUGIN
         ? ((PluginPropertyProcessor)this.propertyProcessor).getGlobalPluginConfig(this.pluginConfiguration, this.configField, this.environment)
         : Optional.ofNullable(this.propertyProcessor.getDefaultValue(this.configField));
   }

   @Nullable
   Class<?> getDefaultValueOnAnnotation() {
      return this.getPropertyType() == PropertyType.PLUGIN
         ? ((PluginPropertyProcessor)this.propertyProcessor).getAnnotation(this.configField).defaultValue()
         : null;
   }

   boolean hasDefaultValueOnPluginPropertyAnnotation() {
      Class<?> annotationDefaultValue = this.getDefaultValueOnAnnotation();
      return annotationDefaultValue != null && !Object.class.equals(annotationDefaultValue);
   }

   public boolean hasDefaultValue() {
      return this.propertyProcessor.hasDefaultValue(this.configField);
   }

   public void removeProperty() {
      this.pluginConfiguration.removeProperty(this.configField.getName(), this.environment, this.configurationContext);
   }

   public <U extends Annotation> U getAnnotation(Class<U> annotationType) {
      return this.configField.getAnnotation(annotationType);
   }

   public T getFieldAnnotation() {
      return (T)this.propertyProcessor.getAnnotation(this.configField);
   }

   public String getConfigPropertyName() {
      return this.configField.getName();
   }

   boolean isPluginProperty() {
      return this.getPropertyType() == PropertyType.PLUGIN
         || this.getPropertyType() == PropertyType.PLUGIN_LIST
         || this.getPropertyType() == PropertyType.PLUGIN_MAP;
   }

   private PropertyType getPropertyType() {
      return this.propertyProcessor.getPropertyType();
   }

   public Class<?> getConfigFieldType() {
      return this.configFieldType;
   }

   public String getDisplayName() {
      return this.propertyProcessor.getDisplayName(this.configField);
   }

   public String getPropertyName() {
      return this.configField.getName();
   }

   public Set<String> getRequiredLicenseTags() {
      return getLicenseManager().getRequiredTagsForProperty(this.pluginConfiguration.getPluginClassName(), this.configField.getName());
   }

   public com.vaadin.v7.ui.Field<?> createField() {
      return this.propertyRenderer.createField(this);
   }

   public boolean isSameConfigurationProperty(MutablePluginConfiguration pc, ConfigurationContext cc, List<String> propertyNames) {
      return this.pluginConfiguration.equals(pc) && this.configurationContext.equals(cc) && propertyNames.contains(this.getConfigPropertyName());
   }

   public Class<?> getCollectionElementType() {
      return PluginListPropertyProcessor.getListElementType(this.configField);
   }

   public Class<?> getMapElementType() {
      return PluginMapPropertyProcessor.getMapElementValueType(this.configField);
   }

   public MutablePluginConfiguration getPluginConfiguration() {
      return this.pluginConfiguration;
   }

   public Optional<Class<?>> getPluginClass() {
      try {
         return Optional.ofNullable(this.pluginConfiguration.getPluginClass());
      } catch (InitializeException e) {
         return Optional.empty();
      }
   }

   public ConfigurationContext getConfigurationContext() {
      return this.configurationContext;
   }

   public ConfigurationEnvironment getConfigurationEnvironment() {
      return this.environment;
   }

   private Class<?> convertTypeToString(Class<?> type) {
      if (Pattern.class.isAssignableFrom(type)) {
         type = String.class;
      } else if (Number.class.isAssignableFrom(type)) {
         type = String.class;
      } else if (File.class.isAssignableFrom(type)) {
         type = String.class;
      }

      return type;
   }

   private static Class<?> convertPrimitiveType(Class<?> type) {
      if (type.equals(boolean.class)) {
         type = Boolean.class;
      } else if (type.equals(int.class)) {
         type = String.class;
      } else if (type.equals(float.class)) {
         type = String.class;
      } else if (type.equals(double.class)) {
         type = String.class;
      } else if (type.equals(byte.class)) {
         type = String.class;
      } else if (type.equals(char.class)) {
         type = Character.class;
      } else if (type.equals(short.class)) {
         type = String.class;
      } else if (type.equals(long.class)) {
         type = String.class;
      }

      return type;
   }

   private static LicenseManager getLicenseManager() {
      return ((ConfigEditorApplication)UI.getCurrent()).getLicenseManager();
   }
}
