package tech.easyflow.ai.service.impl;

import cn.dev33.satoken.stp.StpUtil;
import cn.idev.excel.EasyExcel;
import cn.idev.excel.ExcelWriter;
import cn.idev.excel.FastExcel;
import cn.idev.excel.context.AnalysisContext;
import cn.idev.excel.metadata.data.ReadCellData;
import cn.idev.excel.read.listener.ReadListener;
import cn.idev.excel.write.metadata.WriteSheet;
import cn.idev.excel.write.style.column.SimpleColumnWidthStyleStrategy;
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.search.engine.service.DocumentSearcher;
import com.easyagents.search.engine.service.KeywordSearchMetadataKeys;
import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.spring.service.impl.ServiceImpl;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Element;
import org.jsoup.safety.Safelist;
import org.jsoup.select.Elements;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.web.multipart.MultipartFile;
import tech.easyflow.ai.config.SearcherFactory;
import tech.easyflow.ai.entity.DocumentCollection;
import tech.easyflow.ai.entity.FaqItem;
import tech.easyflow.ai.entity.Model;
import tech.easyflow.ai.mapper.FaqItemMapper;
import tech.easyflow.ai.service.DocumentCollectionService;
import tech.easyflow.ai.service.FaqCategoryService;
import tech.easyflow.ai.service.FaqItemService;
import tech.easyflow.ai.service.ModelService;
import tech.easyflow.ai.support.DocumentStoreLifecycleSupport;
import tech.easyflow.ai.vo.FaqImportErrorRowVo;
import tech.easyflow.ai.vo.FaqImportResultVo;
import tech.easyflow.common.util.StringUtil;
import tech.easyflow.common.web.exceptions.BusinessException;

import javax.annotation.Resource;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigInteger;
import java.util.*;
import java.util.function.Supplier;

import static tech.easyflow.ai.entity.DocumentCollection.KEY_CAN_UPDATE_EMBEDDING_MODEL;

@Service
public class FaqItemServiceImpl extends ServiceImpl<FaqItemMapper, FaqItem> implements FaqItemService {

    private static final Logger LOG = LoggerFactory.getLogger(FaqItemServiceImpl.class);
    private static final Safelist ANSWER_SAFE_LIST = Safelist.basic()
            .addTags("h1", "h2", "h3", "h4", "h5", "h6", "img")
            .addAttributes("img", "src", "alt", "title", "width", "height")
            .addProtocols("img", "src", "http", "https");
    private static final String OPTION_KEY_IMAGES = "images";
    private static final String IMAGE_FIELD_URL = "url";
    private static final String IMAGE_FIELD_ALT = "alt";
    private static final String IMAGE_FIELD_TITLE = "title";
    private static final String IMPORT_HEAD_CATEGORY_PATH = "分类路径";
    private static final String IMPORT_HEAD_QUESTION = "问题";
    private static final String IMPORT_HEAD_ANSWER_HTML = "答案HTML";
    private static final String DEFAULT_CATEGORY_NAME = "默认分类";
    private static final String IMPORT_UPSERT_KEY_SEPARATOR = "\u0001";
    private static final int MAX_IMPORT_ROWS = 5000;
    private static final long MAX_IMPORT_FILE_SIZE_BYTES = 10L * 1024L * 1024L;
    private static final int TEMPLATE_COLUMN_WIDTH = 30;
    private static final int TEMPLATE_NOTE_COLUMN_WIDTH = 90;

    @Resource
    private DocumentCollectionService documentCollectionService;

    @Resource
    private ModelService modelService;

    @Resource
    private FaqCategoryService faqCategoryService;

    @Autowired
    private SearcherFactory searcherFactory;

    @Resource
    private PlatformTransactionManager transactionManager;

    @Override
    @Transactional
    public boolean saveFaqItem(FaqItem entity) {
        checkAndNormalize(entity, true, null);
        DocumentCollection collection = getFaqCollection(entity.getCollectionId());
        entity.setCategoryId(faqCategoryService.ensureFaqItemCategory(entity.getCollectionId(), entity.getCategoryId()));

        Date now = new Date();
        BigInteger userId = getCurrentUserId();
        entity.setCreated(now);
        entity.setModified(now);
        entity.setCreatedBy(userId);
        entity.setModifiedBy(userId);
        if (entity.getOrderNo() == null) {
            entity.setOrderNo(nextOrderNo(entity.getCollectionId()));
        }

        boolean success = save(entity);
        if (!success) {
            return false;
        }

        storeToVector(collection, entity, false);
        return true;
    }

