package org.greenstone.gsdl3.util;

import java.io.IOException;
import java.lang.reflect.Type;
import java.net.URLDecoder;
//import java.util.*;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.log4j.Logger;

import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.SolrQuery;

import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.client.solrj.response.TermsResponse;

import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;

import org.apache.solr.common.params.FacetParams;

import org.greenstone.LuceneWrapper4.SharedSoleneQuery;
import org.greenstone.LuceneWrapper4.SharedSoleneQueryResult;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;

public class SolrQueryWrapper extends SharedSoleneQuery {

    static Logger logger =
        Logger.getLogger(SolrQueryWrapper.class.getName());

    public static String SORT_ASCENDING = "asc";
    public static String SORT_DESCENDING = "desc";
    public static String SORT_BY_RANK = "score";
    public static String SORT_BY_INDEX_ORDER = "_docid_";

    protected SolrClient solr_core = null;

    protected int max_docs = 100;
    protected String sort_order = SORT_DESCENDING;
    protected String sort_field = SORT_BY_RANK;

    protected String docFilter = null;
  
  protected String highlight_field = null;
  protected boolean isHighlightingQuery = false;
  // protected String collection_core_name_prefix = null;
    protected ArrayList<String> facets = new ArrayList<>();
    protected ArrayList<String> facetQueries = new ArrayList<>();

    public SolrQueryWrapper() {
        super();
        start_results = 0;
    }

    public void setSolrCore(SolrClient solr_core) {
        this.solr_core = solr_core;
    }

  //   public void setCollectionCoreNamePrefix(String colCoreNamePrefix) {
  //   this.collection_core_name_prefix = colCoreNamePrefix;
  // }

    public void setMaxDocs(int max_docs) {
        this.max_docs = max_docs;
    }

    public void setSortField(String field) {
        if (field != null) {
            this.sort_field = field;
        }
    }

    public void setSortOrder(String order) {
        this.sort_order = order;
    }

    public void setHighlightField(String field) {
        this.highlight_field = field;
    }

    public void setDocFilter(String filter) {
        this.docFilter = filter;
    }

  public void setHighlightingQuery(boolean value) {
    this.isHighlightingQuery = value;
  }
  
  public boolean isHighlightingQuery() {
    return this.isHighlightingQuery;
  }
    public void addFacet(String facet) {
        if (!facets.contains(facet)) {
            facets.add(facet);
        }
    }

    public void clearFacets() {
        facets.clear();
    }

    public void addFacetQuery(String facetQuery)
  {
    if (!facetQueries.contains(facetQuery))
    {
      facetQueries.add(facetQuery);
    }
  }

  public void clearFacetQueries()
  {
    facetQueries.clear();
  }

    public boolean initialise() {

        if (solr_core == null) {
            logger.error("Solr core not loaded");
            return false;
        }

        return true;
    }

