package eu.dnetlib.functionality.index.solr;

import java.util.List;
import java.util.Map;

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.collect.Maps;

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.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.suggest.Hint;
import eu.dnetlib.functionality.index.solr.suggest.SuggestionApi;
import eu.dnetlib.functionality.index.solr.utils.IndexField;
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.Pair;
import eu.dnetlib.miscutils.datetime.HumanTime;

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

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

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

	/**
	 * {@link eu.dnetlib.functionality.index.solr.SolrIndexServer}
	 */
	@Resource
	private SolrIndexServer solrIndexServer;

	/**
	 * {@link eu.dnetlib.functionality.index.solr.actors.IndexServerActorFactory}
	 */
	@Resource
	private IndexServerActorFactory indexActorFactory;

	/**
	 * {@link eu.dnetlib.enabling.resultset.client.ResultSetClientFactory}
	 */
	@Resource
	private transient ResultSetClientFactory resultSetClientFactory;

	/**
	 * {@link eu.dnetlib.functionality.index.solr.utils.ServiceTools}
	 */
	@Resource
	private transient ServiceTools serviceTools;

	/**
	 * {@link eu.dnetlib.functionality.index.solr.utils.MetadataReferenceFactory}
	 */
	@Resource
	private MetadataReferenceFactory mdFactory;

	/**
	 * {@link eu.dnetlib.functionality.index.solr.utils.MDFormatReader}
	 */
	@Resource
	private transient MDFormatReader mdFormatReader;

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

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

	/**
	 * Map of Index actors
	 */
	private transient Map<MetadataReference, IndexServerActor> actorMap = Maps.newConcurrentMap();

	/**
	 * ResultSetClient page size.
	 */
	private int rsClientPageSize;

	/**
	 * 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);
		final String dsId = serviceTools.registerProfile(dataStructure);
		log.info("registered DsId: " + dsId + "\n" + 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 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");
		}

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

		final MetadataReference mdRef = serviceTools.getMetadataRef(dsId);

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

	/**
	 * 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 = "*=*";
		}

		if (dsId.equals(IndexField.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);

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

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

	/**
	 * @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() {
		try {
			return solrIndexServer.getIndexList();
		} catch (IndexServiceException e) {
			log.error("error executing getListOfIndices", e);
			throw new RuntimeException(e);
		}
	}

	@Override
	public String getListOfIndicesCSV() {
		try {
			return solrIndexServer.getIndexListCSV();
		} catch (IndexServiceException e) {
			log.error("error executing getListOfIndicesCSV", e);
			throw new RuntimeException(e);
		}
	}

	@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, indexActorFactory.newInstance());
		}

		return actorMap.get(mdRef);
	}

	public IndexServerActor getActor(final String dsId) {
		try {
			return getActor(serviceTools.getMetadataRef(dsId));
		} catch (IndexServiceException e) {
			return null;
		}
	}

	/////////////////////////// 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(IndexField.INDEX_DSID_ALL)) {
			dsIds[0] = IndexField.INDEX_DSID_ALL;
		}

		return dsIds;
	}

	/**
	 * Method decodes a Base64 encoded string.
	 * 
	 * @param encoded
	 *            the given string
	 * @return decoded string
	 */
	private String decodeBase64(final String encoded) {
		if (encoded != null) {
			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 setIndexDsTemplate(final StringTemplate indexDsTemplate) {
		this.indexDsTemplate = indexDsTemplate;
	}

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

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

	public int getRsClientPageSize() {
		return rsClientPageSize;
	}

}