    @Override
    @Transactional
    public boolean updateFaqItem(FaqItem entity) {
        if (entity.getId() == null) {
            throw new BusinessException("FAQ条目ID不能为空");
        }
        FaqItem old = getById(entity.getId());
        if (old == null) {
            throw new BusinessException("FAQ条目不存在");
        }
        if (entity.getCollectionId() == null) {
            entity.setCollectionId(old.getCollectionId());
        }
        checkAndNormalize(entity, false, old.getOptions());

        DocumentCollection collection = getFaqCollection(old.getCollectionId());
        old.setCategoryId(faqCategoryService.ensureFaqItemCategory(old.getCollectionId(), entity.getCategoryId()));
        old.setQuestion(entity.getQuestion());
        old.setAnswerHtml(entity.getAnswerHtml());
        old.setAnswerText(entity.getAnswerText());
        if (entity.getOrderNo() != null) {
            old.setOrderNo(entity.getOrderNo());
        }
        old.setModified(new Date());
        old.setModifiedBy(getCurrentUserId());
        old.setOptions(entity.getOptions());

        boolean success = updateById(old);
        if (!success) {
            return false;
        }

        storeToVector(collection, old, true);
        return true;
    }

    @Override
    @Transactional
    public boolean removeFaqItem(BigInteger id) {
        FaqItem old = getById(id);
        if (old == null) {
            throw new BusinessException("FAQ条目不存在");
        }
        DocumentCollection collection = getFaqCollection(old.getCollectionId());
        removeFromVector(collection, old);
        return removeById(id);
    }

    @Override
    public FaqImportResultVo importFromExcel(BigInteger collectionId, MultipartFile file) {
        validateImportFile(file);
        getFaqCollection(collectionId);

        BigInteger defaultCategoryId = faqCategoryService.ensureDefaultCategory(collectionId);
        Map<BigInteger, String> categoryPathMap = faqCategoryService.buildPathMap(collectionId);
        String defaultCategoryPath = normalizeCategoryPath(categoryPathMap.get(defaultCategoryId));
        if (!StringUtil.hasText(defaultCategoryPath)) {
            defaultCategoryPath = DEFAULT_CATEGORY_NAME;
        }

        Map<String, BigInteger> categoryPathIdMap = buildCategoryPathIdMap(categoryPathMap);
        categoryPathIdMap.putIfAbsent(defaultCategoryPath, defaultCategoryId);

        Map<String, FaqItem> existingFaqMap = buildExistingFaqMap(collectionId, categoryPathMap, defaultCategoryPath);
        int nextOrderNo = nextOrderNo(collectionId);
        TransactionTemplate rowTransactionTemplate = createImportRowTransactionTemplate();

        List<FaqImportRow> importRows = parseExcelRows(file);
        FaqImportResultVo result = new FaqImportResultVo();
        result.setTotalCount(importRows.size());

        for (FaqImportRow row : importRows) {
            String question = trimToNull(row.getQuestion());
            String answerHtml = trimToNull(row.getAnswerHtml());
            if (!StringUtil.hasText(question)) {
                appendImportError(result, row, "问题不能为空");
                continue;
            }
            if (!StringUtil.hasText(answerHtml)) {
                appendImportError(result, row, "答案HTML不能为空");
                continue;
            }

            String inputPath = normalizeCategoryPath(row.getCategoryPath());
            String resolvedCategoryPath = StringUtil.hasText(inputPath) ? inputPath : defaultCategoryPath;
            BigInteger resolvedCategoryId = categoryPathIdMap.get(resolvedCategoryPath);
            if (resolvedCategoryId == null) {
                resolvedCategoryId = defaultCategoryId;
                resolvedCategoryPath = defaultCategoryPath;
            }
            String upsertKey = buildImportUpsertKey(resolvedCategoryPath, question);
            FaqItem matchedFaq = existingFaqMap.get(upsertKey);
            Integer targetOrderNo = matchedFaq == null ? nextOrderNo : matchedFaq.getOrderNo();
            if (matchedFaq == null) {
                nextOrderNo++;
            }

            try {
                if (matchedFaq == null) {
                    FaqItem saveEntity = new FaqItem();
                    saveEntity.setCollectionId(collectionId);
                    saveEntity.setCategoryId(resolvedCategoryId);
                    saveEntity.setQuestion(question);
                    saveEntity.setAnswerHtml(answerHtml);
                    saveEntity.setOrderNo(targetOrderNo);
                    executeInRowTransaction(rowTransactionTemplate, () -> saveFaqItem(saveEntity));
                    existingFaqMap.put(upsertKey, saveEntity);
                } else {
                    FaqItem updateEntity = new FaqItem();
                    updateEntity.setId(matchedFaq.getId());
                    updateEntity.setCollectionId(collectionId);
                    updateEntity.setCategoryId(resolvedCategoryId);
                    updateEntity.setQuestion(question);
                    updateEntity.setAnswerHtml(answerHtml);
                    updateEntity.setOrderNo(targetOrderNo);
                    executeInRowTransaction(rowTransactionTemplate, () -> updateFaqItem(updateEntity));
                    matchedFaq.setCategoryId(resolvedCategoryId);
                    matchedFaq.setQuestion(question);
                    matchedFaq.setAnswerHtml(answerHtml);
                    matchedFaq.setOrderNo(targetOrderNo);
                    existingFaqMap.put(upsertKey, matchedFaq);
                }
                result.setSuccessCount(result.getSuccessCount() + 1);
            } catch (Exception e) {
                appendImportError(result, row, extractImportErrorMessage(e));
            }
        }

        result.setErrorCount(result.getErrorRows().size());
        return result;
    }

