package com.trulia.thoth.shrinker;

import com.trulia.thoth.pojo.ServerDetail;
import com.trulia.thoth.requestdocuments.MessageRequestDocument;
import com.trulia.thoth.util.Utils;
import org.apache.log4j.Logger;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.impl.HttpSolrServer;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.client.solrj.util.ClientUtils;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrInputDocument;
import org.joda.time.DateTime;

import java.io.IOException;
import java.util.concurrent.Callable;

/**
 * User: dbraga - Date: 11/7/14
 */
public class DocumentShrinker implements Callable{
  private static final Logger LOG = Logger.getLogger(DocumentShrinker.class);

  private HttpSolrServer shrankServer;
  private HttpSolrServer realTimeServer;
  private DateTime nowMinusTimeToShrink;
  private ServerDetail serverDetail;
  private static final String MASTER_MINUTES_DOCUMENT = "masterDocumentMin_b";
  public static final String SLOW_QUERY_DOCUMENT = "slowQueryDocument_b";
  private static final String MASTER_DOCUMENT_TIMESTAMP = "masterTime_dt";
  private static final String NAME_RANGE_0_10 = "range-0-10_i";
  private static final String NAME_RANGE_10_100 = "range-10-100_i";
  private static final String NAME_RANGE_100_1000 = "range-100-1000_i";
  private static final String NAME_RANGE_1000_OVER = "range-1000-OVER_i";
  private static final String NAME_TOT_COUNT = "tot-count_i";
  private static final String ZERO_HITS_QUERIES_COUNT = "zeroHits-count_i";
  private static final String EXCEPTION_QUERIES_COUNT = "exceptionCount_i";
  private static final String RANGE_QUERIES_COUNT = "RangeQueryCount_i";
  private static final String FACET_QUERIES_COUNT = "FacetQueryCount_i";
  private static final String PROPERTY_LOOKUP_QUERIES_COUNT = "PropertyLookupQueryCount_i";
  private static final String PROPERTY_HASH_LOOKUP_QUERIES_COUNT = "PropertyHashLookupQueryCount_i";
  private static final String GEOSPATIAL_QUERIES_COUNT = "GeospatialQueryCount_i";
  private static final String OPENHOMES_QURIES_COUNT = "OpenHomesQueryCount_i";
  private static final String AVG_QTIME = "avg_qtime_d";
  private static final String AVG_REQUESTS_IN_PROGRESS = "avg_requestsInProgress_d";

  private static final String VALUE_RANGE_0_10 = "0 TO 10";
  private static final String VALUE_RANGE_10_100 = "11 TO 100";
  private static final String VALUE_RANGE_100_1000 = "101 TO 1000";
  private static final String VALUE_RANGE_1000_OVER = "1001 TO *";
  private static final String VALUE_TOT_COUNT = "* TO *";

  private static final String BITMASK_CONTAINS_RANGE_QUERY ="1??????";
  private static final String BITMASK_CONTAINS_FACET_QUERY ="?1?????";
  private static final String BITMASK_CONTAINS_PROPERTY_LOOKUP_QUERY ="??1????";
  private static final String BITMASK_CONTAINS_PROPERTY_HASH_LOOKUP_QUERY ="???1???";
  private static final String BITMASK_CONTAINS_GEOSPATIAL_QUERY ="?????1?";
  private static final String BITMASK_CONTAINS_OPEN_HOMES_QUERY ="??????1";

  public static final String ID = "id";
  public static final String EXCEPTION = "exception_b";
  public static final String QTIME = "qtime_i";
  public static final String HITS = "hits_i";
  public static final String BITMASK = "bitmask_s";
  public static final String REQUESTS_IN_PROGRESS = "requestInProgress_i";

  private int MAX_NUMBER_SLOW_THOTH_DOCS = 10;

  public DocumentShrinker(ServerDetail serverDetail, DateTime nowMinusTimeToShrink, HttpSolrServer thothServer, HttpSolrServer thothShrankServer) {
    this.shrankServer = thothShrankServer;
    this.realTimeServer = thothServer;
    this.nowMinusTimeToShrink = nowMinusTimeToShrink;
    this.serverDetail = serverDetail;

  }

