package com.airlock.iam.common.infrastructure.factory;

import com.airlock.iam.base.api.domain.model.plugin.LogicForConfig;
import com.airlock.iam.base.api.infrastructure.injection.SkipAutoRegistration;
import com.airlock.iam.common.infrastructure.plugin.PluginLogicFinder;
import com.airlock.iam.common.infrastructure.plugin.PluginRepository;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.plugin.api.annotation.property.PluginListProperty;
import com.airlock.iam.plugin.api.annotation.property.PluginProperty;
import com.airlock.iam.plugin.api.exception.PluginClassLoadingException;
import com.airlock.iam.plugin.api.valuereader.PluginPropertyValueReader;
import com.airlock.iam.plugin.impl.cache.PluginClassCache;
import com.google.inject.Injector;
import com.google.inject.PrivateModule;
import com.google.inject.TypeLiteral;
import com.google.inject.util.Types;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.inject.Provider;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Map.Entry;
import java.util.function.Function;
import lombok.NonNull;

@SkipAutoRegistration
public class PluginConfigToLogicInlineModule extends PrivateModule {
   private static final ELogger LOG = ELoggerFactory.getLogger(PluginConfigToLogicInlineModule.class);
   @NonNull
   private final Object config;
   @NonNull
   private final Class<?> logicType;
   @NonNull
   private final Injector injector;
   @NonNull
   private final PluginLogicFinder logicFinder;

   protected void configure() {
      this.bind(this.logicType);
      this.expose(this.logicType);
      Class<?> configType = this.config.getClass();
      this.bind(configType).toInstance(this.config);
      this.expose(configType);
      this.bindAnnotatedSuperClasses(configType);

      try {
         this.configurePluginProperties();
         this.configurePluginListProperties();
      } catch (PluginClassLoadingException e) {
         throw new IllegalStateException(e);
      }
   }

   private void bindAnnotatedSuperClasses(Class<?> configType) {
      LogicForConfig annotation = this.logicType.getAnnotation(LogicForConfig.class);
      Arrays.stream(annotation.value()).filter(configClass -> !configClass.equals(configType)).forEach(configClass -> {
         if (configClass.isAssignableFrom(configType)) {
            this.bind(configClass).toInstance(this.config);
            this.expose(configClass);
         }
      });
   }

   private void configurePluginProperties() throws PluginClassLoadingException {
      PluginRepository repository = (PluginRepository)this.injector.getInstance(PluginRepository.class);

      for (Entry<Field, Class<?>> fieldToType : this.findFieldsWithBindableTypes(repository, PluginProperty.class, Field::getType).entrySet()) {
         Field field = fieldToType.getKey();
         Class<?> bindToType = fieldToType.getValue();
         Object value = this.getValue(field);
         if (value == null) {
            this.bindUnconfiguredValueToType(field, bindToType);
         } else {
            Optional<Class<?>> valueLogicType = repository.findLogicTypeForConfigType(value.getClass());
            if (valueLogicType.isPresent()) {
               this.bindValueToType(field, bindToType, value);
            } else {
               LOG.debug("No logic instance found for config of property " + this.propertyName(field) + " of type " + value.getClass());
            }
         }
      }
   }

   private Map<Field, Class<?>> findFieldsWithBindableTypes(
      PluginRepository repository, Class<? extends Annotation> annotationClass, Function<Field, Class<?>> determineConfigType
   ) throws PluginClassLoadingException {
      Map<Field, Class<?>> fieldToTypeMapping = new HashMap<>();
      Map<Class, Field> seenTypes = new HashMap<>();

      for (Field field : PluginClassCache.collectAllAnnotatedFields(this.config.getClass(), annotationClass)) {
         Class<?> configType = determineConfigType.apply(field);
         Optional<Class<?>> bindToType = repository.findLogicTypeForConfigType(configType);
         if (bindToType.isPresent()) {
            if (seenTypes.containsKey(bindToType.get())) {
               String configPropertyType = annotationClass.equals(PluginListProperty.class) ? "List<%s>".formatted(configType) : configType.toString();
               LOG.debug("There are multiple properties of type " + configPropertyType + ", not binding any of them a logic instance to avoid conflicts.");
               fieldToTypeMapping.remove(seenTypes.get(bindToType.get()));
            } else {
               fieldToTypeMapping.put(field, bindToType.get());
               seenTypes.put(bindToType.get(), field);
            }
         }
      }

      return fieldToTypeMapping;
   }

