package tech.easyflow.ai.service.impl;

import cn.dev33.satoken.stp.StpUtil;
import com.easyagents.core.document.DocumentSplitter;
import com.easyagents.core.document.splitter.MarkdownHeaderSplitter;
import com.easyagents.core.document.splitter.RegexDocumentSplitter;
import com.easyagents.core.document.splitter.SimpleDocumentSplitter;
import com.easyagents.core.document.splitter.SimpleTokenizeSplitter;
import com.easyagents.core.file2text.File2TextUtil;
import com.easyagents.core.model.embedding.EmbeddingModel;
import com.easyagents.core.model.embedding.EmbeddingOptions;
import com.easyagents.core.store.DocumentStore;
import com.easyagents.core.store.StoreOptions;
import com.easyagents.core.store.StoreResult;
import com.easyagents.rag.core.RagChunk;
import com.easyagents.rag.core.RagDefaults;
import com.easyagents.rag.core.RagStrategyCodes;
import com.easyagents.rag.ingestion.RagIngestionService;
import com.easyagents.rag.ingestion.model.AnalysisResult;
import com.easyagents.rag.ingestion.model.StrategyConfig;
import com.easyagents.search.engine.service.DocumentSearcher;
import com.easyagents.search.engine.service.KeywordSearchMetadataKeys;
import com.mybatisflex.core.keygen.impl.FlexIDKeyGenerator;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryMethods;
import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.spring.service.impl.ServiceImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tech.easyflow.ai.config.SearcherFactory;
import tech.easyflow.ai.documentimport.DocumentImportDtos;
import tech.easyflow.ai.documentimport.DocumentImportKeys;
import tech.easyflow.ai.documentimport.DocumentImportPreviewService;
import tech.easyflow.ai.documentimport.task.KnowledgeDocumentImportTaskAppService;
import tech.easyflow.ai.entity.*;
import tech.easyflow.ai.enums.DocumentProcessStatus;
import tech.easyflow.ai.mapper.DocumentChunkMapper;
import tech.easyflow.ai.mapper.DocumentMapper;
import tech.easyflow.ai.service.DocumentChunkService;
import tech.easyflow.ai.service.DocumentCollectionService;
import tech.easyflow.ai.service.DocumentService;
import tech.easyflow.ai.service.ModelService;
import tech.easyflow.ai.support.DocumentStoreLifecycleSupport;
import tech.easyflow.common.ai.rag.ExcelDocumentSplitter;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.filestorage.FileStorageService;
import tech.easyflow.common.util.FileUtil;
import tech.easyflow.common.util.StringUtil;
import tech.easyflow.common.web.exceptions.BusinessException;

import javax.annotation.Resource;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;

import static tech.easyflow.ai.entity.DocumentCollection.KEY_CAN_UPDATE_EMBEDDING_MODEL;
import static tech.easyflow.ai.entity.table.DocumentChunkTableDef.DOCUMENT_CHUNK;
import static tech.easyflow.ai.entity.table.DocumentTableDef.DOCUMENT;

/**
 * 服务层实现。
 *
 * @author michael
 * @since 2024-08-23
 */
@Service("AiService")
public class DocumentServiceImpl extends ServiceImpl<DocumentMapper, Document> implements DocumentService {
    protected Logger Log = LoggerFactory.getLogger(DocumentServiceImpl.class);
    private static final String SOURCE_RANGES_KEY = "sourceRanges";

    @Resource
    private DocumentMapper documentMapper;

    @Resource
    private DocumentChunkMapper documentChunkMapper;

    @Resource
    private DocumentCollectionService knowledgeService;

    @Resource
    private ModelService modelService;

    @Resource(name = "default")
    FileStorageService storageService;

    @Resource
    private DocumentChunkService documentChunkService;

    @Autowired
    private SearcherFactory searcherFactory;

    @Autowired
    private RagIngestionService ragIngestionService;

    @Autowired
    private DocumentImportPreviewService documentImportPreviewService;

    @Autowired
    private KnowledgeDocumentImportTaskAppService importTaskAppService;

    @Override
    public Page<Document> getDocumentList(String knowledgeId, int pageSize, int pageNum, String fileName) {
        QueryWrapper queryWrapper=QueryWrapper.create()
                .select(
                        DOCUMENT.ALL_COLUMNS,
                        QueryMethods.count(DOCUMENT_CHUNK.DOCUMENT_ID).as("chunk_count")

                )
                .from(Document.class)
                .leftJoin(DocumentChunk.class).on(DOCUMENT.ID.eq(DOCUMENT_CHUNK.DOCUMENT_ID))
                .where(DOCUMENT.COLLECTION_ID.eq(knowledgeId))
                .orderBy(DOCUMENT.ID, false)
                ;
        if (fileName != null && !fileName.trim().isEmpty()) {
            queryWrapper.and(DOCUMENT.TITLE.like(fileName));
        }
        // 分组
        queryWrapper.groupBy(DOCUMENT.ID);
        Page<Document> documentVoPage = documentMapper.paginateAs(pageNum, pageSize, queryWrapper, Document.class);
        return documentVoPage;
    }

