package eu.dnetlib.functionality.index.solr;

import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javax.annotation.Resource;
import javax.xml.ws.wsaddressing.W3CEndpointReference;

import org.antlr.stringtemplate.StringTemplate;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.util.StringUtils;

import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.base.Splitter;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;

import eu.dnetlib.data.provision.index.rmi.IndexService;
import eu.dnetlib.data.provision.index.rmi.IndexServiceException;
import eu.dnetlib.enabling.is.lookup.rmi.ISLookUpException;
import eu.dnetlib.enabling.is.registry.ISRegistryDocumentNotFoundException;
import eu.dnetlib.enabling.is.registry.rmi.ISRegistryException;
import eu.dnetlib.enabling.resultset.client.ResultSetClientFactory;
import eu.dnetlib.enabling.tools.AbstractBaseService;
import eu.dnetlib.enabling.tools.blackboard.BlackboardJob;
import eu.dnetlib.functionality.index.solr.actors.BlackboardActorCallback;
import eu.dnetlib.functionality.index.solr.actors.IndexServerActor;
import eu.dnetlib.functionality.index.solr.actors.IndexServerActorFactory;
import eu.dnetlib.functionality.index.solr.actors.ResultsetKeepAliveCallback;
import eu.dnetlib.functionality.index.solr.feed.FeedMode;
import eu.dnetlib.functionality.index.solr.feed.FileType;
import eu.dnetlib.functionality.index.solr.query.IndexQueryFactory;
import eu.dnetlib.functionality.index.solr.query.QueryLanguage;
import eu.dnetlib.functionality.index.solr.resultset.factory.IndexResultSetFactory;
import eu.dnetlib.functionality.index.solr.rmi.SolrIndexRmiEprHandler;
import eu.dnetlib.functionality.index.solr.utils.IndexMap;
import eu.dnetlib.functionality.index.solr.utils.MDFormatReader;
import eu.dnetlib.functionality.index.solr.utils.MetadataReference;
import eu.dnetlib.functionality.index.solr.utils.MetadataReferenceFactory;
import eu.dnetlib.functionality.index.solr.utils.ServiceTools;
import eu.dnetlib.miscutils.collections.MappedCollection;
import eu.dnetlib.miscutils.datetime.HumanTime;
import eu.dnetlib.miscutils.functional.UnaryFunction;

/**
 * Implementation of the IndexService based on Solr.
 * <p>
 * REMARK: we strongly suggest you not to use this service, but rather to use a client for the Solr APIs directly, which performs better and
 * are up-to-date wrt available functionalities.
 * </p>
 * 
 * @author claudio, alessia
 * 
 */

public class SolrIndexServiceImpl extends AbstractBaseService implements IndexService {

	/**
	 * logger.
	 */
	private static final Log log = LogFactory.getLog(SolrIndexServiceImpl.class); // NOPMD

	/**
	 * notification handler.
	 */
	private transient SolrIndexNotificationHandler notificationHandler;

	/**
	 * index ds template.
	 */
	private transient StringTemplate indexDsTemplate;

	/**
	 * IndexResultSetFactory is the main index data provider
	 */
	private transient IndexResultSetFactory indexResultSetFactory;

	private SolrIndexRmiEprHandler rmiEprHandler;

	/**
	 * {@link ResultSetClientFactory}
	 */
	private transient ResultSetClientFactory rsClientFactory;

	/**
	 * {@link SolrIndexServer}
	 */
	private SolrIndexServer solrIndexServer;

	private IndexServerActorFactory actorFactory;

	private transient Map<MetadataReference, IndexServerActor> actorMap;

	/**
	 * {@link ServiceTools}
	 */
	private transient ServiceTools serviceTools;

	private MetadataReferenceFactory mdFactory;

	private int rsClientPageSize;

	@Resource
	private transient MDFormatReader mdFormatReader;

	public void init() {
		actorMap = new ConcurrentHashMap<MetadataReference, IndexServerActor>();
	}

