package cn.reghao.tnb.search.app.lucene;

import cn.reghao.jutil.jdk.web.db.PageList;
import cn.reghao.tnb.search.app.config.AppProperties;
import cn.reghao.tnb.search.app.model.vo.ElasticQuery;
import cn.reghao.tnb.search.app.model.vo.LuceneQuery;
import cn.reghao.tnb.search.app.model.vo.VideoQuery;
import lombok.extern.slf4j.Slf4j;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.IntPoint;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.Term;
import org.apache.lucene.queryparser.classic.MultiFieldQueryParser;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.*;
import org.apache.lucene.search.highlight.*;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.springframework.stereotype.Service;
import org.wltea.analyzer.lucene.IKAnalyzer;

import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @author reghao
 * @date 2023-03-02 09:29:04
 */
@Slf4j
@Service
public class LuceneSearch {
    private String nativeLuceneDir;
    private Analyzer luceneAnalyzer;
    private SimpleHTMLFormatter formatter;
    private Map<String, IndexReader> indexReaderMap = new HashMap<>();

    public LuceneSearch(AppProperties appProperties) {
        this.nativeLuceneDir = String.format("%s/%s", appProperties.getBaseDir(), appProperties.getNativeLuceneDirname());
        this.luceneAnalyzer = new IKAnalyzer();
        this.formatter = new SimpleHTMLFormatter("<span style='color:red;'>", "</span>");
    }

    // TODO 并发处理
    public IndexReader getIndexReader(String indexName) throws IOException {
        IndexReader indexReader = indexReaderMap.get(indexName);
        if (indexReader == null) {
            synchronized (this) {
                indexReader = getIndexReader0(indexName);
                indexReaderMap.put(indexName, indexReader);
            }
        }

        return indexReader;
    }

    public IndexReader getIndexReader0(String indexName) throws IOException {
        String indexDir = String.format("%s/%s", nativeLuceneDir, indexName);
        Directory directory = FSDirectory.open(Paths.get(indexDir));
        IndexReader indexReader = DirectoryReader.open(directory);
        return indexReader;
    }

