package org.infinispan.persistence.remote.upgrade;

import static org.infinispan.persistence.remote.upgrade.HotRodMigratorHelper.MIGRATION_MANAGER_HOT_ROD_KNOWN_KEYS;
import static org.infinispan.persistence.remote.upgrade.HotRodMigratorHelper.awaitTermination;

import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

import org.infinispan.Cache;
import org.infinispan.client.hotrod.MetadataValue;
import org.infinispan.client.hotrod.RemoteCache;
import org.infinispan.commons.CacheException;
import org.infinispan.commons.io.UnsignedNumeric;
import org.infinispan.commons.marshall.AbstractExternalizer;
import org.infinispan.commons.marshall.Marshaller;
import org.infinispan.commons.marshall.WrappedByteArray;
import org.infinispan.commons.util.CloseableIterator;
import org.infinispan.commons.util.Util;
import org.infinispan.commons.util.concurrent.ConcurrentHashSet;
import org.infinispan.container.versioning.NumericVersion;
import org.infinispan.context.Flag;
import org.infinispan.distexec.DistributedCallable;
import org.infinispan.factories.ComponentRegistry;
import org.infinispan.factories.threads.DefaultThreadFactory;
import org.infinispan.metadata.EmbeddedMetadata;
import org.infinispan.metadata.Metadata;
import org.infinispan.metadata.impl.InternalMetadataImpl;
import org.infinispan.notifications.Listener;
import org.infinispan.notifications.cachelistener.annotation.CacheEntryRemoved;
import org.infinispan.notifications.cachelistener.event.CacheEntryRemovedEvent;
import org.infinispan.persistence.manager.PersistenceManager;
import org.infinispan.persistence.remote.RemoteStore;
import org.infinispan.persistence.remote.configuration.RemoteStoreConfiguration;
import org.infinispan.persistence.remote.logging.Log;
import org.infinispan.util.logging.LogFactory;

public class MigrationTask implements DistributedCallable<Object, Object, Integer> {

   private static final Log log = LogFactory.getLog(MigrationTask.class, Log.class);

   private static final String THREAD_NAME = "RollingUpgrade-MigrationTask";

   private final Set<Integer> segments;
   private final int readBatch;
   private final int threads;
   private final ConcurrentHashSet<WrappedByteArray> deletedKeys = new ConcurrentHashSet<>();
   private byte[] ignoredKey;

   private transient Set<RemoteStore> stores;
   private transient Cache<Object, Object> cache;
   private transient ExecutorService executorService;
   private transient final RemoveListener listener = new RemoveListener();

   MigrationTask(Set<Integer> segments, int readBatch, int threads) {
      this.segments = segments;
      this.readBatch = readBatch;
      this.threads = threads;
   }

   @Listener(clustered = true)
   @SuppressWarnings("unused")
   private class RemoveListener {
      @CacheEntryRemoved
      public void entryRemoved(CacheEntryRemovedEvent event) {
         deletedKeys.add(new WrappedByteArray((byte[]) event.getKey()));
      }
   }

   @Override
   public Integer call() throws Exception {
      try {
         Iterator<RemoteStore> storeIterator = stores.iterator();
         if (storeIterator.hasNext()) {
            RemoteStore store = storeIterator.next();
            RemoteCache<Object, Object> storeCache = store.getRemoteCache();
            RemoteStoreConfiguration storeConfig = store.getConfiguration();
            if (!storeConfig.hotRodWrapping()) {
               throw log.remoteStoreNoHotRodWrapping(cache.getName());
            }
            AtomicInteger counter = new AtomicInteger(0);
            migrateEntriesWithMetadata(storeCache, counter);
            awaitTermination(executorService);
            return counter.intValue();
         }
         return null;
      } finally {
         cache.removeListener(listener);
         if (executorService != null) {
            executorService.shutdownNow();
         }
      }
   }