	/**
	 * Create an index and registers its service profile.
	 * 
	 * @param job
	 *            blackboard job
	 * @return String idxId index profile id
	 * @throws IndexServiceException
	 */
	public String createIndex(final BlackboardJob job, final BlackboardActorCallback callback) throws IndexServiceException {

		final MetadataReference mdRef = getMetadataRef(job);

		if ((mdRef.getFormat() == null) || (mdRef.getLayout() == null) || (mdRef.getInterpretation() == null))
			throw new IndexServiceException("some Blackboard parameter is missing in CREATE message");

		final String fields = serviceTools.getIndexFields(mdRef);

		if (fields.isEmpty()) throw new IndexServiceException("No result getting index layout informations");

		final String dataStructure = getDataStructure(mdRef);

		log.info("IndexDataStructure:\n" + dataStructure);

		final String dsId = serviceTools.registerProfile(dataStructure);

		getActor(mdRef).createIndex(dsId, mdRef, fields, callback);

		return dsId;
	}

	/**
	 * Feed index method.
	 * 
	 * @param job
	 * @throws IndexServiceException
	 */
	public void feedIndex(final BlackboardJob job, final ResultsetKeepAliveCallback startCallback, final BlackboardActorCallback endCallback)
			throws IndexServiceException {

		final String dsId = job.getParameters().get(BBParam.INDEX_DS_ID);
		final String localURI = job.getParameters().get(BBParam.LOCAL_URI);
		final FeedMode feedMode = FeedMode.valueOf(job.getParameters().get(BBParam.FEEDING_TYPE));

		String rsEpr = decodeBase64(job.getParameters().get(BBParam.RS_EPR));

		log.debug("\nEPR:\n" + rsEpr + "\n\n");

		if ((dsId == null) || (feedMode == null) || (rsEpr == null)) throw new IndexServiceException("some Blackboard parameter is missing in FEED message");

		// hack used to perform feedings by harvesting local data
		if (localURI != null) {
			rsEpr = indexResultSetFactory.getFileSystemResultSet(FileType.TEXT, localURI).toString();
		}

		log.info("\n\n - FEEDING dsId: " + dsId + " in " + feedMode.toString() + " mode");

		final MetadataReference mdRef = serviceTools.getMetadataRef(dsId);

		getActor(mdRef).feedIndex(dsId, feedMode, rsClientFactory.getClient(rsEpr, getRsClientPageSize()), startCallback, endCallback);
	}

	/**
	 * 
	 * @param query
	 * @param dsId
	 * @param fieldList
	 * @param regex
	 * @param replacement
	 * @return
	 * @throws IndexServiceException
	 */
	public int updateDocuments(final String query, final MetadataReference mdRef, final String fieldList, final String regex, final String replacement)
			throws IndexServiceException {

		final List<String> fields = Lists.newArrayList(Splitter.on(",").trimResults().split(fieldList));

		@SuppressWarnings("unchecked")
		final Predicate<String> predicate = (Predicate<String>) (fields.isEmpty() ? Predicates.alwaysTrue() : Predicates.in(fields));

		final Map<String, String> fieldsMap = Maps.filterKeys(mdFormatReader.getAttributeMap(mdRef, "xpath"), predicate);

		return getActor(mdRef).updateDocuments(query, mdRef, fieldsMap, regex, replacement);
	}

	/**
	 * method deletes documents, according to the specified dataStructure ids and cql query.
	 * 
	 * @param job
	 * @throws IndexServiceException
	 * @throws ISLookUpException
	 * @throws ISRegistryDocumentNotFoundException
	 * @throws ISRegistryException
	 */
	public void deleteIndex(final BlackboardJob job, final BlackboardActorCallback callback) throws IndexServiceException {

		String dsId = job.getParameters().get(BBParam.INDEX_DS_ID);
		String query = job.getParameters().get(BBParam.QUERY);

		if (dsId == null) throw new IndexServiceException("some Blackboard parameter is missing in DELETE message");
		if ((query == null) || (query.length() == 0)) {
			query = "textual";
		}

		if (dsId.equals(IndexMap.INDEX_DSID_ALL)) {
			final MetadataReference mdRef = getMetadataRef(job);
			if ((mdRef.getFormat() == null) || (mdRef.getLayout() == null))
				throw new IndexServiceException("some Blackboard parameter is missing in DELETE message");

			dsId = serviceTools.getIndexDsIdsList(mdRef).get(0);
		}
		log.info("\n\n - DELETE BY QUERY >>>>>>> " + "query: " + query + " dsId: " + dsId);

		getActor(serviceTools.getMetadataRef(dsId)).deleteByQuery(query, dsId, callback);

		if (!serviceTools.deleteIndexDS(dsId)) {
			log.warn("couldn't delete IndexDS: " + dsId);
		}
		log.info("\n\nDELETE report:" + "\n- dsId: " + dsId + "\n- query: " + query);
	}

