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.common.ws.dataprov.DataProviderException;
import eu.dnetlib.common.ws.dataprov.ResultsResponse;
import eu.dnetlib.data.index.IIndexService;
import eu.dnetlib.data.index.IndexServiceException;
import eu.dnetlib.enabling.is.lookup.rmi.ISLookUpDocumentNotFoundException;
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.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.SolrIndexRmi;
import eu.dnetlib.functionality.index.solr.rmi.SolrIndexRmiEprHandler;
import eu.dnetlib.functionality.index.solr.suggest.Hint;
import eu.dnetlib.functionality.index.solr.suggest.SuggestionApi;
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.collections.Pair;
import eu.dnetlib.miscutils.datetime.HumanTime;
import eu.dnetlib.miscutils.functional.UnaryFunction;

/**
 * 
 * @author claudio
 * 
 */
public class SolrIndexServiceImpl implements IIndexService, SolrIndexRmi, SuggestionApi {

	/**
	 * 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) {
			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 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);
	}
	
	/**
	 * @see eu.dnetlib.functionality.index.solr.suggest.SuggestionApi#suggestAlternateTerm(String, String, String, String, String, String) 
	 */
	@Override
	public Hint suggestAlternateTerm(
			final String dsId,
			final String mdFormat, 
			final String layout, 
			final String interpretation, 
			final String heuristicProfileName, 
			final String term) {
		
		try {
			//TODO implement configuration profile parsing
			final Pair<String, Hint> suggest = solrIndexServer.suggest(QueryLanguage.CQL, term, mdFactory.getMetadata(mdFormat, layout, interpretation), dsId);
			return suggest.getValue();
		
		} catch (IndexServiceException e) {
			log.error(e);
			throw new RuntimeException(e);
		} 
	}	

	@Override
	public W3CEndpointReference indexLookup(final String dsId, final String query, final String mdFormatId, final String layoutId)
			throws IndexServiceException {

		log.debug("got lookup request, index: " + dsId + ", query: " + query);
		try {
			final MetadataReference mdRef = mdFactory.getMetadata(mdFormatId, layoutId);

			final long start = System.currentTimeMillis();			
			final W3CEndpointReference epr = indexResultSetFactory.getLookupResultSet(QueryLanguage.CQL, query, mdRef, parseDsId(dsId));
			
			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 getBrowsingStatistics(final String query, final String dsId, final String mdFormatId, final String layoutId)
			throws IndexServiceException {

		log.debug("got browse request, index: " + dsId + ", query: " + query);
		try {
			final MetadataReference mdRef = mdFactory.getMetadata(mdFormatId, layoutId);
			
			final long start = System.currentTimeMillis();
			final W3CEndpointReference epr = indexResultSetFactory.getBrowsingResultSet(QueryLanguage.CQL, query, mdRef, parseDsId(dsId));
			
			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 String getIndexStatistics(String ixId) throws IndexServiceException {

		try {
			return serviceTools.getLookupLocator().getService().getResourceProfile(ixId);
		} catch (ISLookUpDocumentNotFoundException e) {
			final String errorContent = "Exception occured when geting" + " index statistics from IS. Profile identified by" + ixId
					+ " not found in IS Registry.";
			log.error(errorContent, e);
			throw new IndexServiceException(errorContent, e);
		} catch (ISLookUpException e) {
			throw new IndexServiceException("Exception occured when getting index statistics from IS!" + "; nested error message: " + e.getMessage(), e);
		}
	}

	@Override
	public String[] getListOfIndices() {
		return solrIndexServer.getIndexList();
	}

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

	@Override
	public ResultsResponse getNumberOfResults(final String bdId) throws DataProviderException {
		try {
			return solrIndexServer.getNumberOfRecords(bdId);
		} catch (Exception e) {
			log.error("getNumberOfResults: " + bdId, e);
			throw new RuntimeException(e);
		}
	}

	@Override
	public String identify() {

		return getClass().getName();
	}

	@Override
	public boolean 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);
		return true;
	}

	@Override
	public String[] getSimpleBulkData(String bdId, int fromPosition, int toPosition) throws DataProviderException {
		throw new DataProviderException("Not implemented");
	}

	@Override
	public List<String> getBulkData(String bdId, int fromPosition, int toPosition) throws DataProviderException {
		throw new DataProviderException("Not implemented");
	}
	
	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.isArrayByteBase64(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;
	}

}