    @Override
    public void exportToExcel(BigInteger collectionId, OutputStream outputStream) {
        getFaqCollection(collectionId);
        faqCategoryService.ensureDefaultCategory(collectionId);
        Map<BigInteger, String> categoryPathMap = faqCategoryService.buildPathMap(collectionId);
        List<FaqItem> faqItems = list(QueryWrapper.create()
                .eq(FaqItem::getCollectionId, collectionId)
                .orderBy("order_no asc, id asc"));
        List<List<String>> rows = new ArrayList<>();
        for (FaqItem faqItem : faqItems) {
            List<String> row = new ArrayList<>(3);
            row.add(defaultIfNull(normalizeCategoryPath(categoryPathMap.get(faqItem.getCategoryId())), DEFAULT_CATEGORY_NAME));
            row.add(defaultIfNull(faqItem.getQuestion(), ""));
            row.add(defaultIfNull(faqItem.getAnswerHtml(), ""));
            rows.add(row);
        }
        EasyExcel.write(outputStream)
                .head(buildImportHeadList())
                .registerWriteHandler(new SimpleColumnWidthStyleStrategy(TEMPLATE_COLUMN_WIDTH))
                .sheet("FAQ")
                .doWrite(rows);
    }

    @Override
    public void writeImportTemplate(OutputStream outputStream) {
        ExcelWriter excelWriter = EasyExcel.write(outputStream).build();
        try {
            WriteSheet templateSheet = EasyExcel.writerSheet(0, "模板")
                    .head(buildImportHeadList())
                    .registerWriteHandler(new SimpleColumnWidthStyleStrategy(TEMPLATE_COLUMN_WIDTH))
                    .build();
            excelWriter.write(new ArrayList<>(), templateSheet);

            WriteSheet instructionSheet = EasyExcel.writerSheet(1, "填写说明")
                    .head(Collections.singletonList(Collections.singletonList("规则说明")))
                    .registerWriteHandler(new SimpleColumnWidthStyleStrategy(TEMPLATE_NOTE_COLUMN_WIDTH))
                    .build();
            excelWriter.write(buildImportInstructionRows(), instructionSheet);
        } finally {
            excelWriter.finish();
        }
    }