    /**
     * 根据文档id删除文件
     *
     * @param id 文档id
     * @return
     */
    @Override
    public boolean removeDoc(String id) {
        // 查询该文档对应哪些分割的字段，先删除
        QueryWrapper queryWrapperDocument = QueryWrapper.create().eq(Document::getId, id);
        Document oneByQuery = documentMapper.selectOneByQuery(queryWrapperDocument);
        if (oneByQuery == null) {
            return false;
        }
        if (DocumentProcessStatus.PARSING.name().equals(oneByQuery.getProcessStatus())
            || DocumentProcessStatus.INDEXING.name().equals(oneByQuery.getProcessStatus())) {
            throw new BusinessException("文档处理中，暂不允许删除");
        }
        DocumentCollection knowledge = knowledgeService.getById(oneByQuery.getCollectionId());
        if (knowledge == null) {
            return false;
        }

        // 存储到知识库
        DocumentStore documentStore = knowledge.toDocumentStore();
        if (documentStore == null) {
            return false;
        }

        try {
            Model model = modelService.getById(knowledge.getVectorEmbedModelId());
            if (model == null) {
                return false;
            }
            // 设置向量模型
            StoreOptions options = StoreOptions.ofCollectionName(knowledge.getVectorStoreCollection());
            EmbeddingOptions embeddingOptions = new EmbeddingOptions();
            embeddingOptions.setModel(model.getModelName());
            options.setEmbeddingOptions(embeddingOptions);
            options.setCollectionName(knowledge.getVectorStoreCollection());
            // 查询文本分割表tb_document_chunk中对应的有哪些数据，找出来删除
            QueryWrapper queryWrapper = QueryWrapper.create()
                    .select(DOCUMENT_CHUNK.ID).eq(DocumentChunk::getDocumentId, id);
            List<BigInteger> chunkIds = documentChunkMapper.selectListByQueryAs(queryWrapper, BigInteger.class);
            documentStore.delete(chunkIds, options);
            // 删除搜索引擎中的数据
            DocumentSearcher searcher = searcherFactory.getSearcher();
            if (searcher != null) {
                chunkIds.forEach(searcher::deleteDocument);
            }
            int ck = documentChunkMapper.deleteByQuery(QueryWrapper.create().eq(DocumentChunk::getDocumentId, id));
            if (ck < 0) {
                return false;
            }
            // 再删除指定路径下的文件
            Document document = documentMapper.selectOneByQuery(queryWrapperDocument);
            storageService.delete(document.getDocumentPath());
            return true;
        } finally {
            DocumentStoreLifecycleSupport.closeQuietly(documentStore);
        }
    }


    @Override
    @Transactional
    public Result<?> textSplit(DocumentCollectionSplitParams documentCollectionSplitParams) {
        try {
            DocumentCollection knowledge = knowledgeService.getById(documentCollectionSplitParams.getKnowledgeId());
            if (knowledge == null) {
                throw new BusinessException("知识库不存在");
            }
            if (knowledge.isFaqCollection()) {
                throw new BusinessException("FAQ知识库不支持文档上传");
            }
            String filePath = documentCollectionSplitParams.getFilePath();
            String fileOriginName = documentCollectionSplitParams.getFileOriginName();
            InputStream inputStream = storageService.readStream(filePath);
            Document aiDocument = new Document();
            List<DocumentChunk> previewList = new ArrayList<>();
            DocumentSplitter documentSplitter = getDocumentSplitter(documentCollectionSplitParams);
            String content = File2TextUtil.readFromStream(inputStream, fileOriginName, null);
            com.easyagents.core.document.Document document = new com.easyagents.core.document.Document(content);;
            inputStream.close();
            List<com.easyagents.core.document.Document> documents = documentSplitter.split(document);
            FlexIDKeyGenerator flexIDKeyGenerator = new FlexIDKeyGenerator();
            int sort = 1;
            for (com.easyagents.core.document.Document value : documents) {
                DocumentChunk chunk = new DocumentChunk();
                chunk.setId(new BigInteger(String.valueOf(flexIDKeyGenerator.generate(chunk, null))));
                chunk.setContent(value.getContent());
                chunk.setSorting(sort);
                sort++;
                previewList.add(chunk);
            }
            String fileTypeByExtension = FileUtil.getFileTypeByExtension(filePath);
            aiDocument.setDocumentType(fileTypeByExtension);
            aiDocument.setCollectionId(documentCollectionSplitParams.getKnowledgeId());
            aiDocument.setDocumentPath(filePath);
            aiDocument.setCreated(new Date());
            aiDocument.setModifiedBy(BigInteger.valueOf(StpUtil.getLoginIdAsLong()));
        aiDocument.setModified(new Date());
        aiDocument.setContent(document.getContent());
        aiDocument.setChunkSize(documentCollectionSplitParams.getChunkSize());
        aiDocument.setOverlapSize(documentCollectionSplitParams.getOverlapSize());
        aiDocument.setTitle(fileOriginName);
        aiDocument.setProcessStatus(DocumentProcessStatus.COMPLETED.name());
        aiDocument.setTotalChunks(previewList.size());
        aiDocument.setCompletedChunks(previewList.size());
        aiDocument.setFailedChunks(0);
        aiDocument.setProgressPercent(100);
        aiDocument.setTaskModifiedAt(new Date());
        Map<String, Object> res = new HashMap<>();

            List<DocumentChunk> documentChunks = null;
            String operation = documentCollectionSplitParams.getOperation();
            Integer pageNumber = documentCollectionSplitParams.getPageNumber();
            Integer pageSize = documentCollectionSplitParams.getPageSize();
            // 如果是预览拆分，则返回指定页的数据
            if ("textSplit".equals(operation)){
                int startIndex = (pageNumber - 1) * pageSize;
                int endIndex = Math.min(startIndex + pageSize, previewList.size());

                if (startIndex >= previewList.size()) {
                    documentChunks = new ArrayList<>();
                } else {
                    documentChunks = new ArrayList<>(previewList.subList(startIndex, endIndex));
                }

                res.put("total", previewList.size());
                // 保存文件到知识库
            } else if ("saveText".equals(operation)){
                documentChunks = previewList;
                return this.saveTextResult(documentChunks, aiDocument);
            }

            res.put("previewData", documentChunks);
            res.put("aiDocumentData", aiDocument);
            // 返回分割效果给用户
            return Result.ok(res);
        } catch (IOException e) {
            Log.error(e.toString(), e);
            return Result.fail(e.getMessage());

        }
    }