  /**
   * 1) Find thoth documents in the real time core
   * 2) Get information about those docs, like facets and stats
   * 3) Create Shrank summary document from them, and add the document to the shrank core
   * 4) Tag top slow thoth documents and add them to the shrank core
   * 5) Clean up real time core
   */

  @Override
  public Object call() throws Exception {
    try {
      QueryResponse thothDocumentsFacets = fetchFacets();
      if (isThereAnythingToShrink(thothDocumentsFacets)){
        // Something to shrink
        LOG.info("Found documents to shrink for server " + serverDetail.getName() + "("+serverDetail.getCore()+"):" + serverDetail.getPort());
        // Fetch some stats about the thoth documents
        QueryResponse thothDocumentsStats = fetchStats();
        // Prepare the shrank document
        SolrInputDocument shrankDocument = generateShrankDocument(thothDocumentsFacets, thothDocumentsStats);
        // Add the document to the index
        shrankServer.add(shrankDocument);
        // Tag slower request documents and add them to the shrank core
        tagAndAddSlowThothDocuments();
        // Clean real time core
        realTimeServer.deleteByQuery(getRealTimeCoreCleanUpQuery());
      } else {
        LOG.info("Nothing to shrink for server " + serverDetail.getName() +":"+serverDetail.getPort()+" core:"+serverDetail.getCore()+" ...");
      }
      LOG.info( serverDetail.getName() +":"+serverDetail.getPort()+" core:"+serverDetail.getCore()+ " shrinking completed.");
    } catch (SolrServerException e) {
      LOG.error(e);
    } catch (Exception e) {
      LOG.error(e);
    }
    finally {
      return "Completed";
    }

  }




  /**
   * Define facet query to fetch facet counts from Thoth docs
   * @return SolrQuery
   */
  private SolrQuery defineFacetQuery(){
    return new SolrQuery().
        setFacet(true).
        // Get counts on
        addFacetQuery(Utils.createRangeQuery(QTIME,VALUE_RANGE_0_10)).  // Qtime between 0 and 10 ms
        addFacetQuery(Utils.createRangeQuery(QTIME,VALUE_RANGE_10_100)).  // Qtime between 10 and 100 ms
        addFacetQuery(Utils.createRangeQuery(QTIME,VALUE_RANGE_100_1000)). // Qtime between 100 and 1000 ms
        addFacetQuery(Utils.createRangeQuery(QTIME,VALUE_RANGE_1000_OVER)). // Qtime over 1000
        addFacetQuery(Utils.createRangeQuery(QTIME,VALUE_TOT_COUNT)). // All Qtimes
        addFacetQuery(Utils.createFieldValueQuery(HITS, "0")). // 0 hits queries
        addFacetQuery(Utils.createFieldValueQuery("NOT " + HITS, "0")). // queries with hits > 0
        addFacetQuery(Utils.createFieldValueQuery(EXCEPTION, "true")). // exception documents
        addFacetQuery(Utils.createFieldValueQuery(BITMASK, BITMASK_CONTAINS_RANGE_QUERY)).   // range queries
        addFacetQuery(Utils.createFieldValueQuery(BITMASK, BITMASK_CONTAINS_FACET_QUERY)). // facet queries
        addFacetQuery(Utils.createFieldValueQuery(BITMASK, BITMASK_CONTAINS_PROPERTY_LOOKUP_QUERY)). // lookup queries
        addFacetQuery(Utils.createFieldValueQuery(BITMASK, BITMASK_CONTAINS_PROPERTY_HASH_LOOKUP_QUERY)).  // hash lookup queries
        addFacetQuery(Utils.createFieldValueQuery(BITMASK, BITMASK_CONTAINS_GEOSPATIAL_QUERY)). // geospatial queries
        addFacetQuery(Utils.createFieldValueQuery(BITMASK, BITMASK_CONTAINS_OPEN_HOMES_QUERY)). // open homes queries
        // Set normal query
        setQuery(createThothDocsAggregationQuery()).
        setRows(0);

  }

