package com.shopizer.search.services.impl;

import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.annotation.Resource;

import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentBuilder;

import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.shopizer.search.services.IndexKeywordRequest;
import com.shopizer.search.services.SearchRequest;
import com.shopizer.search.services.SearchResponse;
import com.shopizer.search.services.field.BooleanField;
import com.shopizer.search.services.field.DateField;
import com.shopizer.search.services.field.DoubleField;
import com.shopizer.search.services.field.Field;
import com.shopizer.search.services.field.IntegerField;
import com.shopizer.search.services.field.ListField;
import com.shopizer.search.services.field.LongField;
import com.shopizer.search.services.field.StringField;
import com.shopizer.search.utils.SearchClient;

import io.searchbox.action.Action;
import io.searchbox.client.JestClient;
import io.searchbox.client.JestResult;
import io.searchbox.core.Bulk;
import io.searchbox.core.Delete;
import io.searchbox.core.Get;
import io.searchbox.core.Index;
import io.searchbox.core.Search;
import io.searchbox.core.SearchResult;
import io.searchbox.core.search.aggregation.TermsAggregation;
import io.searchbox.indices.CreateIndex;
import io.searchbox.indices.IndicesExists;
import io.searchbox.indices.mapping.PutMapping;
import io.searchbox.params.Parameters;

/**
 * https://www.found.no/foundation/java-clients-for-elasticsearch/
 * https://github.com/searchbox-io/Jest/blob/master/jest/README.md
 * HTTP Proxy https://www.elastic.co/blog/playing-http-tricks-nginx
 * Backup - restore http://tech.domain.com.au/2014/12/elasticsearch-snapshots-backup-restore-s3/
 * @author carlsamson
 *
 */
public class SearchDelegateImpl implements SearchDelegate {

	private SearchClient searchClient = null;
	public SearchClient getSearchClient() {
		return searchClient;
	}

	public void setSearchClient(SearchClient searchClient) {
		this.searchClient = searchClient;
	}
	
	//@Inject
	@Resource(name="facetTermsList")
	List<String> facetTermsList;

	private static Logger log = Logger.getLogger(SearchDelegateImpl.class);
	
	@SuppressWarnings("unchecked")
	private Object readNode(JsonElement jsonElement) throws Exception {
		 
		Object container = null;
		if (jsonElement.isJsonObject()) {
	            Set<java.util.Map.Entry<String, JsonElement>> ens = ((JsonObject) jsonElement).entrySet();
	            if (ens != null) {
	                // Iterate JSON Elements with Key values
	                for (java.util.Map.Entry<String, JsonElement> en : ens) {
	                    //System.out.println(en.getKey() + " : ");
	                	if(container==null) {
	                		container = new HashMap<String, Object>();
	                	}
	                	((Map)container).put(en.getKey(), readNode(en.getValue()));
	                }
	            }
	        } 
	        
	        // Check whether jsonElement is Arrary or not
	        else if (jsonElement.isJsonArray()) {
	                    JsonArray jarr = jsonElement.getAsJsonArray();
	                    // Iterate JSON Array to JSON Elements
	                    container = new ArrayList<Object>();
	                    for (JsonElement je : jarr) {
	                        ((List)container).add(readNode(je));
	                    }
	        }
	        
	        // Check whether jsonElement is NULL or not
	        else if (jsonElement.isJsonNull()) {
	            // print null
	        	
	        } 
	        // Check whether jsonElement is Primitive or not
	        else if (jsonElement.isJsonPrimitive()) {
	            // print value as String
	        	container = jsonElement.getAsString();
	        } 
	        
	
		return container;
	}
	
