/*
 *  Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
 *  <p>
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *  <p>
 *  http://www.apache.org/licenses/LICENSE-2.0
 *  <p>
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package com.easyagents.search.engine.lucene;

import com.easyagents.core.document.Document;
import com.easyagents.search.engine.service.DocumentSearcher;
import com.easyagents.search.engine.service.KeywordSearchMetadataKeys;
import com.easyagents.search.engine.service.KeywordSearchRequest;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.StringField;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.*;
import org.apache.lucene.queryparser.classic.ParseException;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.*;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.util.IOUtils;
import org.lionsoul.jcseg.ISegment;
import org.lionsoul.jcseg.analyzer.JcsegAnalyzer;
import org.lionsoul.jcseg.dic.DictionaryFactory;
import org.lionsoul.jcseg.segmenter.SegmenterConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;

/**
 * 基于 Lucene 的本地关键词搜索器。
 *
 * <p>每个实例长期复用一个线程安全的 {@link IndexWriter}，避免并发任务重复抢占
 * 同一索引目录的 {@code write.lock}。</p>
 */
public class LuceneSearcher implements DocumentSearcher, AutoCloseable {

    private static final Logger LOG = LoggerFactory.getLogger(LuceneSearcher.class);

    private final Directory directory;
    private final Analyzer analyzer;
    private final IndexWriter indexWriter;

    /**
     * 创建 Lucene 搜索器并打开索引写入器。
     *
     * @param config Lucene 配置
     * @throws IllegalStateException 索引目录或写入器初始化失败时抛出
     */
    public LuceneSearcher(LuceneConfig config) {
        Objects.requireNonNull(config, "LuceneConfig 不能为 null");
        Directory openedDirectory = null;
        Analyzer openedAnalyzer = null;
        IndexWriter openedWriter = null;
        try {
            String indexDirPath = config.getIndexDirPath(); // 索引目录路径
            File indexDir = new File(indexDirPath);
            if (!indexDir.exists() && !indexDir.mkdirs()) {
                throw new IllegalStateException("can not mkdirs for path: " + indexDirPath);
            }

            openedDirectory = FSDirectory.open(indexDir.toPath());
            openedAnalyzer = createAnalyzer();
            openedWriter = new IndexWriter(openedDirectory, new IndexWriterConfig(openedAnalyzer));
        } catch (IOException | RuntimeException e) {
            IOUtils.closeWhileHandlingException(openedWriter, openedAnalyzer, openedDirectory);
            LOG.error("初始化 Lucene 索引失败", e);
            throw new IllegalStateException("初始化 Lucene 索引失败", e);
        }
        this.directory = openedDirectory;
        this.analyzer = openedAnalyzer;
        this.indexWriter = openedWriter;
    }

    /**
     * 以文档 ID 为键写入或覆盖单个文档。
     *
     * @param document 待写入文档
     * @return 写入成功时返回 {@code true}
     */
    @Override
    public boolean addDocument(Document document) {
        return addDocuments(Collections.singletonList(document));
    }

    /**
     * 批量写入或覆盖文档，并在批次结束后统一提交。
     *
     * @param documents 待写入文档
     * @return 全部文档写入成功时返回 {@code true}
     */
    @Override
    public boolean addDocuments(List<Document> documents) {
        if (documents == null || documents.isEmpty()) {
            return false;
        }
        List<org.apache.lucene.document.Document> luceneDocuments = new ArrayList<>(documents.size());
        for (Document document : documents) {
            if (document == null || document.getId() == null || document.getContent() == null) {
                return false;
            }
            luceneDocuments.add(toLuceneDocument(document));
        }
        try {
            for (org.apache.lucene.document.Document luceneDocument : luceneDocuments) {
                String documentId = luceneDocument.get("id");
                // updateDocument 以 ID 执行 upsert，确保失败重试不会生成重复索引。
                indexWriter.updateDocument(new Term("id", documentId), luceneDocument);
            }
            indexWriter.commit();
            return true;
        } catch (Exception e) {
            LOG.error("批量添加文档失败: count={}", documents.size(), e);
            return false;
        }
    }

    /**
     * 删除指定文档。
     *
     * @param id 文档 ID
     * @return 删除成功时返回 {@code true}
     */
    @Override
    public boolean deleteDocument(Object id) {
        return deleteDocuments(Collections.singletonList(id));
    }

    /**
     * 批量删除指定文档，并在批次结束后统一提交。
     *
     * @param ids 文档 ID 集合
     * @return 全部文档删除成功时返回 {@code true}
     */
    @Override
    public boolean deleteDocuments(Collection<?> ids) {
        if (ids == null || ids.isEmpty()) {
            return false;
        }
        Set<String> uniqueIds = new LinkedHashSet<>();
        for (Object id : ids) {
            if (id == null) {
                return false;
            }
            uniqueIds.add(id.toString());
        }
        try {
            Term[] terms = new Term[uniqueIds.size()];
            int index = 0;
            for (String id : uniqueIds) {
                terms[index++] = new Term("id", id);
            }
            indexWriter.deleteDocuments(terms);
            indexWriter.commit();
            return true;
        } catch (IOException e) {
            LOG.error("批量删除文档失败: count={}", uniqueIds.size(), e);
            return false;
        }
    }