  private SolrQuery defineStatsQuery(){
    SolrQuery solrQuery = new SolrQuery().
        setQuery(createThothDocsAggregationQuery()).
        setRows(0);
    solrQuery.setGetFieldStatistics(QTIME);
    solrQuery.setGetFieldStatistics(REQUESTS_IN_PROGRESS);
    return solrQuery;
  }

  /**
   * Check if a field is present
   * @param field to check
   * @return true if is present
   */
  public boolean isFieldPresent(Object field){
    return (field != null);
  }


  /**
   * Generate query for to aggregate thoth docs
   * @return solr query
   */
  private String createThothDocsAggregationQuery(){ // look for every document that has
    return
        Utils.createRangeQuery(MessageRequestDocument.TIMESTAMP, Utils.dateTimeToZuluSolrFormat(nowMinusTimeToShrink) + " TO *") +   // timestamp between the interval provided (past) and now
        Utils.createFieldValueQuery(MessageRequestDocument.HOSTNAME,"\"" + serverDetail.getName() + "\"" ) +  // with the provided hostname
        " AND NOT " + Utils.createFieldValueQuery(MASTER_MINUTES_DOCUMENT ,"true ") + // that has not already shrank
        " AND NOT " + Utils.createFieldValueQuery(SLOW_QUERY_DOCUMENT ,"true") ; // and it's not a slow query document
  }

  /**
   * Determine if there is any data to shrink
   * @param rsp query response
   * @return true or false depending if there is any data to shrink
   */
  private boolean isThereAnythingToShrink(QueryResponse rsp){
    return (rsp.getResults().getNumFound() > 0) ;
  }

  /**
   * Fetch stats about the thoth documents
   * @return query response containing stats
   * @throws SolrServerException
   */

  public QueryResponse fetchStats() throws SolrServerException {
    return realTimeServer.query(defineStatsQuery());
  }


  /**
   * Fetch facets about the thoth documents
   * @return query response containing facets
   * @throws SolrServerException
   */
  public QueryResponse fetchFacets() throws SolrServerException {
    return  realTimeServer.query(defineFacetQuery());
  }


  /**
   * Generate a Shrank document given facets and stats
   * @param facets
   * @param stats
   * @return Shrank document
   */
  public SolrInputDocument generateShrankDocument(QueryResponse facets, QueryResponse stats){
     SolrInputDocument shrankDocument = new SolrInputDocument();

    if (isFieldPresent(stats.getFieldStatsInfo().get(QTIME))){  // Add QTime stats
      shrankDocument.addField(AVG_QTIME, stats.getFieldStatsInfo().get(QTIME).getMean());
    }

    if (isFieldPresent(stats.getFieldStatsInfo().get(REQUESTS_IN_PROGRESS))){  // Add number of queries on deck
      shrankDocument.addField(AVG_REQUESTS_IN_PROGRESS, stats.getFieldStatsInfo().get(REQUESTS_IN_PROGRESS).getMean());
    }


    shrankDocument.addField(MASTER_DOCUMENT_TIMESTAMP, Utils.dateTimeToZuluSolrFormat(nowMinusTimeToShrink));
    shrankDocument.addField(MASTER_MINUTES_DOCUMENT, true);

    // Information about the Solr instance
    shrankDocument.addField(MessageRequestDocument.HOSTNAME, serverDetail.getName());
    shrankDocument.addField(MessageRequestDocument.POOL, serverDetail.getPool());
    shrankDocument.addField(MessageRequestDocument.PORT, Integer.parseInt(serverDetail.getPort()));
    shrankDocument.addField(MessageRequestDocument.CORENAME, serverDetail.getCore());


    // QTime ranges
    shrankDocument.addField(NAME_RANGE_0_10, getValueFromFacet(facets, QTIME + ":[" + VALUE_RANGE_0_10 + "]"));
    shrankDocument.addField(NAME_RANGE_10_100, getValueFromFacet(facets, QTIME + ":[" + VALUE_RANGE_10_100 + "]"));
    shrankDocument.addField(NAME_RANGE_100_1000, getValueFromFacet(facets, QTIME + ":[" + VALUE_RANGE_100_1000 + "]"));
    shrankDocument.addField(NAME_RANGE_1000_OVER, getValueFromFacet(facets, QTIME + ":[" + VALUE_RANGE_1000_OVER + "]"));
    shrankDocument.addField(NAME_TOT_COUNT, getValueFromFacet(facets, QTIME + ":[" + VALUE_TOT_COUNT + "]"));

    // Zero hits queries
    shrankDocument.addField(ZERO_HITS_QUERIES_COUNT, getValueFromFacet(facets, HITS + ":0"));

    // Exception count
    shrankDocument.addField(EXCEPTION_QUERIES_COUNT, getValueFromFacet(facets, EXCEPTION + ":true"));

    // Type of queries
    // TODO: generalize
    shrankDocument.addField(RANGE_QUERIES_COUNT, getValueFromFacet(facets, BITMASK + ":" + BITMASK_CONTAINS_RANGE_QUERY));
    shrankDocument.addField(FACET_QUERIES_COUNT, getValueFromFacet(facets, BITMASK + ":" + BITMASK_CONTAINS_FACET_QUERY));
    shrankDocument.addField(PROPERTY_LOOKUP_QUERIES_COUNT, getValueFromFacet(facets, BITMASK + ":" + BITMASK_CONTAINS_PROPERTY_LOOKUP_QUERY));
    shrankDocument.addField(PROPERTY_HASH_LOOKUP_QUERIES_COUNT, getValueFromFacet(facets, BITMASK + ":" + BITMASK_CONTAINS_PROPERTY_HASH_LOOKUP_QUERY));
    shrankDocument.addField(GEOSPATIAL_QUERIES_COUNT, getValueFromFacet(facets, BITMASK + ":" + BITMASK_CONTAINS_GEOSPATIAL_QUERY));
    shrankDocument.addField(OPENHOMES_QURIES_COUNT, getValueFromFacet(facets, BITMASK + ":" + BITMASK_CONTAINS_OPEN_HOMES_QUERY));


    return shrankDocument;
  }