	private Map<String, Object> getResults(JsonObject jsonObject, String path) throws Exception {
		
		Map<String, Object> fields = new HashMap<String, Object>();
		JsonElement jsonElement = jsonObject.get(path);
		Set<java.util.Map.Entry<String, JsonElement>> ens = ((JsonObject) jsonElement).entrySet();
        if (ens != null) {
            // Iterate JSON Elements with Key values
            for (java.util.Map.Entry<String, JsonElement> en : ens) {
                fields.put(en.getKey(),readNode(en.getValue()));
            }
        }
        
        return fields;
		
		
	}
	

	
	/* (non-Javadoc)
	 * @see com.shopizer.search.services.impl.SearchService#indexExist(java.lang.String)
	 */
	@Override
	public boolean indexExist(String indexName) throws Exception {
		JestClient client = searchClient.getClient();
		

		IndicesExists.Builder builder = new IndicesExists.Builder(indexName);
		
		
		if(!StringUtils.isBlank(searchClient.getAuthenticationHeader())) {
			builder.setHeader("Authorization", searchClient.getAuthenticationHeader());
		}
		
		@SuppressWarnings("rawtypes")
		Action action = builder.build();
		
		@SuppressWarnings("unchecked")
		JestResult result = client.execute(action);

		return result.isSucceeded();

	}
	
	
	//https://github.com/searchbox-io/Jest/blob/master/jest/src/test/java/io/searchbox/indices/CreateIndexIntegrationTest.java
	/* (non-Javadoc)
	 * @see com.shopizer.search.services.impl.SearchService#createIndice(java.lang.String, java.lang.String, java.lang.String)
	 */
	@Override
	public void createIndice(String mapping, String settings,String index,String object) throws Exception {
		
	
		
		//Client client = searchClient.getClient();
		JestClient client = searchClient.getClient();
		
		
		CreateIndex.Builder createIndex = new CreateIndex.Builder(index);

		//set settings to the index
		if(settings!=null) {
			createIndex.settings(
			Settings.settingsBuilder()
			.loadFromSource(settings)
			.build()
			.getAsMap());
		}
		
		client.execute(createIndex.build());
		
		PutMapping.Builder putMapping = new PutMapping.Builder(
				index,
				object,
				mapping
		);
		
		
		JestResult result = client.execute(putMapping.build());
		
		if(result!=null && !StringUtils.isBlank(result.getErrorMessage())) {
			log.error("An error occured while creating an index " + result.getErrorMessage());
		}

		
	}
	
	
    /* (non-Javadoc)
	 * @see com.shopizer.search.services.impl.SearchService#index(java.lang.String, java.lang.String, java.lang.String, java.lang.String)
	 */
	@Override
	public void index(String json, String collection, String object, String id) throws Exception {
		
		JestClient client = searchClient.getClient();

        
        Index index = new Index.Builder(json).index(collection).type(object).id(id).build();
        JestResult result = client.execute(index);
        
		if(result!=null && !StringUtils.isBlank(result.getErrorMessage())) {
			log.error("An error occured while indexing a document " + json + " " + result.getErrorMessage());
		}
		
	}
	
	/* (non-Javadoc)
	 * @see com.shopizer.search.services.impl.SearchService#delete(java.lang.String, java.lang.String, java.lang.String)
	 */
	@Override
	public void delete(String collection, String object, String id) throws Exception {
		
		if(this.indexExist(collection)) {
			
			JestClient client = searchClient.getClient();

			Delete builder = new Delete.Builder(id)
            .index(collection)
            .type(object)
            .build();
			
			JestResult result = client.execute(builder);
			
			if(result!=null && !result.isSucceeded() && !StringUtils.isBlank(result.getErrorMessage())) {
				log.error("Cannot delete from " + collection + " with id " + id);
			}

		}
	}
	