    public SharedSoleneQueryResult runQuery(String query_string) {

        if (query_string == null || query_string.isEmpty()) {
            return null;
        }
        String original_query = query_string;
        SolrQueryResult result = new SolrQueryResult();
        result.clear();

        
        if (facetQueries.size() > 0) {
          String facet_query = getFacetQueryString();
          if (facet_query.length() > 0) {
            
            query_string += " AND " + facet_query;
          }
        } 
          
        

        
        try {

          SolrQuery query = new SolrQuery();
          
          /*The Extended DisMax (eDisMax) query parser is an advanced and more flexible version of the original DisMax parser in Apache Solr. It is widely used in modern search applications because it offers a balance between a user-friendly, error-tolerant search experience (like public search engines) and the powerful, fine-grained control of the standard Lucene query syntax. */
          query.setRequestHandler("/select");
          query.set("defType", "edismax"); // alternatively 'lucene'
          query.set("q", query_string);
          query.set("debug", "query"); // returns the expanded query, so we can work out the terms it was expanded into
            
            // these params make searches more like google searching
            query.set("mm", "2<75%"); // minimum match - all terms required if 1 or 2 terms, 75% required if 3 or more
            query.set("qs", "2"); // phrase slop
            query.set("tie", "0.1"); // combine scores across fields
            query.setStart(start_results);
            query.setRows(end_results - start_results);

            // cache repeated queries
            query.set("cache", "true");
            query.addSort(
                sort_field,
                SolrQuery.ORDER.valueOf(sort_order)
            );

            // from chatgpt - use these if there is no explicit field in the query, eg snails, rather than ZZ:snails
            //query.set("qf", "TI^3 TEXT");
            //query.set("pf", "TI^5 TEXT"); - boost phrase fields
            query.setFields("docOID", "score"); // the fields from the documents we want returned

            query.set("q.op", default_conjunction_operator);

            if (docFilter != null) {
                query.addFilterQuery("docOID:" + docFilter + "*");
            }

            if (highlight_field != null) {

                query.setHighlight(true);
                query.set("hl.fl", highlight_field);
                query.set("hl.method", "unified"); // requires either field to be stored (small fields), or storeOffsetsWithPositions=true (large fields)
                query.set("hl.weightMatches", "true"); // chatgpt says this gives better highlighting
                query.set("hl.maxAnalyzedChars", "100000"); // make this higher than the default of 10000
                query.set("hl.useTermVectors", "true");
                query.set("hl.highlightMultiTerm", "true");
                query.set("hl.snippets", "3");
                query.set("hl.fragsize", "100");
                query.set("hl.tag.pre", "&lt;span class=\"snippetText\"&gt;" );
                query.set("hl.tag.post","&lt;/span&gt;" );
                //query.setHighlightSimplePre("<span class=\"snippetText\">");
                //query.setHighlightSimplePost("</span>");
            }

            if (!facets.isEmpty()) {

                query.setFacet(true);

                for (String f : facets) {
                    query.addFacetField(f);
                }

                query.setFacetSort(FacetParams.FACET_SORT_COUNT);
            }

            /*
             * TermsComponent replaces old termfreq extraction
             */
/*            if (highlight_field != null) {
              // now chatgpt is saying setrequesthandler("/terms")
              // but it wrote this file so its a dick
                query.setTerms(true);
                query.set("terms.fl", highlight_field);
                query.set("terms.limit",100);
                query.set("terms.mincount",1);
                query.set("fq", original_query); // restrict the terms back to those in hte query
            }
*/
            logger.error("Solr query: " + query);

            QueryResponse response = solr_core.query(query);

            Map<String,Object> debug = response.getDebugMap(); // to get analysed terms
            String parsed = (String)debug.get("parsedquery_toString");
            getTermFrequencies(result, parsed);
            //logger.error("parsed query string = "+parsed);
            SolrDocumentList hits = response.getResults();

            result.setTotalDocs((int) hits.getNumFound());

            result.setStartResults(start_results);
            result.setEndResults(start_results + hits.size());

            for (SolrDocument doc : hits) {

                String docOID = (String) doc.getFieldValue("docOID");
                Float score = (Float) doc.getFieldValue("score");

                result.addDoc(docOID, score.floatValue(), 0);
            }

            if (highlight_field != null) {
            // add highlight snippets
              Map<String, Map<String, List<String>>> hlResponse = response.getHighlighting();
              result.setHighlightResults(hlResponse);
              //logger.error("highlight response = "+hlResponse.toString());
            }
            
            result.setFacetResults(response.getFacetFields());

        } catch (SolrServerException e) {

            e.printStackTrace();
            result.setError(SolrQueryResult.SERVER_ERROR);

        } catch (IOException e) {

            e.printStackTrace();
            result.setError(SolrQueryResult.IO_ERROR);
        }

        return result;
    }

  public class Pair {
    public String field;
    public String term;