	public void mergeIndex(final BlackboardJob job, final BlackboardActorCallback callback) throws ISLookUpException, IOException {

		final MetadataReference mdRef = getMetadataRef(job);

		if ((mdRef.getFormat() == null) || (mdRef.getLayout() == null))
			throw new ISLookUpException("some Blackboard parameter is missing in TRANSFORM message");

		getActor(mdRef).mergeIndexes(mdRef,
				MappedCollection.listMap(decodeJsonList(job.getParameters().get(BBParam.INDEX_EPR)), new UnaryFunction<W3CEndpointReference, String>() {

					@Override
					public W3CEndpointReference evaluate(final String epr) {
						return rmiEprHandler.hackEpr(epr);
					}
				}), callback);
	}

	@Override
	public W3CEndpointReference lookup(final String ixId, final String query, final String mdformat, final String layout, final String interpretation)
			throws IndexServiceException {
		log.debug("got lookup request, index: " + ixId + ", query: " + query);
		try {
			final MetadataReference mdRef = mdFactory.getMetadata(mdformat, layout, interpretation);

			final long start = System.currentTimeMillis();
			final W3CEndpointReference epr = indexResultSetFactory.getLookupResultSet(QueryLanguage.CQL, query, mdRef, parseDsId(ixId));

			if (log.isDebugEnabled()) {
				log.debug("indexLookup time: " + HumanTime.exactly((System.currentTimeMillis() - start)));
			}

			return epr;
		} catch (Throwable e) {
			log.error("indexLookup error", e);
			throw new IndexServiceException(e);
		}
	}

	@Override
	public W3CEndpointReference lookup(final String query, final String mdformat, final String layout, final String interpretation)
			throws IndexServiceException {
		return this.lookup("ALL", query, mdformat, layout, interpretation);
	}

	@Override
	public W3CEndpointReference browse(final String ixId, final String query, final String mdFormat, final String layout, final String interpretation)
			throws IndexServiceException {
		log.debug("got browse request, index: " + ixId + ", query: " + query);
		try {
			final MetadataReference mdRef = mdFactory.getMetadata(mdFormat, layout, interpretation);

			final long start = System.currentTimeMillis();
			final W3CEndpointReference epr = indexResultSetFactory.getBrowsingResultSet(QueryLanguage.CQL, query, mdRef, parseDsId(ixId));

			if (log.isDebugEnabled()) {
				log.debug("getBrowsingStatistics time: " + HumanTime.exactly((System.currentTimeMillis() - start)));
			}

			return epr;
		} catch (Throwable e) {
			log.error("getBrowsingStatistics error", e);
			throw new IndexServiceException(e);
		}
	}

	@Override
	public W3CEndpointReference browse(final String query, final String mdformat, final String layout, final String interpretation)
			throws IndexServiceException {
		return this.browse("ALL", query, mdformat, layout, interpretation);
	}

	@Override
	public List<String> getIndexFiles(final String format, final String layout, final String interpretation) {
		final MetadataReference mdRef = mdFactory.getMetadata(format, layout, interpretation);
		return getActor(mdRef).getIndexFiles(mdRef);
	}

	@Override
	public List<String> getListOfIndices() {
		return solrIndexServer.getIndexList();
	}

	@Override
	public String getListOfIndicesCSV() {
		return solrIndexServer.getIndexListCSV();
	}

	@Override
	public void notify(final String subscrId, final String topic, final String isId, final String message) {
		log.info("---- service got notification ----");
		log.info("subscrId: " + subscrId);
		log.info("topic " + topic);
		log.info("isId " + isId);
		log.debug("msg: " + message);
		log.info("____ now processing the notification ____");
		notificationHandler.notified(subscrId, topic, isId, message);
	}

	public IndexServerActor getActor(final MetadataReference mdRef) {
		if (actorMap.get(mdRef) == null) {
			actorMap.put(mdRef, actorFactory.newInstance());
		}

		return actorMap.get(mdRef);
	}