   private void migrateEntriesWithMetadata(RemoteCache<Object, Object> sourceCache, AtomicInteger counter) {
      try (CloseableIterator<Map.Entry<Object, MetadataValue<Object>>> iterator = sourceCache.retrieveEntriesWithMetadata(segments, readBatch)) {
         while (iterator.hasNext() && !Thread.currentThread().isInterrupted()) {
            Map.Entry<Object, MetadataValue<Object>> entry = iterator.next();
            if (!Arrays.equals((byte[]) entry.getKey(), ignoredKey)) {
               MetadataValue<Object> metadataValue = entry.getValue();
               int lifespan = metadataValue.getLifespan();
               int maxIdle = metadataValue.getMaxIdle();
               long version = metadataValue.getVersion();
               long created = metadataValue.getCreated();
               long lastUsed = metadataValue.getLastUsed();
               Metadata metadata = new EmbeddedMetadata.Builder()
                     .version(new NumericVersion(version))
                     .lifespan(lifespan, TimeUnit.SECONDS)
                     .maxIdle(maxIdle, TimeUnit.SECONDS)
                     .build();
               InternalMetadataImpl internalMetadata = new InternalMetadataImpl(metadata, created, lastUsed);
               executorService.submit(() -> {
                  Object key = entry.getKey();
                  if (!deletedKeys.contains(new WrappedByteArray((byte[]) key))) {
                     cache.getAdvancedCache().withFlags(Flag.SKIP_CACHE_LOAD, Flag.ROLLING_UPGRADE).putIfAbsent(entry.getKey(), entry.getValue().getValue(), internalMetadata);
                  }
                  int currentCount = counter.incrementAndGet();
                  if (log.isDebugEnabled() && currentCount % 100 == 0)
                     log.debugf(">>    Migrated %s entries\n", (Integer) currentCount);
               });
            }
         }
      }
   }

   @Override
   public void setEnvironment(Cache<Object, Object> cache, Set<Object> inputKeys) {
      ComponentRegistry cr = cache.getAdvancedCache().getComponentRegistry();
      PersistenceManager loaderManager = cr.getComponent(PersistenceManager.class);
      this.stores = loaderManager.getStores(RemoteStore.class);
      Marshaller marshaller = new MigrationMarshaller();
      this.cache = cache;
      this.cache.addFilteredListener(listener, new RemovedFilter<>(), null, Util.asSet(CacheEntryRemoved.class));
      DefaultThreadFactory threadFactory = new DefaultThreadFactory(null, 1, THREAD_NAME + "-%t", null, null);
      this.executorService = Executors.newFixedThreadPool(threads, threadFactory);
      try {
         this.ignoredKey = marshaller.objectToByteBuffer(MIGRATION_MANAGER_HOT_ROD_KNOWN_KEYS);
      } catch (Exception e) {
         throw new CacheException(e);
      }
   }

   public static class Externalizer extends AbstractExternalizer<MigrationTask> {

      @Override
      public Set<Class<? extends MigrationTask>> getTypeClasses() {
         return Collections.singleton(MigrationTask.class);
      }

      @Override
      public void writeObject(ObjectOutput output, MigrationTask task) throws IOException {
         UnsignedNumeric.writeUnsignedInt(output, task.readBatch);
         UnsignedNumeric.writeUnsignedInt(output, task.threads);
         BitSet bs = new BitSet();
         for (Integer segment : task.segments) {
            bs.set(segment);
         }
         byte[] bytes = bs.toByteArray();
         UnsignedNumeric.writeUnsignedInt(output, bytes.length);
         output.write(bs.toByteArray());
      }

      @Override
      public MigrationTask readObject(ObjectInput input) throws IOException, ClassNotFoundException {
         int readBatch = UnsignedNumeric.readUnsignedInt(input);
         int threads = UnsignedNumeric.readUnsignedInt(input);
         int segmentsSize = UnsignedNumeric.readUnsignedInt(input);
         byte[] bytes = new byte[segmentsSize];
         input.read(bytes);
         BitSet bitSet = BitSet.valueOf(bytes);
         Set<Integer> segments = bitSet.stream().boxed().collect(Collectors.toSet());
         return new MigrationTask(segments, readBatch, threads);
      }
   }

}