    private void checkAndNormalize(FaqItem entity, boolean isSave, Map<String, Object> baseOptions) {
        if (entity == null) {
            throw new BusinessException("FAQ条目不能为空");
        }
        if (entity.getCollectionId() == null) {
            throw new BusinessException("知识库ID不能为空");
        }
        if (StringUtil.noText(entity.getQuestion())) {
            throw new BusinessException("问题不能为空");
        }
        if (StringUtil.noText(entity.getAnswerHtml())) {
            throw new BusinessException("答案不能为空");
        }
        if (isSave && entity.getId() != null) {
            throw new BusinessException("新增FAQ条目不允许传入ID");
        }

        entity.setQuestion(entity.getQuestion().trim());
        String cleanHtml = Jsoup.clean(entity.getAnswerHtml(), ANSWER_SAFE_LIST);
        String answerText = Jsoup.parse(cleanHtml).text();
        List<Map<String, String>> images = extractImages(cleanHtml);
        if (StringUtil.noText(answerText) && images.isEmpty()) {
            throw new BusinessException("答案不能为空");
        }
        Map<String, Object> normalizedOptions = baseOptions == null
                ? new HashMap<>()
                : new HashMap<>(baseOptions);
        if (entity.getOptions() != null) {
            normalizedOptions.putAll(entity.getOptions());
        }
        if (images.isEmpty()) {
            normalizedOptions.remove(OPTION_KEY_IMAGES);
        } else {
            normalizedOptions.put(OPTION_KEY_IMAGES, images);
        }
        entity.setAnswerHtml(cleanHtml);
        entity.setAnswerText(answerText);
        entity.setOptions(normalizedOptions);
    }

    private DocumentCollection getFaqCollection(BigInteger collectionId) {
        if (collectionId == null) {
            throw new BusinessException("知识库ID不能为空");
        }
        DocumentCollection collection = documentCollectionService.getById(collectionId);
        if (collection == null) {
            throw new BusinessException("知识库不存在");
        }
        if (!collection.isFaqCollection()) {
            throw new BusinessException("当前知识库不是FAQ类型");
        }
        return collection;
    }

    private void storeToVector(DocumentCollection collection, FaqItem entity, boolean isUpdate) {
        PreparedStore preparedStore = prepareStore(collection);
        try {
            com.easyagents.core.document.Document doc = toSearchDocument(entity);
            StoreResult result = isUpdate
                    ? preparedStore.documentStore.update(doc, preparedStore.storeOptions)
                    : preparedStore.documentStore.store(Collections.singletonList(doc), preparedStore.storeOptions);
            if (result == null || !result.isSuccess()) {
                throw new BusinessException("FAQ向量化失败");
            }

            DocumentSearcher searcher = searcherFactory.getSearcher();
            if (searcher != null) {
                if (isUpdate) {
                    searcher.deleteDocument(entity.getId());
                }
                searcher.addDocument(doc);
            }
            markCollectionEmbedded(collection, preparedStore.embeddingModel);
        } catch (BusinessException e) {
            throw e;
        } catch (RuntimeException e) {
            LOG.error("FAQ vectorization failed. collectionId={}, faqId={}, isUpdate={}",
                    collection == null ? null : collection.getId(),
                    entity == null ? null : entity.getId(),
                    isUpdate,
                    e);
            throw new BusinessException("FAQ向量化失败：请检查知识库绑定的向量模型、请求路径、维度或向量库配置");
        } finally {
            DocumentStoreLifecycleSupport.closeQuietly(preparedStore.documentStore);
        }
    }

    private void removeFromVector(DocumentCollection collection, FaqItem entity) {
        PreparedStore preparedStore = prepareStore(collection);
        try {
            boolean deleteSuccess = deleteFromVectorStore(preparedStore.documentStore, preparedStore.storeOptions, entity.getId());
            if (!deleteSuccess) {
                throw new BusinessException("FAQ向量删除失败");
            }
        } finally {
            DocumentStoreLifecycleSupport.closeQuietly(preparedStore.documentStore);
        }

        DocumentSearcher searcher = searcherFactory.getSearcher();
        if (searcher != null) {
            boolean removed = searcher.deleteDocument(entity.getId());
            if (!removed) {
                LOG.warn("Delete faq search index failed. faqId={}", entity.getId());
            }
        }
    }

    private boolean deleteFromVectorStore(DocumentStore documentStore, StoreOptions storeOptions, BigInteger id) {
        List<Object> candidates = new ArrayList<>(3);
        candidates.add(id);
        candidates.add(String.valueOf(id));
        try {
            candidates.add(id.longValueExact());
        } catch (ArithmeticException ignored) {
            // 非 long 范围不追加 long 类型候选
        }

        for (Object candidate : candidates) {
            StoreResult deleteResult = documentStore.delete(Collections.singletonList(candidate), storeOptions);
            if (deleteResult != null && deleteResult.isSuccess()) {
                return true;
            }
            LOG.warn("Delete faq vector by id candidate failed. faqId={}, candidateType={}, candidate={}",
                    id, candidate == null ? "null" : candidate.getClass().getSimpleName(), candidate);
        }
        return false;
    }