    /**
     * 以文档 ID 为键更新文档。
     *
     * @param document 待更新文档
     * @return 更新成功时返回 {@code true}
     */
    @Override
    public boolean updateDocument(Document document) {
        return addDocument(document);
    }

    /**
     * 按请求条件搜索文档。
     *
     * @param request 搜索请求
     * @return 命中文档
     */
    @Override
    public List<Document> searchDocuments(KeywordSearchRequest request) {
        List<Document> results = new ArrayList<>();
        try (IndexReader reader = DirectoryReader.open(directory)) {
            IndexSearcher searcher = new IndexSearcher(reader);
            Query query = buildQuery(request);
            TopDocs topDocs = searcher.search(query, request == null ? 10 : request.getCount());
            for (ScoreDoc scoreDoc : topDocs.scoreDocs) {
                org.apache.lucene.document.Document doc = searcher.doc(scoreDoc.doc);
                Document resultDoc = new Document();
                resultDoc.setId(doc.get("id"));
                resultDoc.setContent(doc.get("content"));
                resultDoc.setTitle(doc.get("title"));
                if (doc.get(KeywordSearchMetadataKeys.KNOWLEDGE_ID) != null) {
                    resultDoc.addMetadata(KeywordSearchMetadataKeys.KNOWLEDGE_ID, doc.get(KeywordSearchMetadataKeys.KNOWLEDGE_ID));
                }

                resultDoc.setScore((double) scoreDoc.score);

                results.add(resultDoc);
            }
        } catch (Exception e) {
            LOG.error("搜索文档失败", e);
        }

        return results;
    }

    Query buildQuery(KeywordSearchRequest request) {
        try {
            String keyword = request == null ? null : request.getKeyword();

            QueryParser titleQueryParser = new QueryParser("title", analyzer);
            Query titleQuery = titleQueryParser.parse(keyword);
            BooleanClause titleBooleanClause = new BooleanClause(titleQuery, BooleanClause.Occur.SHOULD);

            QueryParser contentQueryParser = new QueryParser("content", analyzer);
            Query contentQuery = contentQueryParser.parse(keyword);
            BooleanClause contentBooleanClause = new BooleanClause(contentQuery, BooleanClause.Occur.SHOULD);

            BooleanQuery.Builder builder = new BooleanQuery.Builder();
            builder.add(titleBooleanClause)
                .add(contentBooleanClause);
            if (request != null && request.getKnowledgeId() != null && !request.getKnowledgeId().trim().isEmpty()) {
                builder.add(new TermQuery(new Term(KeywordSearchMetadataKeys.KNOWLEDGE_ID, request.getKnowledgeId().trim())), BooleanClause.Occur.MUST);
            }
            return builder.build();
        } catch (ParseException e) {
            LOG.error(e.toString(), e);
        }
        return null;
    }

    private static Analyzer createAnalyzer() {
        SegmenterConfig config = new SegmenterConfig(true);
        return new JcsegAnalyzer(ISegment.Type.NLP, config, DictionaryFactory.createSingletonDictionary(config));
    }

    private org.apache.lucene.document.Document toLuceneDocument(Document document) {
        org.apache.lucene.document.Document luceneDoc = new org.apache.lucene.document.Document();
        luceneDoc.add(new StringField("id", document.getId().toString(), Field.Store.YES));
        luceneDoc.add(new TextField("content", document.getContent(), Field.Store.YES));
        if (document.getTitle() != null) {
            luceneDoc.add(new TextField("title", document.getTitle(), Field.Store.YES));
        }
        appendKnowledgeId(document, luceneDoc);
        return luceneDoc;
    }

    private void appendKnowledgeId(Document document, org.apache.lucene.document.Document luceneDoc) {
        if (document == null || document.getMetadataMap() == null) {
            return;
        }
        Object knowledgeId = document.getMetadata(KeywordSearchMetadataKeys.KNOWLEDGE_ID);
        if (knowledgeId != null) {
            luceneDoc.add(new StringField(KeywordSearchMetadataKeys.KNOWLEDGE_ID, String.valueOf(knowledgeId), Field.Store.YES));
        }
    }

    /**
     * 关闭写入器、分词器和索引目录。
     *
     * @throws IOException 任一 Lucene 资源关闭失败时抛出
     */
    @Override
    public void close() throws IOException {
        IOUtils.close(indexWriter, analyzer, directory);
    }
}