  /**
   * Tag slower documents and add them to the shrank core
   */
  private void tagAndAddSlowThothDocuments() throws IOException, SolrServerException {
    // Query to return top MAX_NUMBER_SLOW_THOTH_DOCS slower thoth documents
    QueryResponse qr = realTimeServer.query(
        new SolrQuery()
            .setQuery(createThothDocsAggregationQuery())
            .addSort(QTIME, SolrQuery.ORDER.desc)
            .setRows(MAX_NUMBER_SLOW_THOTH_DOCS)
    );

    for (SolrDocument solrDocument: qr.getResults()){
      SolrInputDocument si = ClientUtils.toSolrInputDocument(solrDocument);
      // Remove old ID and version
      si.removeField(ID);
      si.removeField("_version_");
      // Tag document as slow
      si.addField(SLOW_QUERY_DOCUMENT, true);
      LOG.debug("Adding slow query document for server " + serverDetail.getName());
      shrankServer.add(si);
    }
  }


  /**
   * Create real time core clean up query
   * @return clean up query
   */
  private String getRealTimeCoreCleanUpQuery(){
    // Remove the single documents
    return
        Utils.createFieldValueQuery(MessageRequestDocument.HOSTNAME, "\"" + serverDetail.getName() + "\"")
        + " AND " + Utils.createFieldValueQuery(MessageRequestDocument.CORENAME, "\"" + serverDetail.getCore() + "\"")
        + " AND " + Utils.createFieldValueQuery(MessageRequestDocument.PORT, "\"" + serverDetail.getPort() + "\"")
        + " AND NOT " + Utils.createFieldValueQuery(MASTER_MINUTES_DOCUMENT, "true")
        // Keep the exception documents for now
        + " AND NOT " + Utils.createFieldValueQuery(EXCEPTION, "true")
        + " AND NOT " + Utils.createFieldValueQuery(SLOW_QUERY_DOCUMENT, "true");
  }


  /**
   * Retrieve value from Facet
   * @param rsp query response
   * @param key field key
   * @return object value
   */
  private Object getValueFromFacet(QueryResponse rsp, String key){
    return rsp.getFacetQuery().get(key);
  }


}