	/* (non-Javadoc)
	 * @see com.shopizer.search.services.impl.SearchService#bulkDeleteIndex(java.util.Collection, java.lang.String)
	 */
	@Override
	public void bulkDeleteIndex(Collection<String> ids, String collection, String object) throws Exception {
		

		if(this.indexExist(collection)) {
			
			JestClient client = searchClient.getClient();

			if(ids!=null && ids.size()>0) {
				
				Bulk.Builder bulk = new Bulk.Builder()
			    .defaultIndex(collection);
				
				for(String s : ids) {

					bulk.defaultType(object)
					.addAction(new Delete.Builder(s).index(collection).type(object).build());

					
				}
				
				JestResult results = client.execute(bulk.build());
				
				if(!results.isSucceeded()) {
					log.error("ES response has failures " + results.getErrorMessage());
				}

				
			}
		}
		

	}
	

	
	/* (non-Javadoc)
	 * @see com.shopizer.search.services.impl.SearchService#bulkIndexKeywords(java.util.Collection, java.lang.String, java.lang.String)
	 */
	@Override
	public void bulkIndexKeywords(Collection<IndexKeywordRequest> bulks, String collection, String object) throws Exception {

			JestClient client = searchClient.getClient();

			Bulk.Builder bulk = new Bulk.Builder()
		    .defaultIndex(collection)
		    .defaultType(object);
			
			//@todo, index in appropriate Locale
			for(IndexKeywordRequest key : bulks) {

				String id = key.getKey();
				if(id.length()>25) {
					id = id.substring(0,25);
				}
				id = id.trim().toLowerCase();
				
				XContentBuilder b = jsonBuilder() 
                .startObject() 
                	.field("id", id)//index name is the value trimed and lower cased
                    .field("keyword", key.getKey())
                    .field("_id_", key.getId());
                    
                    @SuppressWarnings("rawtypes")
					Collection fields = key.getFilters();
                    if(fields.size()>0) {
                    	
                    	for(Object o : fields) {
                    		
                    		if(o instanceof BooleanField) {
                    			
                    			Boolean val = ((BooleanField)o).getValue();
                    			b.field(((Field)o).getName(), val.booleanValue());
                    			
                    		} else if(o instanceof IntegerField) {
                    			
                    			Integer val = ((IntegerField)o).getValue();
                    			b.field(((Field)o).getName(), val.intValue());
                    			
                    			
                    		} else if(o instanceof LongField) {
                    			
                    			Long val = ((LongField)o).getValue();
                    			b.field(((Field)o).getName(), val.longValue());
                    			
                    			
                    		} else if(o instanceof ListField) {
                    			
                    			@SuppressWarnings("rawtypes")
								List val = ((ListField)o).getValue();
                    			b.field(((Field)o).getName(), val);
                    			
                    		} else if(o instanceof DoubleField) {
                    			
                    			Double val = ((DoubleField)o).getValue();
                    			b.field(((Field)o).getName(), val.doubleValue());
                    			
                    		} else if(o instanceof DateField) {
                    			
                    			Date val = ((DateField)o).getValue();
                    			b.field(((Field)o).getName(), val);
                    			
                    		} else {
                    			
                    			String val = ((StringField)o).getValue();
                    			b.field(((Field)o).getName(), val);
                    			
                    		}
                    	}
                    }
                    
                b.endObject();

                bulk.addAction(new Index.Builder(b.string()).build());
				//bulkRequest.add(client.prepareIndex(collection, object).setSource(b));
			}
			 
			log.debug("Adding to collection " + collection);
			         
			JestResult results = client.execute(bulk.build());
			
			if(!results.isSucceeded()) {
				log.error("ES response has failures " + results.getErrorMessage());
			}


		
	}
	

	/* (non-Javadoc)
	 * @see com.shopizer.search.services.impl.SearchService#getObject(java.lang.String, java.lang.String, java.lang.String)
	 */
	@Override
	public com.shopizer.search.services.GetResponse getObject(String collection, String object, String id) throws Exception {
		
		JestClient client = searchClient.getClient();
		
		Get get = new Get.Builder(collection, id).type(object).build();
		
		//System.out.println(get.getURI());

		JestResult result = client.execute(get);
		
		
		
		/**
		 * This method throws an exception
		 */
		//GetResponse response = searchClient.getClient().prepareGet(collection, object, id)
		//.setOperationThreaded(true) 
		//.setFields("_source")
        //.execute() 
        //.actionGet();
				
		com.shopizer.search.services.GetResponse response = null;
		if(result!=null && StringUtils.isBlank(result.getErrorMessage())) {
			
				
				JsonObject jsonObject = result.getJsonObject();
				
				Map<String, Object> fields = this.getResults(jsonObject, "_source");

				response = new com.shopizer.search.services.GetResponse(fields);
				response.setObjectJson(result.getJsonString());
				
		} else {
				log.error("Error wile performing a get method " + result.getErrorMessage());
		}

		return response;
		
	}
	