    private PreparedStore prepareStore(DocumentCollection collection) {
        DocumentStore documentStore;
        try {
            documentStore = collection.toDocumentStore();
        } catch (Exception e) {
            throw new BusinessException("向量数据库配置错误");
        }
        if (documentStore == null) {
            throw new BusinessException("向量数据库配置错误");
        }
        try {
            Model model = modelService.getModelInstance(collection.getVectorEmbedModelId());
            if (model == null) {
                throw new BusinessException("该知识库未配置向量模型");
            }
            EmbeddingModel embeddingModel = model.toEmbeddingModel();
            documentStore.setEmbeddingModel(embeddingModel);

            StoreOptions options = StoreOptions.ofCollectionName(collection.getVectorStoreCollection());
            EmbeddingOptions embeddingOptions = new EmbeddingOptions();
            embeddingOptions.setModel(model.getModelName());
            embeddingOptions.setDimensions(collection.getDimensionOfVectorModel());
            options.setEmbeddingOptions(embeddingOptions);
            options.setIndexName(options.getCollectionName());
            return new PreparedStore(documentStore, options, embeddingModel);
        } catch (RuntimeException e) {
            DocumentStoreLifecycleSupport.closeQuietly(documentStore);
            throw e;
        }
    }

    private com.easyagents.core.document.Document toSearchDocument(FaqItem entity) {
        String content = buildSearchContent(entity);
        com.easyagents.core.document.Document doc = com.easyagents.core.document.Document.of(content);
        doc.setId(entity.getId());
        Map<String, Object> metadata = new HashMap<>();
        metadata.put("question", entity.getQuestion());
        metadata.put("answerText", entity.getAnswerText());
        metadata.put("categoryId", entity.getCategoryId());
        metadata.put("imageUrls", readImageUrls(entity.getOptions()));
        metadata.put(KeywordSearchMetadataKeys.KNOWLEDGE_ID, entity.getCollectionId() == null ? null : entity.getCollectionId().toString());
        doc.setMetadataMap(metadata);
        return doc;
    }

    private String buildSearchContent(FaqItem entity) {
        StringBuilder contentBuilder = new StringBuilder();
        contentBuilder.append("问题：").append(entity.getQuestion());
        if (StringUtil.hasText(entity.getAnswerText())) {
            contentBuilder.append("\n答案：").append(entity.getAnswerText());
        }
        String imageDescription = buildImageDescriptionText(entity.getOptions());
        if (StringUtil.hasText(imageDescription)) {
            contentBuilder.append("\n图片说明：").append(imageDescription);
        }
        return contentBuilder.toString();
    }

    private void markCollectionEmbedded(DocumentCollection collection, EmbeddingModel embeddingModel) {
        Map<String, Object> options = collection.getOptions() == null
                ? new HashMap<>()
                : new HashMap<>(collection.getOptions());
        if (!Boolean.FALSE.equals(options.get(KEY_CAN_UPDATE_EMBEDDING_MODEL))) {
            options.put(KEY_CAN_UPDATE_EMBEDDING_MODEL, false);
            DocumentCollection updateCollection = new DocumentCollection();
            updateCollection.setId(collection.getId());
            updateCollection.setOptions(options);
            documentCollectionService.updateById(updateCollection);
        }
        if (collection.getDimensionOfVectorModel() == null) {
            int dimension = Model.getEmbeddingDimension(embeddingModel);
            DocumentCollection updateCollection = new DocumentCollection();
            updateCollection.setId(collection.getId());
            updateCollection.setDimensionOfVectorModel(dimension);
            documentCollectionService.updateById(updateCollection);
        }
    }

    private List<FaqImportRow> parseExcelRows(MultipartFile file) {
        try (InputStream inputStream = file.getInputStream()) {
            FaqExcelReadListener listener = new FaqExcelReadListener();
            FastExcel.read(inputStream, listener)
                    .sheet()
                    .doRead();
            return listener.getRows();
        } catch (BusinessException e) {
            throw e;
        } catch (Exception e) {
            throw new BusinessException("FAQ Excel解析失败");
        }
    }

    private void validateImportFile(MultipartFile file) {
        if (file == null || file.isEmpty()) {
            throw new BusinessException("导入文件不能为空");
        }
        if (file.getSize() > MAX_IMPORT_FILE_SIZE_BYTES) {
            throw new BusinessException("导入文件大小不能超过10MB");
        }
        String fileName = file.getOriginalFilename();
        String lowerName = fileName == null ? "" : fileName.toLowerCase();
        if (!(lowerName.endsWith(".xlsx") || lowerName.endsWith(".xls"))) {
            throw new BusinessException("仅支持 xlsx/xls 文件");
        }
    }