    public PageList queryWithHighlight(ElasticQuery elasticQuery, Class clazz) {
        try {
            LuceneQuery luceneQuery = getLuceneQueryByElasticQuery(elasticQuery);
            return getResults(luceneQuery, clazz);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return PageList.empty();
    }

    private LuceneQuery getLuceneQueryByElasticQuery(ElasticQuery elasticQuery) throws Exception {
        int pageSize = elasticQuery.getPageSize();
        int pageNumber = elasticQuery.getPageNumber();
        String indexName = elasticQuery.getIndexName();
        List<String>  queryFiledNames = elasticQuery.getQueryFiledNames();
        Set<String> highlightFiledNames = new HashSet<>(elasticQuery.getHighlightFiledNames());
        String queryString = elasticQuery.getQueryString();
        Query query = getQueryByQueryParser(queryFiledNames, queryString);
        return new LuceneQuery.Builder()
                .pageSize(pageSize)
                .pageNumber(pageNumber)
                .indexName(indexName)
                .highlightFiledNames(highlightFiledNames)
                .query(query)
                .build();
    }

    private LuceneQuery getLuceneQueryByVideoQuery(VideoQuery videoQuery) throws Exception {
        int pageSize = videoQuery.getPageSize();
        List<String>  queryFiledNames = videoQuery.getQueryFiledNames();
        Set<String> highlightFiledNames = new HashSet<>(videoQuery.getHighlightFiledNames());
        int pageNumber = videoQuery.getPageNumber();
        String indexName = videoQuery.getIndexName();

        Query query1 = getQueryByQueryParser(queryFiledNames, videoQuery.getQueryString());
        Query query2 = IntPoint.newSetQuery("scope", videoQuery.getScopes());
        Query query = new BooleanQuery.Builder()
                .add(query1, BooleanClause.Occur.MUST)
                .add(query2, BooleanClause.Occur.MUST)
                .build();

        return new LuceneQuery.Builder()
                .pageSize(pageSize)
                .pageNumber(pageNumber)
                .indexName(indexName)
                .highlightFiledNames(highlightFiledNames)
                .query(query)
                .build();
    }

    private PageList getResults(LuceneQuery luceneQuery, Class clazz) throws Exception {
        String indexName = luceneQuery.getIndexName();
        int pageNumber = luceneQuery.getPageNumber();
        int pageSize = luceneQuery.getPageSize();
        Query query = luceneQuery.getQuery();
        Set<String> highlightFiledNames = luceneQuery.getHighlightFiledNames();

        IndexReader indexReader = getIndexReader(indexName);
        IndexSearcher indexSearcher = new IndexSearcher(indexReader);
        long total;
        TopDocs topDocs;
        if (pageNumber == 1) {
            int count = pageSize;
            topDocs = indexSearcher.search(query, count);
            total = topDocs.totalHits.value;
        } else {
            int count = (pageNumber-1)*pageSize;
            TopDocs prevTopDocs = indexSearcher.searchAfter(null, query, count);
            total = prevTopDocs.totalHits.value;

            ScoreDoc[] prevScoreDocs = prevTopDocs.scoreDocs;
            ScoreDoc after = prevScoreDocs[prevScoreDocs.length-1];
            topDocs = indexSearcher.searchAfter(after, query, pageSize);
        }

        QueryScorer queryScorer = new QueryScorer(query);
        Highlighter highlighter = new Highlighter(formatter, queryScorer);

        Map<String, Field> map = Arrays.stream(clazz.getDeclaredFields())
                .collect(Collectors.toMap(Field::getName, k -> k));
        //List<T> list = new ArrayList<>();
        List<Object> list = new ArrayList<>();
        ScoreDoc[] scoreDocs = topDocs.scoreDocs;
        for (ScoreDoc scoreDoc : scoreDocs) {
            Document document = indexReader.document(scoreDoc.doc);

            //T tObject = null;
            Object object = null;
            for (Constructor<?> constructor : clazz.getDeclaredConstructors()) {
                int len = constructor.getParameterTypes().length;
                if (len == 0) {
                    object = constructor.newInstance();
                }
            }

            for (Map.Entry<String, Field> entry : map.entrySet()) {
                String fieldName = entry.getKey();
                Field field0 = clazz.getDeclaredField(fieldName);

                String fieldValue = document.get(fieldName);
                if (highlightFiledNames.contains(fieldName)) {
                    fieldValue = highlighter.getBestFragment(luceneAnalyzer, fieldName, fieldValue);
                }

                field0.setAccessible(true);
                Class<?> fieldType = field0.getType();
                if (fieldType.equals(String.class)) {
                    field0.set(object, fieldValue);
                } else if (field0.getType().equals(int.class) || field0.getType().equals(Integer.class)) {
                    field0.set(object, Integer.parseInt(fieldValue));
                } else if (field0.getType().equals(long.class) || field0.getType().equals(Long.class)) {
                    field0.set(object, Long.parseLong(fieldValue));
                } else if (field0.getType().equals(double.class) || field0.getType().equals(Double.class)) {
                    field0.set(object, Double.parseDouble(fieldValue));
                }
            }
            list.add(object);
        }

        return PageList.pageList(pageNumber, pageSize, (int) total, list);
    }

    public PageList queryWithHighlight(VideoQuery videoQuery, Class clazz) {
        try {
            LuceneQuery luceneQuery = getLuceneQueryByVideoQuery(videoQuery);
            return getResults(luceneQuery, clazz);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return PageList.empty();
    }

    private Query getQueryByQueryParser(List<String> queryFiledNames, String queryString) throws Exception {
        Query query;
        if (queryFiledNames.size() == 1) {
            // 单字段查询
            QueryParser queryParser = new QueryParser(queryFiledNames.get(0), luceneAnalyzer);
            query = queryParser.parse(queryString);
        } else if (queryFiledNames.size() > 1) {
            // 多字段查询
            String[] fields = queryFiledNames.toArray(new String[0]);
            MultiFieldQueryParser multiFieldQuery = new MultiFieldQueryParser(fields, luceneAnalyzer);
            query = multiFieldQuery.parse(queryString);
        } else {
            throw new Exception("query field is empty");
        }

        return query;
    }

    public String getHighlightText(String fieldName, String fieldValue, String queryString) throws Exception {
        List<String> queryFiledNames = List.of(fieldName);
        Query query = getQueryByQueryParser(queryFiledNames, queryString);

        QueryScorer queryScorer = new QueryScorer(query);
        Highlighter highlighter = new Highlighter(formatter, queryScorer);
        return highlighter.getBestFragment(luceneAnalyzer, fieldName, fieldValue);
    }

    public Document findDocumentById(String indexName, String id) {
        try {
            IndexReader indexReader = getIndexReader(indexName);
            IndexSearcher indexSearcher = new IndexSearcher(indexReader);

            Query query = new TermQuery(new Term("id", id));
            TopDocs topDocs = indexSearcher.search(query, 1);
            ScoreDoc[] scoreDocs = topDocs.scoreDocs;
            if (scoreDocs.length == 1) {
                Document document = indexReader.document(scoreDocs[0].doc);
                //highlighter.getBestFragment(luceneAnalyzer, fieldName, fieldValue);

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

        return null;
    }
}