    @Override
    public Result<?> saveTextResult(List<DocumentChunk> documentChunks, Document document) {
        if (documentChunks == null || documentChunks.isEmpty()) {
            return Result.fail(1, "切割结果为空，无法保存");
        }

        List<DocumentChunk> validChunks = new ArrayList<>();
        for (DocumentChunk chunk : documentChunks) {
            if (chunk != null && StringUtil.hasText(chunk.getContent())) {
                validChunks.add(chunk);
            }
        }

        if (validChunks.isEmpty()) {
            return Result.fail(1, "切割结果无有效文本，无法进行向量化");
        }

        StoreExecutionContext storeContext = prepareStoreContext(document);
        try {
            storeDocumentChunks(storeContext, validChunks);
            persistDocumentWithChunks(document, validChunks);
            updateKnowledgeAfterStore(storeContext);
            return Result.ok();
        } catch (Exception e) {
            cleanupPersistedDocument(document);
            rollbackStoredChunks(storeContext, validChunks);
            Log.error("保存文档失败: documentId={}, title={}", document.getId(), document.getTitle(), e);
            throw new BusinessException("保存失败：" + e.getMessage());
        } finally {
            closeStoreContext(storeContext);
        }
    }

    protected Boolean storeDocument(Document entity, List<DocumentChunk> documentChunks) {
        StoreExecutionContext storeContext = prepareStoreContext(entity);
        try {
            storeDocumentChunks(storeContext, documentChunks);
            updateKnowledgeAfterStore(storeContext);
            return true;
        } finally {
            closeStoreContext(storeContext);
        }
    }

    @Override
    public Result<DocumentImportDtos.AnalyzeResponse> analyzeImport(DocumentImportDtos.AnalyzeRequest request) {
        DocumentCollection knowledge = assertDocumentCollection(request.getKnowledgeId());
        if (request.getFiles() == null || request.getFiles().isEmpty()) {
            throw new BusinessException("请先上传文件");
        }

        List<DocumentImportDtos.AnalyzeItem> items = new ArrayList<>();
        for (DocumentImportDtos.FileItem file : request.getFiles()) {
            AnalysisResult analysis = analyzeSingleFile(file.getFilePath(), file.getFileName());
            StrategyConfig strategyConfig = resolveStrategyConfig(
                    knowledge,
                    null,
                    analysis
            );

            DocumentImportDtos.AnalyzeItem item = new DocumentImportDtos.AnalyzeItem();
            item.setFilePath(file.getFilePath());
            item.setFileName(file.getFileName());
            item.setAnalysis(analysis);
            item.setStrategyConfig(strategyConfig);
            items.add(item);
        }

        DocumentImportDtos.AnalyzeResponse response = new DocumentImportDtos.AnalyzeResponse();
        response.setItems(items);
        response.setTotal(items.size());
        return Result.ok(response);
    }

    @Override
    public Result<DocumentImportDtos.PreviewResponse> previewImport(DocumentImportDtos.PreviewRequest request) {
        DocumentCollection knowledge = assertDocumentCollection(request.getKnowledgeId());
        if (request.getFiles() == null || request.getFiles().isEmpty()) {
            throw new BusinessException("请先上传文件");
        }

        List<DocumentImportDtos.PreviewFileResult> items = new ArrayList<>();
        int totalChunks = 0;
        for (DocumentImportDtos.PreviewFileRequest file : request.getFiles()) {
            DocumentImportDtos.PreviewSession session = buildPreviewSession(knowledge, file);
            String sessionId = documentImportPreviewService.put(session);

            DocumentImportDtos.PreviewFileResult item = new DocumentImportDtos.PreviewFileResult();
            item.setPreviewSessionId(sessionId);
            item.setFilePath(file.getFilePath());
            item.setFileName(file.getFileName());
            item.setNormalizedContent(session.getAnalysis() == null ? null : session.getAnalysis().getNormalizedContent());
            item.setStrategyCode(session.getStrategyConfig().getStrategyCode());
            item.setStrategyLabel(ragIngestionService.toStrategyLabel(session.getStrategyConfig().getStrategyCode()));
            item.setAnalysis(session.getAnalysis());
            item.setChunks(toPreviewChunkResults(session.getPreviewChunks()));
            item.setTotalChunks(session.getPreviewChunks().size());
            item.setTotalWarnings(countWarnings(session.getPreviewChunks()));
            items.add(item);
            totalChunks += session.getPreviewChunks().size();
        }

        DocumentImportDtos.PreviewResponse response = new DocumentImportDtos.PreviewResponse();
        response.setItems(items);
        response.setTotalFiles(items.size());
        response.setTotalChunks(totalChunks);
        return Result.ok(response);
    }