    private List<List<String>> buildImportHeadList() {
        List<List<String>> headList = new ArrayList<>(3);
        headList.add(Collections.singletonList(IMPORT_HEAD_CATEGORY_PATH));
        headList.add(Collections.singletonList(IMPORT_HEAD_QUESTION));
        headList.add(Collections.singletonList(IMPORT_HEAD_ANSWER_HTML));
        return headList;
    }

    private List<List<String>> buildImportInstructionRows() {
        List<List<String>> rows = new ArrayList<>();
        rows.add(Collections.singletonList("1. 模板页为第一个Sheet（名称：模板），导入时将读取该Sheet。"));
        rows.add(Collections.singletonList("2. 分类路径可为空；为空或不存在时会自动回落到“默认分类”。"));
        rows.add(Collections.singletonList("3. 分类路径分隔符使用“/”，示例：产品/账号/登录。"));
        rows.add(Collections.singletonList("4. 问题不能为空；答案HTML不能为空。"));
        rows.add(Collections.singletonList("5. 导入按“分类路径 + 问题”执行更新/新增（Upsert）。"));
        rows.add(Collections.singletonList("6. 同一个Excel里若键重复，后出现的行会覆盖前面的行。"));
        rows.add(Collections.singletonList("7. 单次最多导入5000条数据，文件大小不超过10MB。"));
        return rows;
    }

    private void appendImportError(FaqImportResultVo result, FaqImportRow row, String reason) {
        FaqImportErrorRowVo errorRow = new FaqImportErrorRowVo();
        errorRow.setRowNumber(row.getRowNumber());
        errorRow.setCategoryPath(row.getCategoryPath());
        errorRow.setQuestion(row.getQuestion());
        errorRow.setReason(reason);
        result.getErrorRows().add(errorRow);
    }

    private String extractImportErrorMessage(Exception e) {
        if (e == null) {
            return "导入失败";
        }
        if (e.getCause() != null && StringUtil.hasText(e.getCause().getMessage())) {
            return e.getCause().getMessage();
        }
        if (StringUtil.hasText(e.getMessage())) {
            return e.getMessage();
        }
        return "导入失败";
    }

    private TransactionTemplate createImportRowTransactionTemplate() {
        TransactionTemplate template = new TransactionTemplate(transactionManager);
        template.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
        return template;
    }

    private boolean executeInRowTransaction(TransactionTemplate template, Supplier<Boolean> action) {
        Boolean success = template.execute(status -> {
            boolean saved = action.get();
            if (!saved) {
                status.setRollbackOnly();
                return false;
            }
            return true;
        });
        if (!Boolean.TRUE.equals(success)) {
            throw new BusinessException("FAQ导入保存失败");
        }
        return true;
    }

    private Map<String, BigInteger> buildCategoryPathIdMap(Map<BigInteger, String> categoryPathMap) {
        Map<String, BigInteger> pathIdMap = new HashMap<>();
        for (Map.Entry<BigInteger, String> entry : categoryPathMap.entrySet()) {
            if (entry.getKey() == null) {
                continue;
            }
            String normalizedPath = normalizeCategoryPath(entry.getValue());
            if (!StringUtil.hasText(normalizedPath)) {
                continue;
            }
            pathIdMap.put(normalizedPath, entry.getKey());
        }
        return pathIdMap;
    }

    private Map<String, FaqItem> buildExistingFaqMap(BigInteger collectionId,
                                                     Map<BigInteger, String> categoryPathMap,
                                                     String defaultCategoryPath) {
        Map<String, FaqItem> existingFaqMap = new HashMap<>();
        List<FaqItem> existingFaqItems = list(QueryWrapper.create()
                .eq(FaqItem::getCollectionId, collectionId)
                .orderBy("order_no asc, id asc"));
        for (FaqItem faqItem : existingFaqItems) {
            if (faqItem.getId() == null || StringUtil.noText(faqItem.getQuestion())) {
                continue;
            }
            String categoryPath = normalizeCategoryPath(categoryPathMap.get(faqItem.getCategoryId()));
            if (!StringUtil.hasText(categoryPath)) {
                categoryPath = defaultCategoryPath;
            }
            String upsertKey = buildImportUpsertKey(categoryPath, faqItem.getQuestion());
            existingFaqMap.putIfAbsent(upsertKey, faqItem);
        }
        return existingFaqMap;
    }

