package eu.dnetlib.functionality.index.solr;

import java.io.IOException;
import java.text.ParseException;
import java.util.List;

import javax.xml.transform.TransformerException;
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.apache.solr.client.solrj.SolrServerException;
import org.dom4j.DocumentException;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.util.StringUtils;
import org.z3950.zing.cql.CQLParseException;

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.rmi.ISRegistryException;
import eu.dnetlib.enabling.resultset.client.ResultSetClientFactory;
import eu.dnetlib.enabling.tools.blackboard.BlackboardJob;
import eu.dnetlib.functionality.index.solr.feed.FeedResult;
import eu.dnetlib.functionality.index.solr.feed.FileType;
import eu.dnetlib.functionality.index.solr.query.IndexQueryFactory;
import eu.dnetlib.functionality.index.solr.resultset.factory.IndexResultSetFactory;
import eu.dnetlib.functionality.index.solr.utils.IndexMap;
import eu.dnetlib.functionality.index.solr.utils.MetadataReference;
import eu.dnetlib.functionality.index.solr.utils.ServiceTools;
import eu.dnetlib.miscutils.datetime.DateUtils;

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

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

	/**
	 * Allowed feed mode.
	 * 
	 * @author claudio
	 * 
	 */
	public enum FeedMode {
		REFRESH, INCREMENTAL, FULLTEXT
	};

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

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

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

	/**
	 * {@link ResultSetClientFactory}
	 */
	private ResultSetClientFactory resultSetClientFactory;

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

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

	/**
	 * Default interpretatation, to be used for the lookup method which doesn't have an interpretation parameter.
	 */
	private String defaultInterpretation = MetadataReference.INTERPRETATION;

	/**
	 * Create an index and registers its service profile.
	 * 
	 * @param job
	 *            blackboard job
	 * @return String idxId index profile id
	 * @throws DocumentException
	 * @throws ISLookUpException
	 * @throws ISLookUpDocumentNotFoundException
	 * @throws ISRegistryException
	 * @throws TransformerException
	 * @throws IOException
	 * @throws IndexServiceException
	 */
	public String createIndex(final BlackboardJob job) throws ISLookUpDocumentNotFoundException, ISLookUpException, DocumentException,
			ISRegistryException, IOException, TransformerException {

		final String format = job.getParameters().get("format");
		final String layout = job.getParameters().get("layout");
		final String interpretation = job.getParameters().get("interpretation");

		if (format == null || layout == null)
			throw new ISLookUpException("some Blackboard parameter is missing in CREATE message");

		final MetadataReference mdRef = new MetadataReference(format, layout, interpretation);

		final String fields = serviceTools.getIndexFields(mdRef);

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

		final String dataStructure = getDataStructure(mdRef);

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

		final String dsId = serviceTools.registerProfile(dataStructure);

		solrIndexServer.create(dsId, mdRef, fields);
		serviceTools.incrementHandledDataStructures();
		serviceTools.refreshServiceLastUpdate();

		return dsId;
	}

	/**
	 * 
	 * @param job
	 * @throws ISLookUpException
	 * @throws ISLookUpDocumentNotFoundException
	 * @throws IOException
	 * @throws SolrServerException
	 * @throws ISRegistryException
	 * @throws CQLParseException
	 * @throws ParseException
	 * @throws DocumentException
	 */
	public void feedIndex(final BlackboardJob job) throws ISLookUpDocumentNotFoundException, ISLookUpException, SolrServerException, IOException,
			ISRegistryException, CQLParseException, ParseException, DocumentException {

		final String dsId = job.getParameters().get("id");
		final String localURI = job.getParameters().get("local_URI");
		final FeedMode feedMode = FeedMode.valueOf(job.getParameters().get("feeding_type"));

		String rsEpr = decodeBase64(job.getParameters().get("resultset_epr"));

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

		if (localURI != null) {
			log.info("overriding resultset_epr parameter with local_URI");
			rsEpr = indexResultSetFactory.getFileSystemResultSet(FileType.TEXT, localURI).toString();
		}

		if (feedMode.equals(FeedMode.FULLTEXT)) {
			log.info("\n\n - FEEDING FULLTEXT >>>>>>>>>> dsId: " + dsId);

			final FeedResult feedResult = solrIndexServer.feedFulltext(dsId, resultSetClientFactory.getClient(rsEpr));
			final long commitTime = solrIndexServer.commit(dsId);

			log.info("\n\nFEED report:" + "\n- dsId: " + dsId + "\n- commit time: " + commitTime + "\n- feed result: " + feedResult.toString());
		} else {
			final String mdFormatVersion = DateUtils.now_ISO8601();
			log.info("\n\n - FEED >>>>>>>>>> dsId: " + dsId + " @ " + mdFormatVersion + " in " + feedMode.toString() + " mode");

			final FeedResult feedResult = solrIndexServer.feed(dsId, mdFormatVersion, resultSetClientFactory.getClient(rsEpr));
			final long cleaningTime = solrIndexServer.cleanMarkedDocuments(dsId);

			if (feedMode.equals(FeedMode.REFRESH)) {
				log.info("DELETE OLD DOCUMENTS process completed in " + solrIndexServer.deleteByVersion(dsId, mdFormatVersion));

				// otherwise, solr automatically overwrites records with the same objIdentifier: INCREMENTAL mode.
			}

			final long commitTime = solrIndexServer.commit(dsId);
			final long newIndexSize = solrIndexServer.getNumberOfRecords(dsId).getTotal();

			if (!serviceTools.updateIndexDS(newIndexSize, dsId))
				log.warn("couldn't update IndexDs with Id: " + dsId);

			log.info(
					"\n\nFEED report:" +
					"\n- feed mode: " + feedMode +
					"\n- dsId: " + dsId + 
					"\n- metaDataVersion: " + mdFormatVersion + 
					"\n- cleaning time: " + cleaningTime + 
					"\n- commit time: " + commitTime + 
					"\n- feed result: " + feedResult.toString() + 
					"\n- new index size: " + newIndexSize);
		}
	}

	/**
	 * method deletes documents, according to the specified dataStructure ids and cql query.
	 * 
	 * @param job
	 * @throws IndexServiceException
	 * @throws ISLookUpException
	 * @throws CQLParseException
	 * @throws IOException
	 * @throws SolrServerException
	 * @throws DocumentException
	 * @throws ISRegistryException
	 */
	public void deleteByQuery(final BlackboardJob job) throws IndexServiceException, ISLookUpException, CQLParseException, IOException,
			SolrServerException, DocumentException, ISRegistryException {

		final String dsId = job.getParameters().get("id");
		final String query = job.getParameters().get("query");

		if (dsId == null || query == null)
			throw new ISLookUpException("some Blackboard parameter is missing in DELETE message");

		String[] dsIdArray = parseDsId(dsId);

		if (dsIdArray[0].equals(IndexMap.INDEX_DSID_ALL)) {

			final String format = job.getParameters().get("format");
			final String layout = job.getParameters().get("layout");
			final String interp = job.getParameters().get("interpretation");

			final MetadataReference mdRef = new MetadataReference(format, layout, interp);

			if (format == null || layout == null)
				throw new ISLookUpException("some Blackboard parameter is missing in DELETE message");

			dsIdArray = serviceTools.getIndexDsIdsArray(mdRef);
		}

		for (String id : dsIdArray) {

			log.info("\n\n - DELETE BY QUERY >>>>>>> " + "query: " + query + " dsId: " + id);

			final long oldIndexSize = solrIndexServer.getNumberOfRecords(id).getTotal();
			final long deleteTime = solrIndexServer.deleteByQuery(query, id);
			final long commitTime = solrIndexServer.commit(id);
			final long newIndexSize = solrIndexServer.getNumberOfRecords(id).getTotal();

			if (!serviceTools.updateIndexDS(newIndexSize, id))
				log.warn("couldn't update IndexDs with Id: " + id);

			log.info("\n\nDELETE report:" + "\n- dsId: " + id + "\n- query: " + query + "\n- delete time: " + deleteTime + "\n- commit time: "
					+ commitTime + "\n- documents removed: " + (oldIndexSize - newIndexSize) + "\n- new index size: " + newIndexSize);
		}
	}

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

		log.info("got lookup request, index: " + dsId + ", query: " + query);

		try {

			final MetadataReference mdRef = new MetadataReference(mdFormatId, layoutId, defaultInterpretation);

			return indexResultSetFactory.getLookupResultSet(query, mdRef, parseDsId(dsId));
		} 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.info("got browse request, index: " + dsId + ", query: " + query);
		try {
			final MetadataReference mdRef = new MetadataReference(mdFormatId, layoutId, defaultInterpretation);

			return indexResultSetFactory.getBrowsingResultSet(query, mdRef, parseDsId(dsId));
		} 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) {
			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(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(String subscrId, String topic, String isId, String message) {
		log.info("---- service got notification ----");
		log.info("subscrId: " + subscrId);
		log.info("topic " + topic);
		log.info("isId " + isId);
		//log.info("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");
	}

	/////////////////////////// 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;
	}

	/**
	 * Method decodes a Base64 encoded string.
	 * 
	 * @param s
	 *            the given string
	 * @return decoded string
	 */
	private String decodeBase64(final String s) {

		if (s != null && Base64.isArrayByteBase64(s.getBytes()))
			return new String(Base64.decodeBase64(s.getBytes()));
		return s;
	}

	/**
	 * 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();
	}

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

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

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

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

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

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

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

	public String getDefaultInterpretation() {
		return defaultInterpretation;
	}

	@Required
	public void setDefaultInterpretation(String defaultInterpretation) {
		this.defaultInterpretation = defaultInterpretation;
	}

}
