package com.airlock.iam.common.infrastructure;

import com.airlock.iam.base.api.domain.model.event.DomainEventSubscriber;
import com.airlock.iam.common.api.infrastructure.ApplicationComponentProvider;
import com.airlock.iam.common.api.infrastructure.ApplicationInitializationHook;
import com.airlock.iam.common.api.infrastructure.mapper.MapperConfiguration;
import com.airlock.iam.core.infrastructure.scanner.ApplicationComponentClasspathScanner;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.google.common.base.Stopwatch;
import com.google.inject.Module;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import io.github.classgraph.ClassInfoList;
import io.github.classgraph.PackageInfo;
import io.github.classgraph.ScanResult;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.ext.Provider;
import java.lang.reflect.Constructor;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import ma.glasnost.orika.MapperFactory;

public final class ClasspathScanningApplicationComponentProvider implements ApplicationComponentProvider {
   private static final ELogger LOG = ELoggerFactory.getLogger(ClasspathScanningApplicationComponentProvider.class);
   private final Collection<Class<?>> jaxRsComponents;
   private final Collection<Class<? extends Module>> guiceModules;
   private final Collection<Class<?>> pluginLogics;
   private final Collection<Class<? extends DomainEventSubscriber>> domainEventSubscribers;
   private final Collection<Consumer<MapperConfiguration>> beanMappers;
   private final Collection<Class<? extends ApplicationInitializationHook>> initializationHooks = Collections.emptyList();

   public ClasspathScanningApplicationComponentProvider(String applicationName, RestApplicationPackages packages) {
      ScanResult result = ApplicationComponentClasspathScanner.getScanResult();
      Stopwatch stopwatch = Stopwatch.createStarted();
      this.jaxRsComponents = classesInPackages(result, packages.jaxRs())
         .flatMap(
            c -> Stream.concat(
               ApplicationComponentClasspathScanner.classesWithAnnotation(c, Provider.class),
               ApplicationComponentClasspathScanner.classesWithAnnotation(c, Path.class)
            )
         )
         .collect(Collectors.toList());
      List<ClassInfoList> dependencyProviderClasses = classesInPackages(result, packages.dependencyProviders()).toList();
      this.guiceModules = dependencyProviderClasses.stream().flatMap(c -> classesImplementingInterface(c, Module.class)).collect(Collectors.toList());
      this.pluginLogics = ApplicationComponentClasspathScanner.getPluginLogics();
      this.domainEventSubscribers = classesInPackages(result, packages.domainEventSubscribers())
         .flatMap(c -> classesImplementingInterface(c, DomainEventSubscriber.class))
         .collect(Collectors.toList());
      this.beanMappers = classesInPackages(result, packages.beanMappers())
         .map(ClassInfoList::loadClasses)
         .flatMap(Collection::stream)
         .filter(ClasspathScanningApplicationComponentProvider::hasValidMapperConstructor)
         .map(ClasspathScanningApplicationComponentProvider::toMapperConfigurationConsumer)
         .collect(Collectors.toList());
      LOG.debug(
         String.format(
            "Initial component configuration of REST application \"%s\" completed (took %s ms) - %s",
            applicationName,
            stopwatch.elapsed(TimeUnit.MILLISECONDS),
            this.stats()
         )
      );
   }

   private String stats() {
      String counts = String.join(
         ", ",
         "jaxRsComponents: " + this.jaxRsComponents.size(),
         "guiceModules: " + this.guiceModules.size(),
         "pluginLogics: " + this.pluginLogics.size(),
         "domainEventSubscribers: " + this.domainEventSubscribers.size(),
         "beanMappers: " + this.beanMappers.size()
      );
      return "[" + counts + "]";
   }

   private static Consumer<MapperConfiguration> toMapperConfigurationConsumer(Class<?> beanMapper) {
      return mapperConfiguration -> {
         try {
            if (acceptsMapperConfiguration(beanMapper)) {
               createBeanMapper(beanMapper, mapperConfiguration);
            } else {
               createBeanMapper(beanMapper, mapperConfiguration.getMapperFactory());
            }
         } catch (ReflectiveOperationException e) {
            throw new IllegalStateException(e);
         }
      };
   }

   private static void createBeanMapper(Class<?> beanMapper, MapperConfiguration mapperConfiguration) throws ReflectiveOperationException {
      Constructor<?> constructor = beanMapper.getDeclaredConstructor(MapperConfiguration.class);
      constructor.setAccessible(true);
      constructor.newInstance(mapperConfiguration);
   }

   private static void createBeanMapper(Class<?> beanMapper, MapperFactory mapperFactory) throws ReflectiveOperationException {
      Constructor<?> constructor = beanMapper.getDeclaredConstructor(MapperFactory.class);
      constructor.setAccessible(true);
      constructor.newInstance(mapperFactory);
   }

   private static Stream<ClassInfoList> classesInPackages(ScanResult result, String... packages) {
      return Stream.of(packages).<PackageInfo>map(result::getPackageInfo).filter(Objects::nonNull).map(PackageInfo::getClassInfoRecursive);
   }

   private static <T> Stream<Class<T>> classesImplementingInterface(ClassInfoList c, Class<T> interfaceType) {
      return c.filter(it -> it.implementsInterface(interfaceType.getName())).loadClasses(interfaceType).stream();
   }

   private static boolean hasValidMapperConstructor(Class<?> type) {
      return acceptsMapperFactory(type) || acceptsMapperConfiguration(type);
   }

   private static boolean acceptsMapperFactory(Class<?> type) {
      return Stream.of(type.getDeclaredConstructors()).anyMatch(ClasspathScanningApplicationComponentProvider::acceptsMapperFactory);
   }

   private static boolean acceptsMapperFactory(Constructor<?> constructor) {
      return isConstructorWithArgumentOfType(constructor, MapperFactory.class);
   }

   private static boolean acceptsMapperConfiguration(Class<?> type) {
      return Stream.of(type.getDeclaredConstructors()).anyMatch(ClasspathScanningApplicationComponentProvider::acceptsMapperConfiguration);
   }

   private static boolean acceptsMapperConfiguration(Constructor<?> constructor) {
      return isConstructorWithArgumentOfType(constructor, MapperConfiguration.class);
   }

   private static boolean isConstructorWithArgumentOfType(Constructor<?> constructor, Class<?> type) {
      return constructor.getParameters().length == 1 && constructor.getParameters()[0].getType().isAssignableFrom(type);
   }

   @SuppressFBWarnings(justification = "generated code")
   @Override
   public Collection<Class<?>> jaxRsComponents() {
      return this.jaxRsComponents;
   }

   @SuppressFBWarnings(justification = "generated code")
   @Override
   public Collection<Class<? extends Module>> guiceModules() {
      return this.guiceModules;
   }

   @SuppressFBWarnings(justification = "generated code")
   @Override
   public Collection<Class<?>> pluginLogics() {
      return this.pluginLogics;
   }

   @SuppressFBWarnings(justification = "generated code")
   @Override
   public Collection<Class<? extends DomainEventSubscriber>> domainEventSubscribers() {
      return this.domainEventSubscribers;
   }

   @SuppressFBWarnings(justification = "generated code")
   @Override
   public Collection<Consumer<MapperConfiguration>> beanMappers() {
      return this.beanMappers;
   }

   @SuppressFBWarnings(justification = "generated code")
   @Override
   public Collection<Class<? extends ApplicationInitializationHook>> initializationHooks() {
      return this.initializationHooks;
   }
}