   private void bindUnconfiguredValueToType(Field field, Class type) {
      if (PluginPropertyValueReader.isOptional(field)) {
         this.bind(this.optionalType(type)).toProvider(Optional::empty);
         LOG.debug("Binding Optional<" + type.getSimpleName() + "> to Optional.empty() for unconfigured plugin property " + this.propertyName(field) + ".");
      } else {
         throw new IllegalStateException("Mandatory property " + this.propertyName(field) + " is not configured");
      }
   }

   private void bindValueToType(Field field, Class type, Object configValue) {
      if (PluginPropertyValueReader.canHaveNullValue(field)) {
         this.bind(this.optionalType(type)).toProvider(() -> Optional.of(this.createPropertyLogic(configValue)));
         logBinding("Optional<" + type.getSimpleName() + ">", "optional plugin property " + this.propertyName(field));
      } else {
         this.bind(type).toProvider(() -> this.createPropertyLogic(configValue));
         logBinding(type.getSimpleName(), "mandatory plugin property " + this.propertyName(field));
      }
   }

   private String propertyName(Field field) {
      return this.config.getClass().getSimpleName() + "." + field.getName();
   }

   private TypeLiteral optionalType(Class type) {
      return TypeLiteral.get(Types.newParameterizedType(Optional.class, new Type[]{type}));
   }

   private void configurePluginListProperties() throws PluginClassLoadingException {
      PluginRepository repository = (PluginRepository)this.injector.getInstance(PluginRepository.class);
      Function<Field, Class<?>> determineConfigTypeOfListField = f -> (Class<?>)((ParameterizedType)f.getGenericType()).getActualTypeArguments()[0];

      for (Entry<Field, Class<?>> fieldToType : this.findFieldsWithBindableTypes(repository, PluginListProperty.class, determineConfigTypeOfListField)
         .entrySet()) {
         Field field = fieldToType.getKey();
         Class<?> logicElementType = fieldToType.getValue();
         List<?> value = (List<?>)this.getValue(field);
         Provider logicListProvider = () -> this.createLogicList(value);
         TypeLiteral listTypeLiteral = TypeLiteral.get(Types.listOf(logicElementType));
         this.bind(listTypeLiteral).toProvider(logicListProvider);
         logBinding("List<" + logicElementType.getSimpleName() + ">", "list property " + this.propertyName(field));
      }
   }

   private static void logBinding(String bindingType, String propertyDesc) {
      LOG.debug("Binding " + bindingType + " to logic instance for " + propertyDesc + ".");
   }

   private Object createPropertyLogic(Object propertyConfig) {
      return this.logicFinder.findPluginLogic(propertyConfig, this.injector);
   }

   private List<?> createLogicList(List<?> configList) {
      return configList != null && !configList.isEmpty() ? configList.stream().map(this::createPropertyLogic).toList() : Collections.emptyList();
   }

   private Object getValue(Field field) {
      try {
         field.setAccessible(true);
         return field.get(this.config);
      } catch (IllegalArgumentException | IllegalAccessException e) {
         return null;
      }
   }

   @SuppressFBWarnings(justification = "generated code")
   public PluginConfigToLogicInlineModule(
      @NonNull Object config, @NonNull Class<?> logicType, @NonNull Injector injector, @NonNull PluginLogicFinder logicFinder
   ) {
      if (config == null) {
         throw new NullPointerException("config is marked non-null but is null");
      }

      if (logicType == null) {
         throw new NullPointerException("logicType is marked non-null but is null");
      }

      if (injector == null) {
         throw new NullPointerException("injector is marked non-null but is null");
      }

      if (logicFinder == null) {
         throw new NullPointerException("logicFinder is marked non-null but is null");
      }

      this.config = config;
      this.logicType = logicType;
      this.injector = injector;
      this.logicFinder = logicFinder;
   }
}
