package eu.dnetlib.functionality.index.solr;

import java.io.IOException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrInputDocument;
import org.dom4j.DocumentException;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.util.StringUtils;
import org.xml.sax.SAXException;
import org.z3950.zing.cql.CQLParseException;

import eu.dnetlib.common.ws.dataprov.ResultsResponse;
import eu.dnetlib.enabling.is.lookup.rmi.ISLookUpDocumentNotFoundException;
import eu.dnetlib.enabling.is.lookup.rmi.ISLookUpException;
import eu.dnetlib.enabling.resultset.client.IterableResultSetClient;
import eu.dnetlib.functionality.index.cql.CqlTranslator;
import eu.dnetlib.functionality.index.solr.feed.FeedResult;
import eu.dnetlib.functionality.index.solr.feed.IndexDocument;
import eu.dnetlib.functionality.index.solr.feed.InputDocumentFactory;
import eu.dnetlib.functionality.index.solr.query.IndexQuery;
import eu.dnetlib.functionality.index.solr.query.IndexQueryFactory;
import eu.dnetlib.functionality.index.solr.query.QueryResponseFactory;
import eu.dnetlib.functionality.index.solr.query.QueryResponseParser;
import eu.dnetlib.functionality.index.solr.utils.IndexMap;
import eu.dnetlib.functionality.index.solr.utils.IndexSchemaFactory;
import eu.dnetlib.functionality.index.solr.utils.MetadataReference;
import eu.dnetlib.functionality.index.solr.utils.ServiceTools;

/**
 * 
 * @author claudio
 *
 */
public class SolrIndexServer {
	
	/**
	 * logger
	 */
	private static final Log log = LogFactory.getLog(SolrIndexServer.class);
	
	/**
	 * physical index server handler
	 */
	private LocalServer localServer;
	
	/**
	 * query factory
	 */
	private IndexQueryFactory indexQueryFactory;
	
	/**
	 * document factory used for the feed process.
	 */
	private InputDocumentFactory documentFactory;
	
	/**
	 * response factory. 
	 */
	private QueryResponseFactory queryResponseFactory;
	
	/**
	 * input buffer for index feeding
	 */
	private Map<String, Collection<SolrInputDocument>> mapInputBuffer;
	
	/**
	 * contais handled index data structures.
	 */
	private IndexMap indexMap;
	
	private ServiceTools serviceTools;
	
	private CqlTranslator translator;
	
	/**
	 * defines how many documents will be added to the index at once
	 */
	private final static int INPUT_THRESHOLD = 200;
	
	/**
	 * initializer
	 * 
	 * @throws SolrException could happen 
	 * @throws IOException 
	 * @throws SAXException 
	 * @throws ParserConfigurationException 
	 * @throws TransformerException 
	 * @throws DocumentException 
	 * @throws DocumentException 
	 * @throws ISLookUpException 
	 * @throws ISLookUpDocumentNotFoundException 
	 * @throws SolrServerException 
	 */
	public void init() 
		throws IOException, ParserConfigurationException, SAXException, DocumentException, TransformerException, ISLookUpDocumentNotFoundException, ISLookUpException, SolrServerException {
	
		log.info("Initializing SolrIndexServer");
		
		mapInputBuffer = new ConcurrentHashMap<String, Collection<SolrInputDocument>>();
		
		final List<String> indexNames = localServer.resumeIndexes();
		
		for (String indexName : indexNames ) {

			final MetadataReference mdRef = indexMap.getMetadataReference(indexName);

			final String[] dsId = serviceTools.getIndexDsIdsArray(mdRef);
			
			if (dsId != null) {
			
				indexMap.register(mdRef, dsId);
				
				mapInputBuffer.put(indexName, new ArrayList<SolrInputDocument>());
				
				log.info("loaded " + indexMap.getDsId(indexName).size() + " dataStructure references for index " + indexName);
			} else {
				log.warn("couldn't find any referenced dataStructure lo load for index: " + indexName);
			}
		}
		
		log.info("resume report: " + indexNames.size() + " indexes resumed: " + indexNames.toString());
	}
	
	/**
	 *  creates an index
	 * 
	 * @param idxId
	 * @param format
	 * @param layout
	 * @param interpretation
	 * @param fields
	 * @throws ISLookUpDocumentNotFoundException
	 * @throws ISLookUpException
	 * @throws IOException
	 * @throws DocumentException
	 * @throws TransformerException
	 */
	public void create(
			final String dsId,
			final MetadataReference mdRef,
			final String fields) 
		throws ISLookUpDocumentNotFoundException, ISLookUpException, IOException, 
			DocumentException, TransformerException {

		log.info("registering DSId: " + dsId);
		
		final String indexName = indexMap.register(fields, mdRef, dsId);

		mapInputBuffer.put(indexName, new ArrayList<SolrInputDocument>());
		
		log.info("indexSet size: " + indexMap.getIndexSize() + ", handled DS: " + indexMap.getDsSize());		
	}
	