    public Pair(String f, String t) {
      this.field = f;
      this.term = t;
    }
  }
  protected void getTermFrequencies(SolrQueryResult result, String parsed) {

    // lets try parsing the query string to get actual terms (eg snails will -> snail) and wildcard things
    // eg econom* will stay like that after parsing. wildcards we need to

    // System.err.println("parsed = "+parsed);
    
    Pattern p = Pattern.compile(
      "(?:(\\w+):)?([\\w*?]+)"
      );
    
    Matcher m = p.matcher(parsed);
    
    List<Pair> terms = new ArrayList<>();
    
    while (m.find()) {
      
      String field = m.group(1);
      String term = m.group(2);
      
      // skip boolean operators
      if (term.equalsIgnoreCase("AND")
          || term.equalsIgnoreCase("OR")
          || term.equalsIgnoreCase("NOT"))
      {
        continue;
      }
      
      terms.add(new Pair(field,term));
    }
    
//  for (Pair t : terms) {
//    System.out.println("TERM: " + t.term + ", "+t.field);
//  }

  // do a terms request for each term
  try {
    for (Pair record : terms) {
      String term = record.term;
      String field = record.field;
      //
      // PREFIX / WILDCARD QUERY
      //
      if (term.endsWith("*")) {

        String prefix =
          term.substring(0, term.length() - 1);
        
        // System.out.println("PREFIX QUERY: " + prefix);

        //
        // TERMS COMPONENT QUERY
        //
        SolrQuery tq = new SolrQuery();
        tq.setRequestHandler("/terms");
        tq.set("terms","true");
        tq.set("terms.fl", field);
        tq.set("terms.prefix",prefix);
        tq.set("terms.limit",100);
        tq.set("terms.ttf", true);
        tq.set("terms.mincount",1);

        QueryResponse tr =
            solr_core.query(tq);

        TermsResponse termsResponse = tr.getTermsResponse();

        List<TermsResponse.Term> expandedTerms = termsResponse.getTerms(field);
        
        if (expandedTerms != null) {
          
          for (TermsResponse.Term expanded : expandedTerms)
          {
            String expandedTerm = expanded.getTerm();
            
            long df = expanded.getFrequency();
            long tf = expanded.getTotalTermFreq();
            result.addTerm(expandedTerm, field, (int)df, (int)tf);
            //System.out.println("  EXPANDED: " + expandedTerm +  " df=" + df + " tf=" + tf);
            
          }
        }
      }

      //
      // NORMAL TERM QUERY
      //
      else {

        //System.out.println("NORMAL TERM: " + term);

        //
        // LOOKUP TERM FREQUENCY
        //
        SolrQuery tq = new SolrQuery();

        tq.setRequestHandler("/terms");
        tq.set("terms","true");
        tq.set("terms.fl", field);
        tq.set("terms.lower",term);
        tq.set("terms.upper",term);
        tq.set("terms.lower.incl", true);
        tq.set("terms.upper.incl", true);
        tq.set("terms.ttf", true);
        tq.set("terms.mincount",1);

        QueryResponse tr = solr_core.query(tq);

        TermsResponse termsResponse = tr.getTermsResponse();

        List<TermsResponse.Term> matchingTerms =  termsResponse.getTerms(field);

        if (matchingTerms != null && !matchingTerms.isEmpty())
        {
            TermsResponse.Term matched = matchingTerms.get(0);
            
            long df = matched.getFrequency();
            long tf = matched.getTotalTermFreq();
            //System.out.println("  EXPANDED: " + term +  " df=" + df + " tf=" + tf);
            // matching docs, term freq
            result.addTerm(term, field, (int)df, (int)tf);
        }
        else {

            System.out.println(term +" TERM NOT FOUND");
        }
      }
    } // for each record
  } catch (SolrServerException e) {

    e.printStackTrace();
    result.setError(SolrQueryResult.SERVER_ERROR);
    
  } catch (IOException e) {
    
    e.printStackTrace();
    result.setError(SolrQueryResult.IO_ERROR);
  }
  
}
  protected String getFacetQueryString() {
      HashMap<String, ArrayList<String>> grouping = new HashMap<String, ArrayList<String>>();
      for (String currentQuery : facetQueries)
      {
        //Facet queries are stored in JSON, so we have to decode it
        Gson gson = new Gson();
        Type type = new TypeToken<List<String>>()
                    {
        }.getType();
        List<String> queryElems = gson.fromJson(currentQuery, type);

        //Group each query segment by the index it uses
        for (String currentQueryElement : queryElems)
        {
          //logger.info("@@@@ currentQueryElement " + currentQueryElement);
				    
          String decodedQueryElement = null;
          try
          {
            decodedQueryElement = URLDecoder.decode(currentQueryElement, "UTF-8");
          }
          catch (Exception ex)
          {
            continue;
          }

          int colonIndex = currentQueryElement.indexOf(":");
          String indexShortName = currentQueryElement.substring(0, colonIndex);

          if (grouping.get(indexShortName) == null)
          {
            grouping.put(indexShortName, new ArrayList<String>());
          }
          grouping.get(indexShortName).add(decodedQueryElement);
        }
      }

      //Construct the facet query string to add to the regular query string
      StringBuilder facetQueryString = new StringBuilder();
      int keysetCounter = 0;
      for (String key : grouping.keySet())
      {
        StringBuilder currentFacetString = new StringBuilder("(");
        int groupCounter = 0;
        for (String queryElem : grouping.get(key))
        {
          currentFacetString.append(queryElem);

          groupCounter++;
          if (groupCounter < grouping.get(key).size())
          {
            currentFacetString.append(" OR ");
          }
        }
        currentFacetString.append(")");

        facetQueryString.append(currentFacetString);

        keysetCounter++;
        if (keysetCounter < grouping.keySet().size())
        {
          facetQueryString.append(" AND ");
        }
      }
      return facetQueryString.toString();
                  }

    public String runHighlightingQuery(String query) {

        try {

            SolrQuery solrQuery = new SolrQuery(query);

            solrQuery.setHighlight(true);
            solrQuery.set("hl.fl", highlight_field);
            solrQuery.set("hl.method", "unified");
            solrQuery.set("hl.weightMatches", "true"); // chatgpt says this gives better highlighting
            solrQuery.set("hl.fragsize", 0); // return the entire field

            solrQuery.addFilterQuery("docOID:" + docFilter);

            solrQuery.setHighlightSimplePre(
                "<span class=\"termHighlight\">"
            );

            solrQuery.setHighlightSimplePost("</span>");

            logger.error("highlight query for docOID "+docFilter+" our query is: "+solrQuery.toString());
            QueryResponse resp = solr_core.query(solrQuery);

            Map<String, Map<String, List<String>>> hl =
                resp.getHighlighting();

            if (hl != null &&
                hl.get(docFilter) != null &&
                hl.get(docFilter).get(highlight_field) != null) {

                return hl.get(docFilter)
                         .get(highlight_field)
                         .get(0);
            }

        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }

    public void setStartResults(int start_results) {

        if (start_results < 0) {
            start_results = 0;
        }

        this.start_results = start_results;
    }

}
