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

import com.airlock.iam.base.api.domain.model.exception.FailureException;
import com.airlock.iam.base.infrastructure.factory.guice.GuiceInjectorFactory;
import com.airlock.iam.common.infrastructure.factory.PluginConfigToLogicInlineModule;
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.plugin.api.event.ConfigurationActivationNotifier;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.inject.Injector;
import com.google.inject.Module;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.inject.Inject;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import lombok.NonNull;

public final class PluginLogicFinder {
   private static final ELogger LOG = ELoggerFactory.getLogger(PluginLogicFinder.class);
   private static final Cache<PluginLogicFinder.CacheKey, Injector> CACHE = createInjectorCache();
   private final PluginRepository repository;

   @Inject
   public PluginLogicFinder(PluginRepository repository) {
      this.repository = repository;
   }

   public <T, C> T findPluginLogic(@NonNull C config, @NonNull Injector parentInjector) {
      if (config == null) {
         throw new NullPointerException("config is marked non-null but is null");
      }

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

      Class<T> logicType = (Class<T>)this.repository.getLogicTypeForPluginConfig(config);
      PluginLogicFinder.CacheKey key = createKey(config, parentInjector);

      try {
         Injector cachedInjector = (Injector)CACHE.get(key, () -> this.createInjector(config, logicType, this.repository, parentInjector));
         return (T)cachedInjector.getInstance(logicType);
      } catch (Exception e) {
         LOG.error(
            EDefaultLogType.GENERIC_LOG_TYPE,
            "Exception while trying to get injector from cache. Config type: "
               + config.getClass().getName()
               + ", logic type: "
               + logicType.getName()
               + ", parent injector: "
               + parentInjector
         );
         throw new FailureException(e.getCause());
      }
   }

   private <T, C> Injector createInjector(C config, Class<T> logicType, PluginRepository repository, Injector parentInjector) {
      Instant start = Instant.now();
      List<Module> modules = new ArrayList<>(repository.getModulesForPluginConfig(config));
      modules.add(new PluginConfigToLogicInlineModule(config, logicType, parentInjector, this));
      Injector injector = GuiceInjectorFactory.guiceInjectorFactory(parentInjector).createInjector(modules);
      LOG.debug(
         "Created new injector for config type "
            + config.getClass().getSimpleName()
            + " and logic type "
            + logicType.getSimpleName()
            + " in "
            + Duration.between(start, Instant.now()).toMillis()
            + "ms"
      );
      return injector;
   }

   private static Cache<PluginLogicFinder.CacheKey, Injector> createInjectorCache() {
      CacheBuilder<Object, Object> cacheBuilder = CacheBuilder.newBuilder().expireAfterAccess(Duration.ofMinutes(10L));
      if (LOG.isDebugEnabled()) {
         cacheBuilder.recordStats();
      }

      ConfigurationActivationNotifier.registerCallback(PluginLogicFinder::clearCache);
      return cacheBuilder.build();
   }

   private static void clearCache() {
      if (LOG.isDebugEnabled()) {
         LOG.debug("Clearing plugin logic injector cache. Stats: " + CACHE.stats());
      }

      CACHE.invalidateAll();
      CACHE.cleanUp();
   }

   @VisibleForTesting
   static PluginLogicFinder.CacheKey createKey(Object config, Injector parentInjector) {
      return new PluginLogicFinder.CacheKey(config.getClass().getName(), System.identityHashCode(config), System.identityHashCode(parentInjector));
   }

   @VisibleForTesting
   record CacheKey(@NonNull String configClassName, int configObjectId, int parentInjectorObjectId) {
      @SuppressFBWarnings(justification = "generated code")
      public CacheKey {
         if (configClassName == null) {
            throw new NullPointerException("configClassName is marked non-null but is null");
         }
      }
   }
}