	/**
	 * feeds the documents provided by the {@link IterableResultSetClient} to the index specified by the dsId
	 * 
	 * @param dsId
	 * @param version
	 * @param documentIterator
	 * @return
	 * @throws SolrServerException
	 * @throws IOException
	 */
	public FeedResult feed(final String dsId, final String version, final IterableResultSetClient documentIterator) 
		throws SolrServerException, IOException {
		
		FeedResult res = new FeedResult(System.currentTimeMillis());
		
		for (String doc : documentIterator) {
			
			final IndexDocument document = documentFactory.getInputDocument(dsId, version, doc);
			parseDocumentStatus(document, dsId, res);
		}
		return res.setTimeElapsed(System.currentTimeMillis());
	}
	
	/**
	 * TODO feed documents w/o needing to retrieve the original document from the index: simply adding 
	 * 		the new field
	 * 
	 * @param dsId
	 * @param documentIterator
	 * @return
	 * @throws SolrServerException
	 * @throws IOException
	 * @throws CQLParseException 
	 */
	public FeedResult feedFulltext(final String dsId, final IterableResultSetClient documentIterator) 
		throws SolrServerException, IOException, CQLParseException {
		
		FeedResult res = new FeedResult(System.currentTimeMillis());
				
		for (String fullTextMetadata : documentIterator) {

			final IndexDocument document = documentFactory.getFullTextDocument(dsId, fullTextMetadata);
			parseDocumentStatus(document, dsId, res);
		}
		return res.setTimeElapsed(System.currentTimeMillis());
	}
	
	/**
	 * helper method, parses document's {@link IndexDocument.STATUS} to updates the FeedResult,
	 * if the STATUS is OK, feeds the document.
	 * 
	 * @param document
	 * 			document whose status must be parsed
	 * @param dsId
	 * 			index dataStructure Id needed to perform feeding
	 * @param res
	 * 			FeedResult to update
	 * @throws SolrServerException
	 * @throws IOException
	 */
	private void parseDocumentStatus(final IndexDocument document, final String dsId, FeedResult res) 
		throws SolrServerException, IOException {
		
		switch (document.getStatus()) {

		case OK:
			performFeed(dsId, document);
			res.add();
			break;
	
		case MARKED:
			res.mark();
			break;
			
		case ERROR:
			res.skip();
			break;					
			
		default:
			throw new IllegalStateException("unknow document status");
		}
	}
	
	/**
	 * helper method, actually performs documents feeding by flushing the {@link #mapInputBuffer} to the {@link #localServer}
	 * 
	 * @param dsId
	 * 			index DataStructure Id
	 * @param document
	 * 			document to feed
	 * @throws SolrServerException
	 * @throws IOException
	 */
	private void performFeed(final String dsId, final SolrInputDocument document) 
		throws SolrServerException, IOException {
		
		final String indexName = indexMap.getIndexName(dsId);
		
		mapInputBuffer.get(indexName).add(document);
		
		if (mapInputBuffer.get(indexName).size() >= INPUT_THRESHOLD ) {
			localServer.add(mapInputBuffer.get(indexName), indexName);
			mapInputBuffer.get(indexName).clear();
		}
	}
	
	/**
	 * method deletes all the documents of a specified dsId whose {@link IndexMap}.DS_VERSION 
	 * field is older than the specified mdFormatVersion. 
	 * 
	 * @param dsId
	 * @param mdFormatVersion
	 * @return
	 * 		the time elapsed to complete the operation.
	 * @throws SolrServerException
	 * @throws IOException
	 * @throws CQLParseException
	 * @throws ParseException
	 */
	public long deleteByVersion(final String dsId, final String mdFormatVersion) 
		throws SolrServerException, IOException, CQLParseException, ParseException {

		final String query = IndexMap.DS_VERSION + " < \"" + 
							 documentFactory.getParsedDateField(mdFormatVersion) + "\" and " +
							 IndexMap.DS_ID + ":" + dsId;

		return localServer.deleteByQuery(translator.toLucene(query), indexMap.getIndexName(dsId)).getElapsedTime();
	}
	
	/**
	 * method delete documents where IndexMap.DELETE_DOCUMENT field is true 
	 * 
	 * @param dsId
	 * @return
	 * 		the time elapsed to complete the operation.
	 * @throws SolrServerException
	 * @throws IOException
	 * @throws CQLParseException
	 */
	public long cleanMarkedDocuments(final String dsId) 
		throws SolrServerException, IOException, CQLParseException {
	
		final String query = IndexMap.DELETE_DOCUMENT + ":true and " + IndexMap.DS_ID + ":" + dsId;

		return localServer.deleteByQuery(translator.toLucene(query), indexMap.getIndexName(dsId)).getElapsedTime();
	}