    @Override
    public Result<DocumentImportDtos.CommitResponse> commitImport(DocumentImportDtos.CommitRequest request) {
        DocumentCollection knowledge = assertDocumentCollection(request.getKnowledgeId());
        if (request.getPreviewSessionIds() == null || request.getPreviewSessionIds().isEmpty()) {
            throw new BusinessException("请选择需要提交的预览结果");
        }

        List<DocumentImportDtos.CommitFileResult> results = new ArrayList<>();
        int successCount = 0;
        int errorCount = 0;
        for (String previewSessionId : request.getPreviewSessionIds()) {
            DocumentImportDtos.CommitFileResult result = new DocumentImportDtos.CommitFileResult();
            result.setPreviewSessionId(previewSessionId);
            try {
                DocumentImportDtos.PreviewSession session = documentImportPreviewService.getRequired(previewSessionId);
                if (!Objects.equals(session.getKnowledgeId(), knowledge.getId())) {
                    throw new BusinessException("预览会话与当前知识库不匹配");
                }
                commitSingleSession(session);
                result.setSuccess(true);
                result.setFileName(session.getFileName());
                result.setDocumentId(session.getDocument().getId());
                result.setChunkCount(session.getDocumentChunks().size());
                documentImportPreviewService.remove(previewSessionId);
                successCount++;
            } catch (Exception e) {
                result.setSuccess(false);
                result.setReason(e.getMessage());
                errorCount++;
            }
            results.add(result);
        }

        DocumentImportDtos.CommitResponse response = new DocumentImportDtos.CommitResponse();
        response.setTotalFiles(results.size());
        response.setSuccessCount(successCount);
        response.setErrorCount(errorCount);
        response.setResults(results);
        return Result.ok(response);
    }

    private void commitSingleSession(DocumentImportDtos.PreviewSession session) {
        Document document = session.getDocument();
        document.setCreated(new Date());
        document.setModified(new Date());
        document.setCreatedBy(BigInteger.valueOf(StpUtil.getLoginIdAsLong()));
        document.setModifiedBy(BigInteger.valueOf(StpUtil.getLoginIdAsLong()));
        document.setProcessStatus(DocumentProcessStatus.COMPLETED.name());
        document.setTotalChunks(session.getDocumentChunks().size());
        document.setCompletedChunks(session.getDocumentChunks().size());
        document.setFailedChunks(0);
        document.setProgressPercent(100);
        document.setTaskModifiedAt(new Date());
        for (DocumentChunk chunk : session.getDocumentChunks()) {
            chunk.setDocumentId(document.getId());
            chunk.setDocumentCollectionId(document.getCollectionId());
        }

        StoreExecutionContext storeContext = prepareStoreContext(document);
        try {
            storeDocumentChunks(storeContext, session.getDocumentChunks());
            persistDocumentWithChunks(document, session.getDocumentChunks());
            updateKnowledgeAfterStore(storeContext);
        } catch (Exception e) {
            cleanupPersistedDocument(document);
            rollbackStoredChunks(storeContext, session.getDocumentChunks());
            throw new BusinessException("提交导入失败：" + e.getMessage());
        } finally {
            closeStoreContext(storeContext);
        }
    }

    private DocumentImportDtos.PreviewSession buildPreviewSession(DocumentCollection knowledge,
                                                                 DocumentImportDtos.PreviewFileRequest fileRequest) {
        AnalysisResult analysis = analyzeSingleFile(fileRequest.getFilePath(), fileRequest.getFileName());
        StrategyConfig strategyConfig = resolveStrategyConfig(knowledge, fileRequest.getStrategyConfig(), analysis);
        List<RagChunk> previewChunks = ragIngestionService.split(analysis, strategyConfig);
        if (previewChunks.isEmpty()) {
            throw new BusinessException("未生成有效分块，请调整策略后重试");
        }

        FlexIDKeyGenerator flexIDKeyGenerator = new FlexIDKeyGenerator();
        Document document = buildPreviewDocument(flexIDKeyGenerator, knowledge, fileRequest, analysis, strategyConfig);
        List<DocumentChunk> documentChunks = buildDocumentChunks(flexIDKeyGenerator, document, previewChunks);

        DocumentImportDtos.PreviewSession session = new DocumentImportDtos.PreviewSession();
        session.setKnowledgeId(knowledge.getId());
        session.setDocumentId(document.getId());
        session.setFilePath(fileRequest.getFilePath());
        session.setFileName(fileRequest.getFileName());
        session.setSourceFormat(analysis.getSourceFormat());
        session.setStrategyConfig(strategyConfig);
        session.setAnalysis(analysis);
        session.setDocument(document);
        session.setDocumentChunks(documentChunks);
        session.setPreviewChunks(previewChunks);
        session.setCreatedAt(new Date());
        return session;
    }