    private String buildImportUpsertKey(String categoryPath, String question) {
        return defaultIfNull(categoryPath, DEFAULT_CATEGORY_NAME) + IMPORT_UPSERT_KEY_SEPARATOR + question.trim();
    }

    private String normalizeCategoryPath(String categoryPath) {
        if (!StringUtil.hasText(categoryPath)) {
            return null;
        }
        String[] nodes = categoryPath.trim().split("/");
        List<String> normalized = new ArrayList<>();
        for (String node : nodes) {
            String trimmed = trimToNull(node);
            if (trimmed != null) {
                normalized.add(trimmed);
            }
        }
        if (normalized.isEmpty()) {
            return null;
        }
        return String.join(" / ", normalized);
    }

    private String defaultIfNull(String value, String fallback) {
        return value == null ? fallback : value;
    }

    private Integer nextOrderNo(BigInteger collectionId) {
        java.util.List<FaqItem> list = list(QueryWrapper.create()
                .eq(FaqItem::getCollectionId, collectionId)
                .orderBy("order_no desc"));
        if (list == null || list.isEmpty() || list.get(0).getOrderNo() == null) {
            return 0;
        }
        return list.get(0).getOrderNo() + 1;
    }

    private BigInteger getCurrentUserId() {
        if (!StpUtil.isLogin()) {
            return null;
        }
        return BigInteger.valueOf(StpUtil.getLoginIdAsLong());
    }

    private List<Map<String, String>> extractImages(String answerHtml) {
        if (StringUtil.noText(answerHtml)) {
            return Collections.emptyList();
        }
        Elements imageElements = Jsoup.parseBodyFragment(answerHtml).select("img[src]");
        if (imageElements.isEmpty()) {
            return Collections.emptyList();
        }
        List<Map<String, String>> images = new ArrayList<>();
        Set<String> visited = new HashSet<>();
        for (Element imageElement : imageElements) {
            String url = trimToNull(imageElement.attr("src"));
            if (url == null || !url.startsWith("http")) {
                continue;
            }
            if (!visited.add(url)) {
                continue;
            }
            Map<String, String> image = new LinkedHashMap<>();
            image.put(IMAGE_FIELD_URL, url);
            String alt = trimToNull(imageElement.attr("alt"));
            if (alt != null) {
                image.put(IMAGE_FIELD_ALT, alt);
            }
            String title = trimToNull(imageElement.attr("title"));
            if (title != null) {
                image.put(IMAGE_FIELD_TITLE, title);
            }
            images.add(image);
        }
        return images;
    }

    private String buildImageDescriptionText(Map<String, Object> options) {
        List<Map<String, String>> images = normalizeImageField(options);
        if (images.isEmpty()) {
            return null;
        }
        List<String> descriptions = new ArrayList<>();
        for (Map<String, String> image : images) {
            String alt = trimToNull(image.get(IMAGE_FIELD_ALT));
            if (alt != null) {
                descriptions.add(alt);
                continue;
            }
            String title = trimToNull(image.get(IMAGE_FIELD_TITLE));
            if (title != null) {
                descriptions.add(title);
            }
        }
        if (descriptions.isEmpty()) {
            return null;
        }
        return String.join("；", descriptions);
    }

    private List<String> readImageUrls(Map<String, Object> options) {
        List<Map<String, String>> images = normalizeImageField(options);
        if (images.isEmpty()) {
            return Collections.emptyList();
        }
        List<String> imageUrls = new ArrayList<>();
        for (Map<String, String> image : images) {
            String url = trimToNull(image.get(IMAGE_FIELD_URL));
            if (url != null) {
                imageUrls.add(url);
            }
        }
        return imageUrls;
    }