	public IndexServerActor getActor(final String dsId) {
		return getActor(solrIndexServer.getIndexMap().getMdRefById(dsId));
	}

	// ///////////////////////// helpers
	private String[] parseDsId(String dsId) {

		dsId = dsId.replace(" ", "").replace("\t", "").replace("\n", "");
		String[] dsIds = StringUtils.delimitedListToStringArray(dsId, IndexQueryFactory.INDEX_DELIMITER);
		if (dsIds.length == 0) throw new IllegalArgumentException("Invalid ixId parameter: " + dsId);
		if ((dsIds.length == 1) && dsIds[0].toUpperCase().equals(IndexMap.INDEX_DSID_ALL)) {
			dsIds[0] = IndexMap.INDEX_DSID_ALL;
		}

		return dsIds;
	}

	private List<String> decodeJsonList(final String json) {
		return new Gson().fromJson(json, new TypeToken<List<String>>() {}.getType());
	}

	/**
	 * Method decodes a Base64 encoded string.
	 * 
	 * @param encoded
	 *            the given string
	 * @return decoded string
	 */
	private String decodeBase64(final String encoded) {
		if ((encoded != null) && Base64.isBase64(encoded.getBytes())) return new String(Base64.decodeBase64(encoded.getBytes()));
		return encoded;
	}

	/**
	 * Method applies given format, layout and interpretation to the index dataStructure template.
	 * 
	 * @param format
	 * @param layout
	 * @param interp
	 * @return String representation of the indexDataStructure
	 */
	private String getDataStructure(final MetadataReference mdRef) {

		final StringTemplate ds = new StringTemplate(indexDsTemplate.getTemplate());

		ds.setAttribute("serviceUri", serviceTools.getServiceAddress());
		ds.setAttribute("format", mdRef.getFormat());
		ds.setAttribute("layout", mdRef.getLayout());
		ds.setAttribute("interpretation", mdRef.getInterpretation());

		return ds.toString();
	}

	private MetadataReference getMetadataRef(final BlackboardJob job) {
		return mdFactory.getMetadata(job.getParameters().get(BBParam.FORMAT), job.getParameters().get(BBParam.LAYOUT), job.getParameters().get(BBParam.INTERP));
	}

	// //////////////////////////////// setters

	@Required
	public void setNotificationHandler(final SolrIndexNotificationHandler notificationHandler) {
		this.notificationHandler = notificationHandler;
	}

	@Required
	public void setSolrIndexServer(final SolrIndexServer solrIndexServer) {
		this.solrIndexServer = solrIndexServer;
	}

	public SolrIndexServer getSolrIndexServer() {
		return solrIndexServer;
	}

	@Required
	public void setIndexDsTemplate(final StringTemplate indexDsTemplate) {
		this.indexDsTemplate = indexDsTemplate;
	}

	@Required
	public void setRsClientFactory(final ResultSetClientFactory rsClientFactory) {
		this.rsClientFactory = rsClientFactory;
	}

	@Required
	public void setIndexResultSetFactory(final IndexResultSetFactory indexResultSetFactory) {
		this.indexResultSetFactory = indexResultSetFactory;
	}

	@Required
	public void setServiceTools(final ServiceTools serviceTools) {
		this.serviceTools = serviceTools;
	}

	@Required
	public void setMdFactory(final MetadataReferenceFactory mdFactory) {
		this.mdFactory = mdFactory;
	}

	public MetadataReferenceFactory getMdFactory() {
		return mdFactory;
	}

	@Required
	public void setActorFactory(final IndexServerActorFactory actorFactory) {
		this.actorFactory = actorFactory;
	}

	public IndexServerActorFactory getActorFactory() {
		return actorFactory;
	}

	@Required
	public void setRmiEprHandler(final SolrIndexRmiEprHandler rmiEprHandler) {
		this.rmiEprHandler = rmiEprHandler;
	}

	public SolrIndexRmiEprHandler getRmiEprHandler() {
		return rmiEprHandler;
	}

	@Required
	public void setRsClientPageSize(final int rsClientPageSize) {
		this.rsClientPageSize = rsClientPageSize;
	}

	public int getRsClientPageSize() {
		return rsClientPageSize;
	}

}