    private Document buildPreviewDocument(FlexIDKeyGenerator flexIDKeyGenerator,
                                          DocumentCollection knowledge,
                                          DocumentImportDtos.PreviewFileRequest fileRequest,
                                          AnalysisResult analysis,
                                          StrategyConfig strategyConfig) {
        Document document = new Document();
        document.setId(new BigInteger(String.valueOf(flexIDKeyGenerator.generate(document, null))));
        document.setCollectionId(knowledge.getId());
        document.setDocumentType(analysis.getSourceFormat());
        document.setDocumentPath(fileRequest.getFilePath());
        document.setTitle(fileRequest.getFileName());
        document.setContent(analysis.getNormalizedContent());
        document.setCreated(new Date());
        document.setModified(new Date());
        document.setModifiedBy(BigInteger.valueOf(StpUtil.getLoginIdAsLong()));

        Map<String, Object> options = new LinkedHashMap<>();
        options.put(DocumentImportKeys.KEY_DOCUMENT_STRATEGY_CODE, strategyConfig.getStrategyCode());
        options.put(DocumentImportKeys.KEY_DOCUMENT_STRATEGY_LABEL, ragIngestionService.toStrategyLabel(strategyConfig.getStrategyCode()));
        options.put(DocumentImportKeys.KEY_DOCUMENT_STRATEGY_SNAPSHOT, strategyConfigToMap(strategyConfig));
        options.put(DocumentImportKeys.KEY_DOCUMENT_ANALYSIS_SUMMARY, analysis.getFeatures());
        options.put(DocumentImportKeys.KEY_DOCUMENT_SOURCE_FILE_EXT, analysis.getSourceFormat());
        options.put(DocumentImportKeys.KEY_DOCUMENT_PREVIEW_VERSION, "v1");
        document.setOptions(options);
        return document;
    }

    private List<DocumentChunk> buildDocumentChunks(FlexIDKeyGenerator flexIDKeyGenerator,
                                                    Document document,
                                                    List<RagChunk> previewChunks) {
        List<DocumentChunk> chunks = new ArrayList<>();
        for (int i = 0; i < previewChunks.size(); i++) {
            RagChunk previewChunk = previewChunks.get(i);
            DocumentChunk chunk = new DocumentChunk();
            chunk.setId(new BigInteger(String.valueOf(flexIDKeyGenerator.generate(chunk, null))));
            chunk.setDocumentId(document.getId());
            chunk.setDocumentCollectionId(document.getCollectionId());
            chunk.setContent(previewChunk.getContent());
            chunk.setSorting(i + 1);

            Map<String, Object> options = new LinkedHashMap<>(previewChunk.getOptions());
            options.put("chunkType", previewChunk.getChunkType());
            options.put("sourceLabel", previewChunk.getSourceLabel());
            options.put("headingPath", previewChunk.getHeadingPath());
            options.put("charCount", previewChunk.getCharCount());
            options.put("tokenEstimate", previewChunk.getTokenEstimate());
            options.put("qaQuestion", previewChunk.getQuestion());
            options.put("qaAnswer", previewChunk.getAnswer());
            options.put("partNo", previewChunk.getPartNo());
            options.put("partTotal", previewChunk.getPartTotal());
            options.put("warnings", previewChunk.getWarnings());
            chunk.setOptions(options);
            chunks.add(chunk);
        }
        return chunks;
    }

    private AnalysisResult analyzeSingleFile(String filePath, String fileName) {
        String fileExt = normalizeFileExtension(fileName, filePath);
        assertSupportedImportFile(fileExt);
        String content = readFileContent(filePath, fileName);
        return ragIngestionService.analyze(content, fileExt);
    }

    private String readFileContent(String filePath, String fileName) {
        try (InputStream inputStream = storageService.readStream(filePath)) {
            return File2TextUtil.readFromStream(inputStream, fileName, null);
        } catch (IOException e) {
            Log.error("读取导入文件失败: filePath={}, fileName={}", filePath, fileName, e);
            throw new BusinessException("文件解析失败：" + e.getMessage());
        }
    }

    private void assertSupportedImportFile(String fileExt) {
        if (!Arrays.asList("pdf", "docx", "txt", "md").contains(fileExt)) {
            throw new BusinessException("当前仅支持 pdf/docx/txt/md 文档导入");
        }
    }

    private String normalizeFileExtension(String fileName, String filePath) {
        String target = StringUtil.hasText(fileName) ? fileName : filePath;
        String ext = FileUtil.getFileTypeByExtension(target);
        return ext == null ? "" : ext.toLowerCase(Locale.ROOT);
    }

    private DocumentCollection assertDocumentCollection(BigInteger knowledgeId) {
        DocumentCollection knowledge = knowledgeService.getById(knowledgeId);
        if (knowledge == null) {
            throw new BusinessException("知识库不存在");
        }
        if (knowledge.isFaqCollection()) {
            throw new BusinessException("FAQ知识库不支持文档上传");
        }
        return knowledge;
    }

    private StrategyConfig resolveStrategyConfig(DocumentCollection knowledge,
                                                StrategyConfig requestConfig,
                                                AnalysisResult analysisResult) {
        Map<String, Object> options = knowledge.getOptions() == null
                ? Collections.emptyMap()
                : knowledge.getOptions();
        String recommended = analysisResult.getRecommendedStrategyCode();
        String defaultStrategyCode = asString(options.get(DocumentImportKeys.KEY_SPLITTER_DEFAULT_STRATEGY));
        String fallbackStrategyCode = asString(options.get(DocumentImportKeys.KEY_SPLITTER_FALLBACK_STRATEGY));
        Boolean autoRecommendEnabled = asBoolean(options.get(DocumentImportKeys.KEY_SPLITTER_AUTO_RECOMMEND_ENABLED), true);

        StrategyConfig config = readProfileConfig(options, defaultStrategyCode);
        if (config == null) {
            config = StrategyConfig.defaults();
        }
        String requestedStrategyCode = requestConfig == null ? null : requestConfig.getStrategyCode();

        String strategyCode = StringUtil.hasText(requestedStrategyCode)
                ? requestedStrategyCode
                : config.getStrategyCode();
        if (!StringUtil.hasText(strategyCode) || RagStrategyCodes.AUTO.equals(strategyCode)) {
            strategyCode = Boolean.TRUE.equals(autoRecommendEnabled)
                    ? recommended
                    : (StringUtil.hasText(defaultStrategyCode) ? defaultStrategyCode : recommended);
        }
        if (!StringUtil.hasText(strategyCode)) {
            strategyCode = StringUtil.hasText(fallbackStrategyCode)
                    ? fallbackStrategyCode
                    : RagStrategyCodes.PARAGRAPH_LENGTH;
        }

        StrategyConfig profileConfig = readProfileConfig(options, strategyCode);
        if (profileConfig != null) {
            mergeStrategyConfig(config, profileConfig);
        }
        if (requestConfig != null) {
            mergeStrategyConfig(config, requestConfig);
        }
        config.setStrategyCode(strategyCode);
        if (config.getChunkSize() == null || config.getChunkSize() <= 0) {
            config.setChunkSize(RagDefaults.CHUNK_SIZE);
        }
        if (config.getOverlapSize() == null || config.getOverlapSize() < 0) {
            config.setOverlapSize(RagDefaults.OVERLAP_SIZE);
        }
        if (config.getMdSplitterLevel() == null || config.getMdSplitterLevel() <= 0) {
            config.setMdSplitterLevel(RagDefaults.MD_SPLITTER_LEVEL);
        }
        return config;
    }