	/* (non-Javadoc)
	 * @see com.shopizer.search.services.impl.SearchService#search(com.shopizer.search.services.SearchRequest)
	 */
	@SuppressWarnings("unchecked")
	@Override
	public SearchResponse search(SearchRequest request) throws Exception {

		   
		
		    JestClient client = searchClient.getClient();
		
		    SearchResponse response = new SearchResponse();

			Search.Builder search = new Search.Builder(request.getJson());
                // multiple index or types can be added.
            for(String index : request.getCollections()) {
            	search.addIndex(index);
            }


			
			//org.elasticsearch.action.search.SearchRequestBuilder builder = searchClient.getClient().prepareSearch(request.getCollection())
	        
					
			//.setQuery("{ \"term\" : { \"keyword\" : \"dynamic\" }}")
			//.setQuery(request.getJson())
			//.addHighlightedField("description")
			//.addHighlightedField("tags")
			//with extra you can set everything
			//.setExtraSource(request.getJson())
	        //.setExplain(false);
			
			
			if(request.getSize()>-1) {
				search.setParameter(Parameters.SIZE, request.getSize());
			}
	        

			SearchResult result = client.execute(search.build());
			
			if(result==null) {
				throw new Exception("Search result returned a null object");
			}
			
			if(!result.isSucceeded() && !StringUtils.isBlank(result.getErrorMessage())) {
				log.error("An error occured while searching " + result.getErrorMessage());
				return response;
			}
	
	        //org.elasticsearch.action.search.SearchResponse rsp = builder.execute().actionGet();
	        //SearchHit[] docs = rsp.getHits().getHits();
			List<com.shopizer.search.services.SearchHit> hits = new ArrayList<com.shopizer.search.services.SearchHit>();
	        @SuppressWarnings("rawtypes")
			List ids = new ArrayList();
	        
	        JsonObject responseHitsObject = result.getJsonObject().getAsJsonObject("hits");
	        Long count = responseHitsObject.get("total").getAsLong();
	        
	        JsonArray responseHits = responseHitsObject.getAsJsonArray("hits");
	        
	        //JsonArray responseHits = result.getJsonObject().getAsJsonObject("hits").getAsJsonArray("hits");
	        //response.setCount(responseHits.size());
	        
	        response.setCount(count);
	        
	        
	        //for (SearchHit sd : docs) {
	        Iterator<JsonElement> elementsIterator = responseHits.iterator();
	        while (elementsIterator.hasNext()) {
	        	JsonElement element = elementsIterator.next();
	        	
	        	JsonObject jsonObject = element.getAsJsonObject();
	        	
	        	String type = jsonObject.get("_type").getAsString();

	        	Map<String,Object> item = this.getResults(jsonObject, "_source");

	        	
	        	JsonElement _idElement = jsonObject.get("_id");
	        	String _id = _idElement.getAsString();


	            //System.out.println(sd.getScore());
	        	com.shopizer.search.services.SearchHit hit = new com.shopizer.search.services.SearchHit(item, _id);
	        	//com.shopizer.search.services.SearchHit hit = new com.shopizer.search.services.SearchHit(element.getAsJsonArray());
	        	
	        	hit.setType(type);
	        	hits.add(hit);
	        	
	        	
	        	ids.add(item.get("id"));

	        	//Map source = sd.getSource();
	        	//Map highlights = sd.getHighlightFields();
	        	//hits.add(sd);
	          
	          
	        }
	        
	        response.setIds(ids);
	        response.setSearchHits(hits);
	        
	        /**
	        we need to know what will be the term aggregations decided on the client
	        so we can build a dynamic response based on what we want to retrieve
	        **/
	        
	        //for each configured term aggregation
	        Map<String,List<com.shopizer.search.services.FacetEntry>> facetsMap = new HashMap<String,List<com.shopizer.search.services.FacetEntry>>();
	        for(String facetString : facetTermsList) {
	        	TermsAggregation  aggregations = result.getAggregations().getTermsAggregation(facetString);
	        
	        	if(aggregations!=null) {
	        	
		        	String name = aggregations.getName();
		        	List<com.shopizer.search.services.FacetEntry> entries = new ArrayList<com.shopizer.search.services.FacetEntry>();
		        	for(int i = 0; i < aggregations.getBuckets().size(); i++) {
		        		io.searchbox.core.search.aggregation.TermsAggregation.Entry entry = aggregations.getBuckets().get(i);
		        		com.shopizer.search.services.FacetEntry f = new com.shopizer.search.services.FacetEntry();
		        		String facetKey = entry.getKey();
		        		Long numberOfHits = entry.getCount();
		        		f.setName(facetKey);
		        		f.setCount(numberOfHits);
		        		entries.add(f);
		        	}
		        	facetsMap.put(name, entries);
	        	
	        	}

	        }
	        
	        response.setFacets(facetsMap);
	        
	        //START FACETS

/*	        List<TermsFacet> termsFacets = result.getFacets(TermsFacet.class);
	        //TermsAggregation terms = result.getAggregations().getTermsAggregation("terms1");
	        
	        
	        //Facets facets = rsp.getFacets();
	        if(termsFacets!=null) {
	        	Map<String,com.shopizer.search.services.Facet> facetsMap = new HashMap<String,com.shopizer.search.services.Facet>();
	        	for (TermsFacet facet : termsFacets) {

	        	     //if (facet instanceof TermsFacet) {
	        	         //TermsFacet ff = (TermsFacet) facet;
	        	         com.shopizer.search.services.Facet f = new com.shopizer.search.services.Facet();
	        	         f.setName(facet.getName());
	        	         List<com.shopizer.search.services.Entry> entries = new ArrayList<com.shopizer.search.services.Entry>();
	        	         for(Term o : facet.terms()) {
	        	        	 com.shopizer.search.services.Entry entry = new com.shopizer.search.services.Entry();
	        	        	 entry.setName(o.getName());
	        	        	 entry.setCount(o.getCount());
	        	        	 entries.add(entry);
	        	         }
	        	         f.setEntries(entries);
	        	         facetsMap.put(facet.getName(), f);
	        	}
	        	response.setFacets(facetsMap);
	        }*/
	        
	        //END FACETS
	        
	        response.setSearchHits(hits);
	        return response;

		
	}