	/**
	 * method deletes all the documents that matches the specified cql 
	 * query from the index that handles the given dsId.
	 * 
	 * @param query
	 * @param dsId
	 * @return
	 * @throws CQLParseException
	 * @throws IOException
	 * @throws SolrServerException
	 */
	public long deleteByQuery(final String query, final String dsId) 
		throws CQLParseException, IOException, SolrServerException {

		return localServer.deleteByQuery(translator.toLucene(query), indexMap.getIndexName(dsId)).getElapsedTime();
	}
	
	/**
	 * method queries the index to get the number of indexed documents
	 * of a specific dataStructure.
	 * 
	 * @param dsId
	 * @return
	 * 		the time elapsed to complete the operation.
	 * @throws CQLParseException
	 * @throws IOException
	 * @throws SolrServerException
	 * @throws DocumentException
	 */
	public ResultsResponse getNumberOfRecords(final String dsId) 
		throws CQLParseException, IOException, SolrServerException, DocumentException {

		final MetadataReference mdRef = indexMap.dsMetadataReference(dsId);
		final IndexQuery query = indexQueryFactory.getIndexQuery("textual", mdRef, dsId);
		final QueryResponseParser lookupRes = this.lookup(query, mdRef);
		 
		return newResultsResponse(lookupRes.getStatus(), lookupRes.getNumFound());
	}
	
	/**
	 * method performs a commit of the physical index delegated to handle the given dsId
	 * 
	 * @param dsId
	 * 			needed to retrieve the physical index.
	 * @return
	 * 			the time (ms) elapsed to complete the operation.
	 * @throws SolrServerException
	 * @throws IOException
	 */
	public long commit(final String dsId) 
		throws SolrServerException, IOException {
		
		final String indexName = indexMap.getIndexName(dsId);
		
		//flushes the buffer to add the last documents...
		final Collection<SolrInputDocument> docs = mapInputBuffer.get(indexName);
		if (docs != null && docs.size() > 0)
			localServer.add(docs, indexName);			
		
		//..and commits the server 
		return localServer.commit(indexName).getElapsedTime();
	}

	/**
	 * method performs an optimization of the physical index delegated to handle the given dsId
	 * 
	 * @param dsId
	 * 			needed to retrieve the physical index.
	 * @return
	 * 			the time (ms) elapsed to complete the operation.
	 * @throws SolrServerException
	 * @throws IOException
	 */
	public long optimize(final String dsId) 
		throws SolrServerException, IOException {
		
		final String indexName = indexMap.getIndexName(dsId);
		
		return localServer.optimize(indexName).getElapsedTime();
	}
	
	/**
	 * method returns list of registered Store Data Structure identifiers.
	 * 
	 * @return a list of available indexes
	 */
	public String[] getIndexList() {

		return indexMap.getDsIdArray();
	}
	
	/**
	 * method returns list of registered Store Data Structure identifiers as comma-separated string.
	 * @return comma separated list index identifiers
	 */
	public String getIndexListCSV() {
	
		return StringUtils.arrayToCommaDelimitedString(getIndexList());
	}
	
	/**
	 * 
	 * @param idxId
	 * @param query
	 * @param fromPosition
	 * @param toPosition
	 * @return List<String> of matching records
	 * @throws SolrServerException
	 * @throws CQLParseException
	 * @throws IOException
	 * @throws DocumentException
	 */
	public QueryResponseParser lookup(final IndexQuery query, final MetadataReference mdRef) 
		throws SolrServerException, CQLParseException, IOException, DocumentException {
				
		return queryResponseFactory.getQueryResponseParser(localServer.query(query), mdRef);
	}
	
	/////////////////////////// helper
	
	private ResultsResponse newResultsResponse(final String status, final long total) {
		ResultsResponse res = new ResultsResponse();
		res.setStatus(status);
		res.setTotal((int)total);
		return res;
	}
	
	/////////////////////////// setters and getters
	
	public IndexSchemaFactory getSchemaFactory() {
		return localServer.getSchemaFactory();
	}	
	
	public IndexQueryFactory getQueryFactory() {
		return indexQueryFactory;
	}
	
	@Required
	public void setQueryFactory(IndexQueryFactory indexQueryFactory) {
		this.indexQueryFactory = indexQueryFactory;
	}
	
	@Required
	public void setDocumentFactory(InputDocumentFactory documentFactory) {
		this.documentFactory = documentFactory;
	}
	
	@Required
	public void setLocalServer(LocalServer localServer) {
		this.localServer = localServer;
	}

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

	@Required
	public void setIndexMap(IndexMap indexMap) {
		this.indexMap = indexMap;
	}

	@Required
	public void setTranslator(CqlTranslator translator) {
		this.translator = translator;
	}
}