    @SuppressWarnings("unchecked")
    private List<Map<String, String>> normalizeImageField(Map<String, Object> options) {
        if (options == null) {
            return Collections.emptyList();
        }
        Object imagesValue = options.get(OPTION_KEY_IMAGES);
        if (!(imagesValue instanceof List<?>)) {
            return Collections.emptyList();
        }
        List<Map<String, String>> normalized = new ArrayList<>();
        for (Object imageObj : (List<Object>) imagesValue) {
            if (!(imageObj instanceof Map<?, ?>)) {
                continue;
            }
            Map<?, ?> imageMap = (Map<?, ?>) imageObj;
            String url = trimToNull(toStringValue(imageMap.get(IMAGE_FIELD_URL)));
            if (url == null) {
                continue;
            }
            Map<String, String> image = new LinkedHashMap<>();
            image.put(IMAGE_FIELD_URL, url);
            String alt = trimToNull(toStringValue(imageMap.get(IMAGE_FIELD_ALT)));
            if (alt != null) {
                image.put(IMAGE_FIELD_ALT, alt);
            }
            String title = trimToNull(toStringValue(imageMap.get(IMAGE_FIELD_TITLE)));
            if (title != null) {
                image.put(IMAGE_FIELD_TITLE, title);
            }
            normalized.add(image);
        }
        return normalized;
    }

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

    private String trimToNull(String value) {
        if (!StringUtil.hasText(value)) {
            return null;
        }
        return value.trim();
    }

    private static class FaqImportRow {
        private Integer rowNumber;
        private String categoryPath;
        private String question;
        private String answerHtml;

        public Integer getRowNumber() {
            return rowNumber;
        }

        public void setRowNumber(Integer rowNumber) {
            this.rowNumber = rowNumber;
        }

        public String getCategoryPath() {
            return categoryPath;
        }

        public void setCategoryPath(String categoryPath) {
            this.categoryPath = categoryPath;
        }

        public String getQuestion() {
            return question;
        }

        public void setQuestion(String question) {
            this.question = question;
        }

        public String getAnswerHtml() {
            return answerHtml;
        }

        public void setAnswerHtml(String answerHtml) {
            this.answerHtml = answerHtml;
        }
    }

    private class FaqExcelReadListener implements ReadListener<LinkedHashMap<Integer, Object>> {
        private final Map<String, Integer> headIndex = new HashMap<>();
        private final List<FaqImportRow> rows = new ArrayList<>();
        private int sheetRowNo;

        @Override
        public void invoke(LinkedHashMap<Integer, Object> data, AnalysisContext context) {
            sheetRowNo++;
            String categoryPath = getCellValue(data, IMPORT_HEAD_CATEGORY_PATH);
            String question = getCellValue(data, IMPORT_HEAD_QUESTION);
            String answerHtml = getCellValue(data, IMPORT_HEAD_ANSWER_HTML);
            if (!StringUtil.hasText(categoryPath) && !StringUtil.hasText(question) && !StringUtil.hasText(answerHtml)) {
                return;
            }
            if (rows.size() >= MAX_IMPORT_ROWS) {
                throw new BusinessException("单次最多导入5000条FAQ");
            }
            FaqImportRow row = new FaqImportRow();
            row.setRowNumber(sheetRowNo + 1);
            row.setCategoryPath(categoryPath);
            row.setQuestion(question);
            row.setAnswerHtml(answerHtml);
            rows.add(row);
        }

        @Override
        public void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {
            for (Map.Entry<Integer, ReadCellData<?>> entry : headMap.entrySet()) {
                String headValue = entry.getValue() == null ? null : entry.getValue().getStringValue();
                String header = trimToNull(headValue);
                if (header == null) {
                    continue;
                }
                headIndex.put(header, entry.getKey());
            }
            if (!headIndex.containsKey(IMPORT_HEAD_CATEGORY_PATH)
                    || !headIndex.containsKey(IMPORT_HEAD_QUESTION)
                    || !headIndex.containsKey(IMPORT_HEAD_ANSWER_HTML)) {
                throw new BusinessException("导入模板表头不正确，必须包含：分类路径、问题、答案HTML");
            }
        }

        @Override
        public void doAfterAllAnalysed(AnalysisContext context) {
            // no-op
        }

        public List<FaqImportRow> getRows() {
            return rows;
        }

        private String getCellValue(Map<Integer, Object> row, String headName) {
            Integer index = headIndex.get(headName);
            if (index == null) {
                return null;
            }
            Object value = row.get(index);
            return value == null ? null : String.valueOf(value).trim();
        }
    }

    private static class PreparedStore {
        private final DocumentStore documentStore;
        private final StoreOptions storeOptions;
        private final EmbeddingModel embeddingModel;

        private PreparedStore(DocumentStore documentStore, StoreOptions storeOptions, EmbeddingModel embeddingModel) {
            this.documentStore = documentStore;
            this.storeOptions = storeOptions;
            this.embeddingModel = embeddingModel;
        }
    }
}
