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

import com.airlock.iam.base.api.domain.model.exception.FailureException;
import com.airlock.iam.base.api.domain.model.plugin.LogicForConfig;
import com.airlock.iam.base.api.infrastructure.plugin.ModuleForConfig;
import com.airlock.iam.base.infrastructure.reflection.ReflectionHelper;
import com.airlock.iam.elog.api.EDefaultLogType;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.elog.api.LogTypes;
import com.google.inject.Module;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

public final class PluginRepository {
   private static final ELogger LOG = ELoggerFactory.getLogger(PluginRepository.class);
   private final Map<Class<?>, Class<?>> configToLogic;
   private final Map<Class<?>, Set<Module>> configToModule;

   public PluginRepository(Collection<Class<?>> logicTypes, Collection<Class<? extends Module>> moduleTypes) {
      this.configToLogic = this.indexLogicTypes(logicTypes);
      this.configToModule = this.indexModuleTypes(moduleTypes);
   }

   public Optional<Class<?>> findLogicTypeForConfigType(Class<?> configType) {
      Class<?> logicType = this.configToLogic.get(configType);
      return logicType == null && configType.getSuperclass() != null
         ? this.findLogicTypeForConfigType(configType.getSuperclass()).or(() -> this.findLogicTypeWithConfigInterfaces(configType))
         : Optional.ofNullable(logicType);
   }

   private Optional<? extends Class<?>> findLogicTypeWithConfigInterfaces(Class<?> configType) {
      List<? extends Class<?>> interfaceLogics = Arrays.stream(configType.getInterfaces())
         .map(this::findLogicTypeForConfigType)
         .filter(Optional::isPresent)
         .map(Optional::get)
         .toList();
      if (interfaceLogics.size() > 1) {
         throw new FailureException(
            String.format(
               "There are multiple plugin logics referring to the same plugin config type %s (via interface): found %s",
               configType.getName(),
               interfaceLogics.stream().map(Class::getName).collect(Collectors.joining(", "))
            )
         );
      } else {
         return interfaceLogics.isEmpty() ? Optional.empty() : Optional.of(interfaceLogics.getFirst());
      }
   }

   public Optional<Class<?>> findLogicTypeForPluginConfig(Object config) {
      return this.findLogicTypeForConfigType(config.getClass());
   }

   public Class<?> getLogicTypeForPluginConfig(Object config) {
      Optional<Class<?>> logicType = this.findLogicTypeForPluginConfig(config);
      return logicType.orElseThrow(() -> new FailureException(String.format("No plugin logic found for plugin config type %s", config.getClass().getName())));
   }

   public Set<Module> getModulesForPluginConfig(Object config) {
      Set<Module> modules = new HashSet<>();

      for (Class<?> configType = config.getClass(); configType != null; configType = configType.getSuperclass()) {
         if (this.configToModule.containsKey(configType)) {
            modules.addAll(this.configToModule.get(configType));
         }
      }

      return modules;
   }

   private Map<Class<?>, Class<?>> indexLogicTypes(Collection<Class<?>> logicTypes) {
      Map<Class<?>, Class<?>> index = new HashMap<>();

      for (Class<?> logicType : logicTypes) {
         Class<?>[] configTypes = this.configTypesOfLogic(logicType);

         for (Class<?> configType : configTypes) {
            boolean conflict = index.putIfAbsent(configType, logicType) != null;
            if (conflict) {
               String error = String.format(
                  "There are multiple plugin logics referring to the same plugin config type %s: found %s",
                  configType.getName(),
                  List.of(logicType, index.get(configType))
               );
               LOG.error(LogTypes.CONFIGURATION_ERROR, error);
               throw new FailureException(error);
            }
         }
      }

      return index;
   }

   private Class<?>[] configTypesOfLogic(Class<?> logicType) {
      try {
         return logicType.getDeclaredAnnotation(LogicForConfig.class).value();
      } catch (TypeNotPresentException e) {
         LOG.info(
            EDefaultLogType.GENERIC_LOG_TYPE,
            "Logic type " + logicType.getName() + " could not be indexed, because a dependent class could not be loaded: " + e.getMessage()
         );
         return new Class[0];
      } catch (Exception e) {
         LOG.error(LogTypes.CONFIGURATION_ERROR, String.format("Cannot load configuration type for plugin logic %s: %s", logicType.getName(), e.getMessage()));
         throw e;
      }
   }

   private Map<Class<?>, Set<Module>> indexModuleTypes(Collection<Class<? extends Module>> moduleTypes) {
      Map<Class<?>, Set<Module>> index = new HashMap<>();

      for (Class<?> moduleType : moduleTypes) {
         Class<?> configType = this.configTypeOfModule(moduleType);
         Set<Module> modules = index.getOrDefault(configType, new HashSet<>());
         modules.add(ReflectionHelper.newInstance((Class<Module>)moduleType));
         index.put(configType, modules);
      }

      return index;
   }

   private Class<?> configTypeOfModule(Class<?> moduleType) {
      return moduleType.getDeclaredAnnotation(ModuleForConfig.class).value();
   }
}
