package com.airlock.iam.common.domain.service.cache;

import com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.google.common.annotations.VisibleForTesting;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.annotation.Nonnull;
import java.time.Duration;
import java.util.concurrent.CompletionException;
import java.util.concurrent.Executor;
import java.util.concurrent.ForkJoinPool;
import java.util.function.Supplier;
import org.jspecify.annotations.Nullable;

public final class FailureTolerantExpiringCache<K, V> {
   private final LoadingCache<K, V> cache;

   public FailureTolerantExpiringCache(
      Duration refreshInterval,
      FailureTolerantExpiringCache.ValueLoader<K, V> loader,
      FailureTolerantExpiringCache.RefreshFailureListener<K> refreshFailureListener
   ) {
      this(refreshInterval, loader, refreshFailureListener, ForkJoinPool.commonPool(), System::nanoTime);
   }

   @VisibleForTesting
   FailureTolerantExpiringCache(
      Duration refreshInterval,
      FailureTolerantExpiringCache.ValueLoader<K, V> loader,
      FailureTolerantExpiringCache.RefreshFailureListener<K> refreshFailureListener,
      Executor refreshExecutor,
      Supplier<Long> nanoTicker
   ) {
      this.cache = Caffeine.newBuilder()
         .ticker(nanoTicker::get)
         .executor(refreshExecutor)
         .initialCapacity(256)
         .refreshAfterWrite(refreshInterval)
         .expireAfterWrite(refreshInterval.multipliedBy(2L))
         .build(new FailureTolerantExpiringCache.FailureTolerantCacheLoader(loader, refreshFailureListener));
   }

   public V get(K key) {
      return (V)this.cache.get(key);
   }

   public FailureTolerantExpiringCache.ValueWithRefreshInfo<V> getWithRefreshInfo(K key) {
      V previous = (V)this.cache.policy().getIfPresentQuietly(key);
      V current = (V)this.cache.get(key);
      return new FailureTolerantExpiringCache.ValueWithRefreshInfo<>(current, previous != current);
   }

   public V refresh(K key) {
      try {
         return (V)this.cache.refresh(key).join();
      } catch (CompletionException e) {
         if (e.getCause() instanceof RuntimeException runtimeException) {
            throw runtimeException;
         } else {
            throw e;
         }
      }
   }

   private final class FailureTolerantCacheLoader implements CacheLoader<K, V> {
      private final FailureTolerantExpiringCache.ValueLoader<K, V> loader;
      private final FailureTolerantExpiringCache.RefreshFailureListener<K> refreshFailureListener;

      public V load(@Nonnull K key) {
         return this.loader.load(key);
      }

      public @Nullable V reload(@Nonnull K key, @Nonnull V previousValue) {
         try {
            return this.loader.load(key);
         } catch (RuntimeException e) {
            this.refreshFailureListener.onRefreshFailure(key, e);
            return previousValue;
         }
      }

      @SuppressFBWarnings(justification = "generated code")
      public FailureTolerantCacheLoader(
         final FailureTolerantExpiringCache.ValueLoader<K, V> loader, final FailureTolerantExpiringCache.RefreshFailureListener<K> refreshFailureListener
      ) {
         this.loader = loader;
         this.refreshFailureListener = refreshFailureListener;
      }
   }

   @FunctionalInterface
   public interface RefreshFailureListener<K> {
      void onRefreshFailure(K var1, RuntimeException var2);
   }

   @FunctionalInterface
   public interface ValueLoader<K, V> {
      V load(K var1);
   }

   public record ValueWithRefreshInfo<V>(V value, boolean hasBeenRefreshed) {
   }
}