    @SuppressWarnings("unchecked")
    private StrategyConfig readProfileConfig(Map<String, Object> options, String strategyCode) {
        if (!StringUtil.hasText(strategyCode)) {
            return null;
        }
        Object profileObject = options.get(DocumentImportKeys.KEY_SPLITTER_STRATEGY_PROFILES);
        if (!(profileObject instanceof Map)) {
            return null;
        }
        Object strategyObject = ((Map<String, Object>) profileObject).get(strategyCode);
        if (!(strategyObject instanceof Map)) {
            return null;
        }
        Map<String, Object> rawProfile = (Map<String, Object>) strategyObject;
        StrategyConfig config = StrategyConfig.defaults();
        config.setStrategyCode(strategyCode);
        config.setChunkSize(asInteger(rawProfile.get("chunkSize"), config.getChunkSize()));
        config.setOverlapSize(asInteger(rawProfile.get("overlapSize"), config.getOverlapSize()));
        config.setRegex(asString(rawProfile.get("regex")));
        config.setRowsPerChunk(asInteger(rawProfile.get("rowsPerChunk"), config.getRowsPerChunk()));
        config.setMdSplitterLevel(asInteger(rawProfile.get("mdSplitterLevel"), config.getMdSplitterLevel()));
        return config;
    }

    private void mergeStrategyConfig(StrategyConfig target, StrategyConfig source) {
        if (source == null) {
            return;
        }
        if (StringUtil.hasText(source.getStrategyCode())) {
            target.setStrategyCode(source.getStrategyCode());
        }
        if (source.getChunkSize() != null) {
            target.setChunkSize(source.getChunkSize());
        }
        if (source.getOverlapSize() != null) {
            target.setOverlapSize(source.getOverlapSize());
        }
        if (StringUtil.hasText(source.getRegex())) {
            target.setRegex(source.getRegex());
        }
        if (source.getRowsPerChunk() != null) {
            target.setRowsPerChunk(source.getRowsPerChunk());
        }
        if (source.getMdSplitterLevel() != null) {
            target.setMdSplitterLevel(source.getMdSplitterLevel());
        }
    }

