package com.romeh.ordermanager.reader.services;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;

import org.apache.ignite.IgniteCache;
import org.apache.ignite.cache.CacheAtomicityMode;
import org.apache.ignite.cache.CacheMode;
import org.apache.ignite.cache.QueryEntity;
import org.apache.ignite.cache.QueryIndex;
import org.apache.ignite.configuration.CacheConfiguration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;

import com.romeh.ordermanager.reader.entities.JournalReadItem;
import com.romeh.ordermanager.reader.streamer.IgniteSinkConstants;
import com.romeh.ordermanager.reader.streamer.IgniteSourceConstants;
import com.romeh.ordermanager.reader.streamer.grid.streamer.IgniteCacheEventStreamerRx;

import akka.actor.AbstractActor;
import akka.actor.ActorSystem;
import akka.actor.Props;
import akka.japi.pf.ReceiveBuilder;
import akka.persistence.ignite.common.entities.JournalStarted;
import akka.persistence.ignite.extension.IgniteExtension;
import akka.persistence.ignite.extension.IgniteExtensionProvider;

/**
 * the read side service to show the case how you can stream stored write event store to a read side store using Rx java and ignite streamer APIs
 *
 * @author romeh
 */
@Component
public class ReadStoreStreamerService implements ApplicationListener<ContextRefreshedEvent> {

	private static final String JOURNAL_CACHE = "akka-journal";
	private static final String READ_CACHE = "Read-Store";
	private final IgniteExtension igniteExtension;
	private final ActorSystem actorSystem;
	private final IgniteCache<String, JournalReadItem> readStore;

	@Autowired
	public ReadStoreStreamerService(ActorSystem actorSystem) {
		this.actorSystem = actorSystem;
		this.igniteExtension = IgniteExtensionProvider.EXTENSION.get(actorSystem);
		// make sure the read store cache is created
		//usually you should not need that as the writer and reader should be different nodes
		this.readStore = getOrCreateReadStoreCache();
	}


	/**
	 * @param orderId order id to get the status for
	 * @return the order status if any
	 */
	public JournalReadItem getOrderStatus(String orderId) throws OrderNotFoundException {
		return Optional.ofNullable(readStore.get(orderId))
				.orElseThrow(() -> new OrderNotFoundException("order is not found in the read store"));
	}

	/**
	 * @param contextStartedEvent the spring context started event
	 *                            start the events streamer once the spring context init is finished
	 */
	@Override
	public void onApplicationEvent(ContextRefreshedEvent contextStartedEvent) {
		actorSystem.actorOf(Props.create(IgniteStreamerStarter.class, IgniteStreamerStarter::new), "IgniteStreamerActor");

	}

	/**
	 * start the event streamer from the journal write event store to the read side cache store which store only query intersted data
	 */
	private void startIgniteStreamer() {
		// streamer parameters
		Map<String, String> sourceMap = new HashMap<>();
		sourceMap.put(IgniteSourceConstants.CACHE_NAME, JOURNAL_CACHE);
		Map<String, String> sinkMap = new HashMap<>();
		sinkMap.put(IgniteSinkConstants.CACHE_NAME, READ_CACHE);
		sinkMap.put(IgniteSinkConstants.CACHE_ALLOW_OVERWRITE, "true");
		sinkMap.put(IgniteSinkConstants.SINGLE_TUPLE_EXTRACTOR_CLASS, ReadStoreExtractor.class.getName());
		// start the streamer
		final IgniteCacheEventStreamerRx journalStreamer = IgniteCacheEventStreamerRx.builderWithContinuousQuery()
				.pollingInterval(500)
				.flushBufferSize(5)
				.retryTimesOnError(2)
				.sourceCache(sourceMap, igniteExtension.getIgnite())
				.sinkCache(sinkMap, igniteExtension.getIgnite())
				.build();

		journalStreamer.execute();
	}

	/**
	 * check if the read side cache is already created or create it if missing
	 * usually you should not need that as the writer and reader should be different nodes
	 */
	private IgniteCache<String, JournalReadItem> getOrCreateReadStoreCache() {
		IgniteCache<String, JournalReadItem> cache = igniteExtension.getIgnite().cache(READ_CACHE);

		if (null == cache) {
			CacheConfiguration<String, JournalReadItem> readItemCacheConfiguration = new CacheConfiguration<>();
			readItemCacheConfiguration.setBackups(1);
			readItemCacheConfiguration.setName(READ_CACHE);
			readItemCacheConfiguration.setAtomicityMode(CacheAtomicityMode.ATOMIC);
			readItemCacheConfiguration.setCacheMode(CacheMode.PARTITIONED);
			readItemCacheConfiguration.setQueryEntities(Collections.singletonList(createJournalBinaryQueryEntity()));
			cache = igniteExtension.getIgnite().getOrCreateCache(readItemCacheConfiguration);
		}
		return cache;
	}

	/**
	 * @return QueryEntity which the binary query definition of the binary object stored into the read side cache
	 */
	private QueryEntity createJournalBinaryQueryEntity() {
		QueryEntity queryEntity = new QueryEntity();
		queryEntity.setValueType(JournalReadItem.class.getName());
		queryEntity.setKeyType(String.class.getName());
		LinkedHashMap<String, String> fields = new LinkedHashMap<>();
		fields.put(JournalReadItem.ORDER_ID, String.class.getName());
		fields.put(JournalReadItem.STATUS_FIELD, String.class.getName());
		queryEntity.setFields(fields);
		queryEntity.setIndexes(Arrays.asList(new QueryIndex(JournalReadItem.ORDER_ID),
				new QueryIndex(JournalReadItem.STATUS_FIELD)));
		return queryEntity;

	}

	/**
	 * simple akka actor to listen to the journal started event so it can start the event store streamer once the persistence store is running
	 * usually you should not need that as the writer and reader should be different nodes but as here for the sake of the example we have reader and writer
	 * running in the same server ignite node
	 */
	private final class IgniteStreamerStarter extends AbstractActor {

		@Override
		public void preStart() {
			getContext().getSystem().eventStream().subscribe(getSelf(), JournalStarted.class);
		}

		@Override
		public Receive createReceive() {
			// start the streamer once it receive the journal started event
			return ReceiveBuilder.create().match(JournalStarted.class, journalStarted -> startIgniteStreamer()).build();
		}
	}
}