	/* (non-Javadoc)
	 * @see com.shopizer.search.services.impl.SearchService#searchAutocomplete(java.lang.String, java.lang.String, int)
	 */
	
	@Override
	public Set<String> searchAutocomplete(String collection,String json,int size) throws Exception {

		    JestClient client = searchClient.getClient();

			Search.Builder search = new Search.Builder(json);
			search.addIndex(collection);

			SearchResult result = client.execute(search.build());
			
			if(result==null) {
				throw new Exception("Search result is null");
			}
			
			if(!result.isSucceeded() && !StringUtils.isBlank(result.getErrorMessage())) {
				log.error("An error occured while searching " + result.getErrorMessage());
				return null;
			}
			
	        JsonArray responseHits = result.getJsonObject().getAsJsonObject("hits").getAsJsonArray("hits");
	        
	        Set<String> keywords = new HashSet<String>();
	        
	        
	        //for (SearchHit sd : docs) {
	        Iterator<JsonElement> elementsIterator = responseHits.iterator();
	        while (elementsIterator.hasNext()) {
	        	JsonElement element = elementsIterator.next();
	        	
	        	JsonObject jsonObject = element.getAsJsonObject();

	        	Map<String,Object> item = this.getResults(jsonObject, "_source");
	        	
	        	keywords.add((String)item.get("keyword"));
	        	
	        	
	        }

	
	        //org.elasticsearch.action.search.SearchResponse rsp = builder.execute().actionGet();
	        //SearchHit[] docs = rsp.getHits().getHits();
	        //for (SearchHit sd : docs) {
	          //to get explanation you'll need to enable this when querying:
	          //System.out.println(sd.getExplanation().toString());
	
	          // if we use in mapping: "_source" : {"enabled" : false}
	          // we need to include all necessary fields in query and then to use doc.getFields()
	          // instead of doc.getSource()
	        	//Map source = sd.getSource();
	        	//System.out.println(sd.getType());
	        	//System.out.println(sd.getExplanation().toString());
	        	//System.out.println(sd.fields().toString());
	        	//System.out.println(sd.getMatchedFilters().length);
	        	//SearchHitField f = sd.field("keyword");
	        	//String f = (String)source.get("keyword");
	            //System.out.println(sd.sourceAsString());
	            //System.out.println(sd.getScore());
	        	
	        	//returnList.add(sd.sourceAsString());
	        	//returnList.add(f.toLowerCase());
	        //}
        

		
		return keywords;
		
	}

}