    private Map<String, Object> strategyConfigToMap(StrategyConfig strategyConfig) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("strategyCode", strategyConfig.getStrategyCode());
        map.put("chunkSize", strategyConfig.getChunkSize());
        map.put("overlapSize", strategyConfig.getOverlapSize());
        map.put("regex", strategyConfig.getRegex());
        map.put("rowsPerChunk", strategyConfig.getRowsPerChunk());
        map.put("mdSplitterLevel", strategyConfig.getMdSplitterLevel());
        return map;
    }

    private int countWarnings(List<RagChunk> chunks) {
        int total = 0;
        for (RagChunk chunk : chunks) {
            total += chunk.getWarnings() == null ? 0 : chunk.getWarnings().size();
        }
        return total;
    }

    private List<DocumentImportDtos.PreviewChunkResult> toPreviewChunkResults(List<RagChunk> chunks) {
        List<DocumentImportDtos.PreviewChunkResult> result = new ArrayList<>();
        if (chunks == null) {
            return result;
        }
        for (RagChunk chunk : chunks) {
            DocumentImportDtos.PreviewChunkResult item = new DocumentImportDtos.PreviewChunkResult();
            item.setAnswer(chunk.getAnswer());
            item.setCharCount(chunk.getCharCount());
            item.setChunkId(chunk.getChunkId());
            item.setChunkType(chunk.getChunkType());
            item.setContent(chunk.getContent());
            item.setHeadingPath(chunk.getHeadingPath() == null ? new ArrayList<>() : new ArrayList<>(chunk.getHeadingPath()));
            item.setPartNo(chunk.getPartNo());
            item.setPartTotal(chunk.getPartTotal());
            item.setQuestion(chunk.getQuestion());
            item.setSourceLabel(chunk.getSourceLabel());
            item.setTokenEstimate(chunk.getTokenEstimate());
            item.setWarnings(chunk.getWarnings() == null ? new ArrayList<>() : new ArrayList<>(chunk.getWarnings()));
            item.setSourceRanges(copySourceRanges(chunk));
            result.add(item);
        }
        return result;
    }

    @SuppressWarnings("unchecked")
    private List<DocumentImportDtos.PreviewSourceRange> copySourceRanges(RagChunk chunk) {
        List<DocumentImportDtos.PreviewSourceRange> result = new ArrayList<>();
        if (chunk == null || chunk.getOptions() == null) {
            return result;
        }
        Object rawRanges = chunk.getOptions().get(SOURCE_RANGES_KEY);
        if (!(rawRanges instanceof List<?> rangeList)) {
            return result;
        }
        for (Object item : rangeList) {
            if (!(item instanceof Map<?, ?> rangeMap)) {
                continue;
            }
            DocumentImportDtos.PreviewSourceRange range = new DocumentImportDtos.PreviewSourceRange();
            range.setStart(asInteger(rangeMap.get("start"), null));
            range.setEnd(asInteger(rangeMap.get("end"), null));
            if (range.getStart() != null && range.getEnd() != null) {
                result.add(range);
            }
        }
        return result;
    }

    private StoreExecutionContext prepareStoreContext(Document entity) {
        DocumentCollection knowledge = knowledgeService.getById(entity.getCollectionId());
        if (knowledge == null) {
            throw new BusinessException("知识库不存在");
        }
        if (knowledge.isFaqCollection()) {
            throw new BusinessException("FAQ知识库不支持文档上传");
        }

        DocumentStore documentStore;
        try {
            documentStore = knowledge.toDocumentStore();
        } catch (Exception e) {
            Log.error("向量库配置错误: knowledgeId={}", knowledge.getId(), e);
            throw new BusinessException("向量数据库配置错误");
        }
        if (documentStore == null) {
            throw new BusinessException("向量数据库配置错误");
        }
        try {
            Model model = modelService.getModelInstance(knowledge.getVectorEmbedModelId());
            if (model == null) {
                throw new BusinessException("该知识库未配置大模型");
            }
            EmbeddingModel embeddingModel = model.toEmbeddingModel();
            documentStore.setEmbeddingModel(embeddingModel);

            StoreOptions options = StoreOptions.ofCollectionName(knowledge.getVectorStoreCollection());
            EmbeddingOptions embeddingOptions = new EmbeddingOptions();
            embeddingOptions.setModel(model.getModelName());
            embeddingOptions.setDimensions(knowledge.getDimensionOfVectorModel());
            options.setEmbeddingOptions(embeddingOptions);
            options.setIndexName(options.getCollectionName());

            DocumentSearcher searcher = null;
            searcher = searcherFactory.getSearcher();
            return new StoreExecutionContext(knowledge, model, embeddingModel, documentStore, options, searcher);
        } catch (RuntimeException e) {
            DocumentStoreLifecycleSupport.closeQuietly(documentStore);
            throw e;
        }
    }

    private void storeDocumentChunks(StoreExecutionContext storeContext, List<DocumentChunk> documentChunks) {
        List<com.easyagents.core.document.Document> documents = new ArrayList<>();
        for (DocumentChunk item : documentChunks) {
            com.easyagents.core.document.Document document = new com.easyagents.core.document.Document();
            document.setId(item.getId());
            document.setContent(item.getContent());
            document.addMetadata(KeywordSearchMetadataKeys.KNOWLEDGE_ID,
                storeContext.knowledge.getId() == null ? null : storeContext.knowledge.getId().toString());
            documents.add(document);
        }

        StoreResult result;
        try {
            result = storeContext.documentStore.store(documents, storeContext.options);
        } catch (Exception e) {
            Log.error("Vector store failed: knowledgeId={}, collection={}, chunkCount={}",
                    storeContext.knowledge.getId(),
                    storeContext.options.getCollectionName(),
                    documents.size(),
                    e);
            throw new BusinessException("向量过程中发生错误，错误信息为：" + e.getMessage());
        }
        if (result == null || !result.isSuccess()) {
            Log.error("DocumentStore.store failed: {}", result);
            throw new BusinessException("DocumentStore.store failed");
        }

        if (storeContext.searcher != null) {
            for (com.easyagents.core.document.Document document : documents) {
                storeContext.searcher.addDocument(document);
            }
        }
    }

    private void rollbackStoredChunks(StoreExecutionContext storeContext, List<DocumentChunk> documentChunks) {
        try {
            List<BigInteger> chunkIds = new ArrayList<>();
            for (DocumentChunk chunk : documentChunks) {
                chunkIds.add(chunk.getId());
            }
            storeContext.documentStore.delete(chunkIds, storeContext.options);
            if (storeContext.searcher != null) {
                for (BigInteger chunkId : chunkIds) {
                    storeContext.searcher.deleteDocument(chunkId);
                }
            }
        } catch (Exception e) {
            Log.error("回滚向量文档失败: knowledgeId={}", storeContext.knowledge.getId(), e);
        }
    }

    private void updateKnowledgeAfterStore(StoreExecutionContext storeContext) {
        DocumentCollection documentCollection = new DocumentCollection();
        documentCollection.setId(storeContext.knowledge.getId());
        Map<String, Object> knowledgeOptions = storeContext.knowledge.getOptions() == null
                ? new HashMap<>()
                : new HashMap<>(storeContext.knowledge.getOptions());
        knowledgeOptions.put(KEY_CAN_UPDATE_EMBEDDING_MODEL, false);
        documentCollection.setOptions(knowledgeOptions);
        knowledgeService.updateById(documentCollection);

        if (storeContext.knowledge.getDimensionOfVectorModel() == null) {
            int dimension = Model.getEmbeddingDimension(storeContext.embeddingModel);
            DocumentCollection update = new DocumentCollection();
            update.setId(storeContext.knowledge.getId());
            update.setDimensionOfVectorModel(dimension);
            knowledgeService.updateById(update);
        }
    }

    private void closeStoreContext(StoreExecutionContext storeContext) {
        if (storeContext == null) {
            return;
        }
        DocumentStoreLifecycleSupport.closeQuietly(storeContext.documentStore);
    }

    private void persistDocumentWithChunks(Document document, List<DocumentChunk> chunks) {
        this.getMapper().insert(document);
        AtomicInteger sort = new AtomicInteger(1);
        for (DocumentChunk item : chunks) {
            item.setDocumentCollectionId(document.getCollectionId());
            item.setDocumentId(document.getId());
            item.setSorting(sort.getAndIncrement());
            documentChunkService.save(item);
        }
    }

    private void cleanupPersistedDocument(Document document) {
        if (document == null || document.getId() == null) {
            return;
        }
        documentChunkMapper.deleteByQuery(QueryWrapper.create().eq(DocumentChunk::getDocumentId, document.getId()));
        this.getMapper().deleteById(document.getId());
    }

    private String asString(Object value) {
        return value == null ? null : String.valueOf(value);
    }

    private Integer asInteger(Object value, Integer defaultValue) {
        if (value == null) {
            return defaultValue;
        }
        if (value instanceof Number) {
            return ((Number) value).intValue();
        }
        if (value instanceof String && StringUtil.hasText((String) value)) {
            return Integer.parseInt((String) value);
        }
        return defaultValue;
    }

    private Boolean asBoolean(Object value, boolean defaultValue) {
        if (value == null) {
            return defaultValue;
        }
        if (value instanceof Boolean) {
            return (Boolean) value;
        }
        if (value instanceof Number) {
            return ((Number) value).intValue() != 0;
        }
        return Boolean.parseBoolean(String.valueOf(value));
    }

    private static class StoreExecutionContext {
        private final DocumentCollection knowledge;
        private final Model model;
        private final EmbeddingModel embeddingModel;
        private final DocumentStore documentStore;
        private final StoreOptions options;
        private final DocumentSearcher searcher;

        private StoreExecutionContext(DocumentCollection knowledge,
                                      Model model,
                                      EmbeddingModel embeddingModel,
                                      DocumentStore documentStore,
                                      StoreOptions options,
                                      DocumentSearcher searcher) {
            this.knowledge = knowledge;
            this.model = model;
            this.embeddingModel = embeddingModel;
            this.documentStore = documentStore;
            this.options = options;
            this.searcher = searcher;
        }
    }

    public DocumentSplitter getDocumentSplitter(DocumentCollectionSplitParams params) {
        String splitterName = params.getSplitterName();
        int chunkSize = params.getChunkSize();
        int overlapSize = params.getOverlapSize();
        String regex = params.getRegex();
        int excelRows = params.getRowsPerChunk();
        if (StringUtil.noText(splitterName)) {
            return null;
        }
        switch (splitterName) {
            case "SimpleDocumentSplitter":
                return new SimpleDocumentSplitter(chunkSize, overlapSize);
            case "RegexDocumentSplitter":
                return new RegexDocumentSplitter(regex);
            case "SimpleTokenizeSplitter":
                if (overlapSize == 0) {
                    return new SimpleTokenizeSplitter(chunkSize);
                } else {
                    return new SimpleTokenizeSplitter(chunkSize, overlapSize);
                }
            case "ExcelDocumentSplitter":
                return new ExcelDocumentSplitter(excelRows);
            case "MarkdownHeaderSplitter":
                return new MarkdownHeaderSplitter(params.getMdSplitterLevel());
            default:
                return null;
        }

    }

    public static String getFileExtension(String filePath) {
        int lastDotIndex = filePath.lastIndexOf('.');
        if (lastDotIndex != -1) {
            return filePath.substring(lastDotIndex + 1);
        }
        return null;
    }

    public static String getFileName(String filePath) {
        int lastDotIndex = filePath.lastIndexOf('.');
        if (lastDotIndex != -1) {
            return filePath.substring(lastDotIndex + 1);
        }
        return null;
    }

    @Override
    public Result<DocumentImportDtos.TaskCreateResponse> createImportTask(DocumentImportDtos.TaskCreateRequest request) {
        return importTaskAppService.createImportTask(request);
    }

    @Override
    public Result<DocumentImportDtos.TaskDetailResponse> getImportTaskDetail(BigInteger taskId) {
        return importTaskAppService.getImportTaskDetail(taskId);
    }

    @Override
    public Result<DocumentImportDtos.PreviewResponse> previewImportTask(DocumentImportDtos.PreviewRequest request) {
        return importTaskAppService.previewImportTask(request);
    }

    @Override
    public Result<DocumentImportDtos.TaskStartIndexResponse> startIndexTask(DocumentImportDtos.TaskStartIndexRequest request) {
        return importTaskAppService.startIndexTask(request);
    }

    @Override
    public Result<DocumentImportDtos.TaskStartIndexResponse> retryParseTask(DocumentImportDtos.TaskRetryRequest request) {
        return importTaskAppService.retryParseTask(request);
    }

    @Override
    public Result<DocumentImportDtos.TaskStartIndexResponse> retryIndexTask(DocumentImportDtos.TaskRetryRequest request) {
        return importTaskAppService.retryIndexTask(request);
    }
}
