package digital.laboratory.platform.inspection.service.impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.config.Configure;
import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy;
import com.deepoove.poi.util.TableTools;
import com.deepoove.poi.xwpf.NiceXWPFDocument;
import digital.laboratory.platform.common.core.util.R;
import digital.laboratory.platform.common.feign.RemoteWord2PDFService;
import digital.laboratory.platform.common.oss.service.OssFile;
import digital.laboratory.platform.inspection.dto.TestRecordSampleDataDocDTO;
import digital.laboratory.platform.inspection.entity.TestRecordInstrument;
import digital.laboratory.platform.inspection.entity.TestRecordReagent;
import digital.laboratory.platform.inspection.entity.TestRecordSampleData;
import digital.laboratory.platform.inspection.entity.TestRecordSampleDataExpand;
import digital.laboratory.platform.inspetion.api.enums.BusinessType;
import digital.laboratory.platform.inspection.enums.TestRecordFileUrl;
import digital.laboratory.platform.inspection.service.*;
import digital.laboratory.platform.inspection.vo.IonPairAndCEVO;
import digital.laboratory.platform.inspection.vo.TestRecordReagentVO;
import digital.laboratory.platform.inspetion.api.entity.EntrustInfo;
import digital.laboratory.platform.inspetion.api.entity.SampleInfo;
import digital.laboratory.platform.inspetion.api.entity.TestRecord;
import digital.laboratory.platform.sys.entity.Drug;
import digital.laboratory.platform.sys.enums.entrust.EntrustBiologyType;
import feign.Response;
import org.apache.commons.io.output.ByteArrayOutputStream;
import org.apache.poi.xwpf.usermodel.XWPFTable;
import org.apache.poi.xwpf.usermodel.XWPFTableCell;
import org.apache.poi.xwpf.usermodel.XWPFTableRow;
import org.springframework.beans.BeanUtils;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * @author ChenJiangBao
 * @version 1.0
 * @description: 生成检验记录相关接口实现类
 * @date 2025/3/18 14:50
 */
@Service
public class InspectRecordServiceImpl implements InspectRecordService {

    @Resource
    private OssFile ossFile;

    @Resource
    private EntrustInfoService entrustInfoService;

    @Resource
    private TestRecordService testRecordService;

    @Resource
    private TestRecordInstrumentService testRecordInstrumentService;

    @Resource
    private TestRecordSampleDataService testRecordSampleDataService;

    @Resource
    private TestRecordReagentService testRecordReagentService;

    @Resource
    private SampleInfoService sampleInfoService;

    @Resource
    private TestRecordSampledataExpandService testRecordSampledataExpandService;

    @Resource
    private RemoteWord2PDFService remoteWord2PDFService;

    /**
     * 生成检验记录-贵阳禁毒
     *
     * @param businessId   委托id
     * @param materialType 标注当前委托的检材是尿液还是毛发
     * @return
     * @throws Exception
     */
    @Override
/**
 * 构建检验记录
 *
 * @param businessId  业务ID，用于查询委托信息和检验记录
 * @param materialType 材料类型，用于生成特定类型的检验记录
 * @return 返回封装的响应对象，如果已存在 `.docx` 文件，则直接返回其路径，否则根据业务类型生成新的检验记录
 * @throws Exception 可能抛出的异常
 */
    public R buildInspectionRecord(String businessId, String materialType, boolean isPreview) throws Exception {
        // 根据业务ID查询委托信息
        EntrustInfo entrustInfo = entrustInfoService.getById(businessId);
        if (entrustInfo == null) {
            return R.ok(false, "委托信息不存在！");
        }

        // 查询该业务ID对应的检验记录
        TestRecord testRecord = testRecordService.lambdaQuery()
                .eq(TestRecord::getBusinessId, businessId)
                .one();

        // 判断是否是预览，如果是预览，则每次都要重新生成，如果不是预览，那就是已经结束实验后生成的检验记录，就去判断是否生成过了，如果生成过就不再生成了
        if (!isPreview) {
            // 获取OSS存储中的检验记录目录下的文件列表
            List<String> list = ossFile.fileList(TestRecordFileUrl.TEST_RECORD_CATALOGUE.getFileUrl() + "/" + businessId + "/");

            // 如果该目录下存在文件，则遍历查找是否有 .docx 结尾的文件
            if (CollUtil.isNotEmpty(list)) {
                for (String fileName : list) {
                    // 提取文件后缀名
                    String suffix = fileName.substring(fileName.lastIndexOf(".") + 1);
                    // 如果找到 .docx 文件，则直接返回文件路径
                    if (suffix.equals("docx")) {
                        return R.ok(TestRecordFileUrl.TEST_RECORD_CATALOGUE.getFileUrl() + "/" + businessId + "/" + fileName, "生成成功!");
                    }
                }
            }
        }
        // 如果业务类型为 "BOINT_CASE"，生成体内检验记录
        if (entrustInfo.getBusinessType().equals(BusinessType.BOINT_CASE.getBusinessType())) {
            return R.ok(this.buildInVivoDocFile(entrustInfo, materialType), "生成成功!");
        }

        // 默认情况，生成通用的药品检验记录
        return R.ok(this.generateCommonDrugInpectRecord(businessId));
    }


    @Override
    public void previewInspectionRecord(String businessId, String materialType, HttpServletResponse servletResponse) throws Exception {
        String path = "";
        path = (String) this.buildInspectionRecord(businessId, materialType, true).getData();

        java.io.ByteArrayOutputStream fosWord = new java.io.ByteArrayOutputStream();
        ossFile.fileGet(path, fosWord);
        ByteArrayInputStream fisWord = new ByteArrayInputStream(fosWord.toByteArray());
        fosWord.close();

        // 转换 Word 文档为 PDF
        MockMultipartFile mockMultipartFile = new MockMultipartFile("file", "检验记录" + ".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", fisWord);
        String pdfFilePath = path.substring(0, path.lastIndexOf(".")) + ".pdf";
        Response response = remoteWord2PDFService.word2pdf(mockMultipartFile);
        fisWord.close();

        org.apache.commons.io.output.ByteArrayOutputStream outPDF = new org.apache.commons.io.output.ByteArrayOutputStream();
        IoUtil.copy(response.body().asInputStream(), outPDF, IoUtil.DEFAULT_MIDDLE_BUFFER_SIZE);
        ByteArrayInputStream isPDF = new ByteArrayInputStream(outPDF.toByteArray());
        outPDF.close();

        // 保存 PDF 文件
        ossFile.fileSave(pdfFilePath, isPDF);
        isPDF.close();

        // 删除原 Word 文件
        ossFile.fileDelete(path);
        System.out.println(String.format("已删除 Word 文件: %s", path));

        // 发送 PDF 文件到响应流
        ossFile.fileGet(pdfFilePath, servletResponse.getOutputStream());
        System.out.println(String.format("转换为 PDF 结束"));
    }


    /**
     * 构建体内检验记录数据
     *
     * @param entrustInfo  委托信息对象
     * @param materialType 材料类型（如血液、尿液等）
     * @return 包含体内检验记录的Map数据
     * @throws Exception 如果未找到检验记录，则抛出异常
     */
    public Map<String, Object> buildInVivoRecordData(EntrustInfo entrustInfo, String materialType) throws Exception {

        // 1️⃣ 获取当前委托业务对应的检验记录信息
        TestRecord testRecord = testRecordService.getTestRecordByBusinessId(entrustInfo.getId());

        // 2️⃣ 如果找不到对应的检验记录，则抛出异常
        if (testRecord == null) {
            throw new Exception("未找到检验记录信息");
        }

        // 3️⃣ 生成基础数据，构建通用的检验记录文档数据
        HashMap<String, Object> data = buildCommonInspectRecordDocMap(entrustInfo, testRecord, materialType);

        // 4️⃣ 查询该检验记录下的所有样品数据
        List<TestRecordSampleData> dataList = testRecordSampleDataService.list(
                Wrappers.<TestRecordSampleData>lambdaQuery().eq(TestRecordSampleData::getTestId, testRecord.getId()));

        // 5️⃣ 如果没有样品数据，则直接返回空数据
        if (CollectionUtils.isEmpty(dataList)) {
            data.put("dataDtos", Collections.emptyList());
            data.put("sampleSize", 0);
            data.put("vo", testRecord);
            return data;
        }

        // 6️⃣ 批量查询所有样品的扩展数据，避免多次查询数据库，提高效率
        Map<String, List<TestRecordSampleDataExpand>> dataExpandMap = testRecordSampledataExpandService.list(
                Wrappers.<TestRecordSampleDataExpand>lambdaQuery()
                        .in(TestRecordSampleDataExpand::getTestDataId, dataList.stream().map(TestRecordSampleData::getId).collect(Collectors.toList()))
        ).stream().collect(Collectors.groupingBy(TestRecordSampleDataExpand::getTestDataId));

        // 7️⃣ 按化合物名称进行分组，确保同一化合物的数据在一起处理
        Map<String, List<TestRecordSampleData>> dataMap = dataList.stream()
                .collect(Collectors.groupingBy(TestRecordSampleData::getCompoundCnName));

        // 8️⃣ 获取标准品列表，并按照其中的化合物顺序进行排序
        String referenceMaterialName = (String) data.get("referenceMaterialName");
        List<String> collect = Arrays.stream(referenceMaterialName.split("\n"))  // 按换行拆分
                .map(line -> line.replaceFirst("^\u3000\u3000\u2611 ", "") // 去掉前缀 "　　✔ "
                        .replaceFirst("^\u2611 ", "")) // 去掉前缀 "✔ "
                .collect(Collectors.toList()); // 转换为列表

        // 9️⃣ 用于存储最终的检验数据
        List<TestRecordSampleDataDocDTO> dataDtos = new ArrayList<>();

        // 🔟 遍历化合物列表，按照标准品顺序构建数据
        for (String compoundName : collect) {
            List<TestRecordSampleData> list = dataMap.get(compoundName);

            // 10.1️⃣ 先添加一个空白样品数据
            TestRecordSampleDataDocDTO blankVo = new TestRecordSampleDataDocDTO();
            blankVo.setName("空白" + materialType);
            blankVo.setCompoundName(compoundName);
            blankVo.setPTargetRtTime("/");
            blankVo.setPRtTimeError("/");
            blankVo.setPRtTimeWithinError("否");
            blankVo.setPIonAbundanceRatio("/");
            blankVo.setPIonAbundanceRatioError("/");
            blankVo.setPIonAbundanceRatioWithinError("否");
            blankVo.setPIsDetected("否");
            dataDtos.add(blankVo);

            // 10.2️⃣ 按样品类型（STD：标准样品，Analyte：检材样品）分组
            Map<String, List<TestRecordSampleData>> map = list.stream()
                    .collect(Collectors.groupingBy(TestRecordSampleData::getSampleType));

            // 11️⃣ 处理标准样品（STD）
            if (map.containsKey("STD")) {
                TestRecordSampleData std = map.get("STD").get(0);
                TestRecordSampleDataDocDTO stdVo = new TestRecordSampleDataDocDTO();
                BeanUtils.copyProperties(std, stdVo);

                List<TestRecordSampleDataExpand> expandList = dataExpandMap.get(std.getId());
                if (expandList != null) {
                    for (TestRecordSampleDataExpand expand : expandList) {
                        if (!expand.getBasePeak()) {
                            stdVo.setPIonAbundanceRatio(expand.getIonAbundanceRatio() != null ? expand.getIonAbundanceRatio().setScale(2, RoundingMode.HALF_UP).toString() : "/");
                            stdVo.setPIonAbundanceRatioError("/");
                            stdVo.setPIonAbundanceRatioWithinError("/");
                            break; // 只取第一个符合条件的扩展数据
                        }
                    }
                }
                stdVo.setName("空白" + materialType + "加标");
                stdVo.setPTargetRtTime(std.getTargetRtTime() != null ? String.format("%.2f", std.getTargetRtTime()) : "/");
                stdVo.setPRtTimeError("/");
                stdVo.setPRtTimeWithinError("/");
                stdVo.setPIsDetected("是");
                stdVo.setCompoundName(compoundName);
                dataDtos.add(stdVo);
            }

            // 12️⃣ 处理检材样品（Analyte）
            if (map.containsKey("Analyte")) {
                List<TestRecordSampleData> analyte = map.get("Analyte");
                analyte.sort(testRecordSampleDataService.getSortBySampleNo());

                List<TestRecordSampleDataDocDTO> dataVOS = new ArrayList<>();
                for (int i = 0; i < analyte.size(); i++) {
                    TestRecordSampleData item = analyte.get(i);
                    TestRecordSampleDataDocDTO vo = new TestRecordSampleDataDocDTO();
                    BeanUtils.copyProperties(item, vo);
                    List<TestRecordSampleDataExpand> expandList = dataExpandMap.get(item.getId());
                    vo.setPTargetRtTime(item.getTargetRtTime() != null ? String.format("%.2f", item.getTargetRtTime()) : "/");
                    vo.setPRtTimeError(item.getRtTimeError() != null ? item.getRtTimeError().setScale(2, RoundingMode.HALF_UP).toString() : "/");
                    vo.setPRtTimeWithinError(item.getRtTimeWithinError() != null ? item.getRtTimeWithinError().toString() : "/");
                    vo.setPIsDetected(item.getIsDetected() != null && item.getIsDetected() == 1 ? "是" : "否");

                    if (expandList != null) {
                        for (TestRecordSampleDataExpand expand : expandList) {
                            if (!expand.getBasePeak()) {
                                vo.setPIonAbundanceRatio(expand.getIonAbundanceRatio() != null ? expand.getIonAbundanceRatio().setScale(2, RoundingMode.HALF_UP).toString() : "/");
                                vo.setPIonAbundanceRatioError(expand.getIonAbundanceRatioError() != null ? expand.getIonAbundanceRatioError().setScale(2, RoundingMode.HALF_UP).toString() : "/");
                                vo.setPIonAbundanceRatioWithinError(expand.getIonAbundanceRatioWithinError() != null ? expand.getIonAbundanceRatioWithinError() : "/");
                                break; // 只取第一个符合条件的扩展数据
                            }
                        }
                    }

                    // 重新命名样品
                    vo.setName((dataList
                            .stream()
                            .collect(Collectors.groupingBy(TestRecordSampleData::getSampleNo))
                            .keySet()
                            .size() == 1) ? "检材样品" : (i + 1) + "号检材样品");
                    vo.setCompoundName(compoundName);
                    dataVOS.add(vo);
                }
                dataDtos.addAll(dataVOS);
            }
        }

        // 13️⃣ 为所有数据项添加序号
        int indexNum = 1;
        for (TestRecordSampleDataDocDTO dto : dataDtos) {
            dto.setIndexNum(String.valueOf(indexNum++));
        }

        // 14️⃣ 构建离子对和CE值数据
        this.buildIonPairAndCE(data, testRecordReagentService.list(
                        Wrappers.<TestRecordReagent>lambdaQuery()
                                .in(TestRecordReagent::getId, testRecord.getReagentConsumablesList())
                                .eq(TestRecordReagent::getCategory, "标准物质"))
                .stream().map(TestRecordReagent::getId).collect(Collectors.toList()));

        // 15️⃣ 返回最终处理的数据
        data.put("dataDtos", dataDtos);
        data.put("type", materialType);
        data.put("inspectOpinion", this.buildInspectOpinion(testRecordSampleDataService.lambdaQuery()
                .eq(TestRecordSampleData::getTestId, testRecord.getId())
                .eq(TestRecordSampleData::getSampleType, "Analyte").list()));
        return data;
    }


    public Map<String, Object> buildInVitroRecordData(EntrustInfo entrustInfo, TestRecord testRecord, Map<String, Object> data) {


        // 1️⃣  按化合物名称分组 TestRecordSampleData

        List<TestRecordSampleData> dataList = testRecordSampleDataService
                .lambdaQuery()
                .eq(TestRecordSampleData::getTestId, testRecord.getId())
                .list();

        Map<String, List<TestRecordSampleData>> compoundSampleDataMap = dataList
                .stream()
                .collect(Collectors.groupingBy(TestRecordSampleData::getCompoundCnName));


        // 2️⃣  过滤出 "标准物质" 类别的试剂，并转换为 Map (key=药品名称, value=试剂VO)
        List<String> reagentConsumablesList = testRecord.getReagentConsumablesList();
        Map<String, TestRecordReagentVO> reagentMap = reagentConsumablesList.stream()
                .map(testRecordReagentService::getVOById)
                .filter(vo -> "标准物质".equals(vo.getCategory())) // 仅保留 "标准物质"
                .collect(Collectors.toMap(vo -> vo.getDrug().getName(), vo -> vo));

        List<TestRecordSampleDataDocDTO> dataDtos = new ArrayList<>();

        // 3️⃣ 遍历每种化合物，处理实验数据
        for (Map.Entry<String, List<TestRecordSampleData>> entry : compoundSampleDataMap.entrySet()) {
            String compoundName = entry.getKey();
            List<TestRecordSampleData> sampleDataList = entry.getValue();

            // 按 sampleType 分组（如 STD, Analyte）
            Map<String, List<TestRecordSampleData>> sampleTypeMap = sampleDataList.stream()
                    .collect(Collectors.groupingBy(TestRecordSampleData::getSampleType));

            // 获取标准样本（STD）
            List<TestRecordSampleData> stdList = sampleTypeMap.get("STD");
            if (stdList == null || stdList.isEmpty()) {
                continue; // 没有 STD 样本，跳过该化合物
            }
            TestRecordSampleData stdSample = stdList.get(0);

            // 4️⃣  获取 STD 样本的扩展数据 (以 MassToChargeRatio 为 key)
            Map<String, TestRecordSampleDataExpand> expandMap = testRecordSampledataExpandService.lambdaQuery()
                    .eq(TestRecordSampleDataExpand::getTestDataId, stdSample.getId())
                    .list()
                    .stream()
                    .collect(Collectors.toMap(
                            e -> e.getMassToChargeRatio().setScale(0, RoundingMode.FLOOR).toString(), // 保留整数部分
                            Function.identity()
                    ));

            // 5️⃣  获取当前化合物对应的试剂，提取特征离子
            TestRecordReagentVO reagentVO = reagentMap.get(compoundName);
            if (reagentVO == null || StringUtils.isBlank(reagentVO.getDrug().getCharacteristicIons())) {
                continue; // 没有对应试剂或特征离子数据，跳过
            }
            String[] peaks = reagentVO.getDrug().getCharacteristicIons().split("、");


            // 6️⃣  处理 "标样" 数据 (STD)
            for (int i = 0; i < peaks.length; i++) {
                String peak = (i == 0) ? peaks[i].substring(0, peaks[i].length() - 1) : peaks[i];
                TestRecordSampleDataExpand expand = expandMap.get(peak);
                if (expand != null) {
                    TestRecordSampleDataDocDTO dto = new TestRecordSampleDataDocDTO();
                    BeanUtils.copyProperties(stdSample, dto);
                    dto.setName("标样");
                    dto.setCompoundName(compoundName);
                    dto.setPTargetRtTime(Optional.ofNullable(dto.getTargetRtTime()).map(Object::toString).orElse("/"));
                    dto.setPRtTimeError("±1");
                    dto.setPRtTimeWithinError("/");
                    dto.setPMassToChargeRatio(i == 0 ? peak + "（基峰）" : peak);
                    dto.setPIonAbundanceRatio(Optional.ofNullable(expand.getIonAbundanceRatio())
                            .map(value -> value.setScale(2, RoundingMode.HALF_UP).toString())
                            .orElse("/"));

                    dto.setPIonAbundanceRatioError(Optional.ofNullable(expand.getIonAbundanceRatioError())
                            .map(value -> value.setScale(2, RoundingMode.HALF_UP).toString())
                            .orElse("/"));
                    dto.setPIonAbundanceRatioWithinError(Optional.ofNullable(expand.getIonAbundanceRatioWithinError()).filter(s -> !s.isEmpty()).orElse("/"));
                    dto.setPIsDetected("/");
                    dataDtos.add(dto);
                }
            }

            // 7️⃣  处理 "标样空白" 数据
            for (String peak : peaks) {
                TestRecordSampleDataDocDTO dto = new TestRecordSampleDataDocDTO();
                dto.setName("标样空白");
                dto.setCompoundName(compoundName);
                dto.setPTargetRtTime("/");
                dto.setPRtTimeError("/");
                dto.setPRtTimeWithinError("/");
                dto.setPMassToChargeRatio(peak.equals(peaks[0]) ? peaks[0].substring(0, peaks[0].length() - 1) + "（基峰）" : peak);
                dto.setPIonAbundanceRatio("/");
                dto.setPIonAbundanceRatioError("/");
                dto.setPIonAbundanceRatioWithinError(peak.equals(peaks[0]) ? "/" : "否");
                dto.setPIsDetected("/");
                dataDtos.add(dto);
            }

            // 8️⃣  处理 "Analyte" 样本
            List<TestRecordSampleData> analyteList = sampleTypeMap.get("Analyte");
            if (analyteList != null) {
                analyteList.sort(testRecordSampleDataService.getSortBySampleNo()); // 按样本编号排序

                for (TestRecordSampleData analyte : analyteList) {
                    Map<String, TestRecordSampleDataExpand> analyteExpandMap = testRecordSampledataExpandService.lambdaQuery()
                            .eq(TestRecordSampleDataExpand::getTestDataId, analyte.getId())
                            .list()
                            .stream()
                            .collect(Collectors.toMap(
                                    e -> e.getMassToChargeRatio().setScale(0, RoundingMode.FLOOR).toString(), // 保留整数部分
                                    Function.identity()
                            ));

                    for (int i = 0; i < peaks.length; i++) {
                        String peak = (i == 0) ? peaks[i].substring(0, peaks[i].length() - 1) : peaks[i];
                        TestRecordSampleDataExpand expand = analyteExpandMap.get(peak);
                        if (expand != null) {
                            TestRecordSampleDataDocDTO dto = new TestRecordSampleDataDocDTO();
                            BeanUtils.copyProperties(analyte, dto);
                            dto.setPTargetRtTime(Optional.ofNullable(dto.getTargetRtTime()).map(Object::toString).orElse("/"));
                            dto.setPRtTimeError(Optional.ofNullable(dto.getRtTimeError())
                                    .map(value -> new BigDecimal(value.toString()).setScale(2, RoundingMode.HALF_UP).toString())
                                    .orElse("/"));
                            dto.setPRtTimeWithinError(Optional.ofNullable(dto.getRtTimeWithinError()).orElse("/"));
                            dto.setPMassToChargeRatio(i == 0 ? peak + "（基峰）" : peak);
                            dto.setPIonAbundanceRatio(Optional.ofNullable(expand.getIonAbundanceRatio())
                                    .map(value -> value.setScale(2, RoundingMode.HALF_UP).toString())
                                    .orElse("/"));

                            dto.setPIonAbundanceRatioError(Optional.ofNullable(expand.getIonAbundanceRatioError())
                                    .map(value -> value.setScale(2, RoundingMode.HALF_UP).toString())
                                    .orElse("/"));
                            dto.setPIonAbundanceRatioWithinError(Optional.ofNullable(expand.getIonAbundanceRatioWithinError()).orElse("/"));
                            dto.setPIsDetected(dto.getIsDetected() != null && dto.getIsDetected() == 1 ? "是" : "否");
                            dto.setCompoundName(compoundName);
                            dataDtos.add(dto);
                        }
                    }
                }
            }
        }

        // 9️⃣  返回处理后的数据
        data.put("dataDtos", dataDtos);
        data.put("sampleSize", dataDtos.size());

        // 1️⃣0 添加检测结果与分析结果判定
        List<TestRecordSampleData> sampleDataList = testRecordSampleDataService
                .lambdaQuery()
                .eq(TestRecordSampleData::getTestId, testRecord.getId())
                .eq(TestRecordSampleData::getSampleType, "Analyte")
                .list();

        data.put("type", "常规毒品");
        data.put("inspectOpinion", sampleDataList.size() > 0 ? this.buildInspectOpinion(sampleDataList) : "");
        this.processDetectionResults(data, testRecord);

        return data;
    }

    /**
     * 📌 处理检测结果并格式化
     *
     * @param data       存储检测结果的 Map
     * @param testRecord 当前检测记录
     */
    public void processDetectionResults(Map<String, Object> data, TestRecord testRecord) {
        // 查询所有 "Analyte" 类型的样本数据
        List<TestRecordSampleData> analyteDataList = testRecordSampleDataService
                .lambdaQuery()
                .eq(TestRecordSampleData::getTestId, testRecord.getId())
                .eq(TestRecordSampleData::getSampleType, "Analyte")
                .list();

        if (analyteDataList.isEmpty()) {
            return;  // 如果没有数据，直接返回
        }

        // 按 `isDetected` 分组，1 为检出，0 为未检出
        Map<Integer, List<TestRecordSampleData>> isDetectedMap = analyteDataList.stream()
                .filter(Objects::nonNull)
                .collect(Collectors.groupingBy(TestRecordSampleData::getIsDetected));

        List<String> isDetectedStr = new ArrayList<>();
        List<String> notDetectedStr = new ArrayList<>();

        // 遍历检测结果，生成描述
        for (Map.Entry<Integer, List<TestRecordSampleData>> entry : isDetectedMap.entrySet()) {
            List<TestRecordSampleData> samples = entry.getValue();
            if (samples == null || samples.isEmpty()) {
                continue;  // 如果样本数据为空，跳过
            }

            // 按化合物名称分组样本
            Map<String, List<TestRecordSampleData>> compoundSampleMap = samples.stream()
                    .filter(sample -> sample.getCompoundCnName() != null && sample.getSampleNo() != null)
                    .collect(Collectors.groupingBy(TestRecordSampleData::getCompoundCnName));

            for (Map.Entry<String, List<TestRecordSampleData>> compoundEntry : compoundSampleMap.entrySet()) {
                String compoundName = compoundEntry.getKey();
                List<TestRecordSampleData> compoundSamples = compoundEntry.getValue();

                if (compoundSamples == null || compoundSamples.isEmpty()) {
                    continue;  // 如果化合物名称或样本为空，跳过
                }

                // 按 `sampleNo` 排序样本
                compoundSamples.sort(testRecordSampleDataService.getSortBySampleNo());

                // 拼接样本编号
                String sampleNos = compoundSamples.stream()
                        .map(sample -> {
                            String[] parts = sample.getSampleNo().split("-");
                            return (parts.length > 2) ? parts[2] + "号" : sample.getSampleNo();  // 防止数组越界
                        })
                        .collect(Collectors.joining("、"));

                // 生成描述
                String description;
                if (entry.getKey() == 1) {
                    description = String.format(
                            "☑　%s样品空白对照无干扰，在相同条件下进行测定时，样品溶液中目标物与浓度相近的标准工作溶液中的目标物相比，" +
                                    "色谱峰保留时间一致、质谱特征离子一致，各离子丰度比相对偏差符合规定范围，判定样品中检出%s。",
                            sampleNos, compoundName);
                    isDetectedStr.add(description);
                } else {
                    description = String.format(
                            "☑　%s样品空白对照无干扰，样品溶液中未检出符合阳性结果评价要求的色谱峰，标准溶液空白无干扰，判定样品中未检出%s。",
                            sampleNos, compoundName);
                    notDetectedStr.add(description);
                }
            }
        }

        // 将阳性结果和阴性结果放入数据 Map 中
        if (!isDetectedStr.isEmpty()) {
            data.put("isDetectedStrs", "阳性结果：" + "\n" + isDetectedStr.stream()
                    .map(item -> "　　" + item)  // 使用全角空格缩进
                    .collect(Collectors.joining("\n")));
        }
        if (!notDetectedStr.isEmpty()) {
            data.put("notDetectedStrs", "阴性结果：" + "\n" + notDetectedStr.stream()
                    .map(item -> "　　" + item)  // 使用全角空格缩进
                    .collect(Collectors.joining("\n")));
        }
    }


    /**
     * 📌 构建离子对和碰撞能量信息，并存入数据 Map
     *
     * @param data          存储结果的 Map
     * @param reagentIdList 试剂 ID 列表
     */
    public void buildIonPairAndCE(Map<String, Object> data, List<String> reagentIdList) {
        List<IonPairAndCEVO> ionPairAndCEVOS = new ArrayList<>();

        // 🔄 遍历 reagentIdList 获取试剂信息
        for (String id : reagentIdList) {
            TestRecordReagentVO vo = testRecordReagentService.getVOById(id);
            if (vo == null) {
                continue;  // ⏭️ 如果 vo 为空，跳过当前循环
            }

            Drug drug = vo.getDrug();
            if (drug == null) {
                continue;  // ⏭️ 如果 drug 为空，跳过当前循环
            }

            // 📌 创建主产物离子信息并添加到列表
            ionPairAndCEVOS.add(createIonPairAndCEVO(drug.getName(), drug.getMainProductIon(),
                    drug.getMainDeclusteringPotential(), drug.getMainCollisionEnergy()));

            // 📌 创建次产物离子信息并添加到列表
            ionPairAndCEVOS.add(createIonPairAndCEVO(drug.getName(), drug.getMinorProductIon(),
                    drug.getMinorDeclusteringPotential(), drug.getMinorCollisionEnergy()));
        }

        // 📌 将结果放入数据 map 中，供后续使用
        data.put("ionPairAndCEVOS", ionPairAndCEVOS);
    }


    // 创建 IonPairAndCEVO 对象的辅助方法

    /**
     * 📌 创建 IonPairAndCEVO 对象并初始化属性
     *
     * @param compoundName          化合物名称
     * @param productIon            产物离子
     * @param declusteringPotential 去簇电位（DP）
     * @param collisionEnergy       碰撞能量（CE）
     * @return 初始化后的 IonPairAndCEVO 对象
     */
    private IonPairAndCEVO createIonPairAndCEVO(String compoundName, String productIon,
                                                Integer declusteringPotential, Integer collisionEnergy) {
        IonPairAndCEVO ionPairAndCEVO = new IonPairAndCEVO();
        ionPairAndCEVO.setCompoundName(compoundName); // 设置化合物名称
        ionPairAndCEVO.setProductIon(productIon); // 设置产物离子
        ionPairAndCEVO.setDeclusteringPotential(declusteringPotential); // 设置去簇电位（DP）
        ionPairAndCEVO.setCollisionEnergy(collisionEnergy); // 设置碰撞能量（CE）
        return ionPairAndCEVO; // 返回创建的对象
    }


    /**
     * 根据委托信息和样本类型生成生物样本检验记录文件。
     *
     * @param entrustInfo  委托信息对象
     * @param materialType 样本类型（如"毛发"、"尿液"等）
     * @return 生成的生物样本检验记录文件的路径
     * @throws Exception 如果文件生成过程中出现错误，抛出异常
     */
    public String buildInVivoDocFile(EntrustInfo entrustInfo, String materialType) throws Exception {
        // 获取文件数据map
        Map<String, Object> data = this.buildInVivoRecordData(entrustInfo, materialType);
        // 调用模板生成检验记录
        String templatePath = "";
        if (materialType.equals("毛发")) {
            templatePath = "/template" + "/贵阳生物样本毛发检验记录模板.docx";
        } else {
            templatePath = "/template" + "/贵阳生物样本尿液检验记录模板.docx";
        }
        LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();
        Configure config = Configure.builder().
                bind("dataDtos", policy)
                .bind("ionPairAndCEVOS", policy).build();
        return buildDocFileAndUploadToOss(entrustInfo.getId(), templatePath, config, data);
    }

    /**
     * 📄 构建 Word 文档并上传至 OSS
     *
     * @param entrustId    委托 ID
     * @param templatePath 模板文件路径
     * @param config       配置对象
     * @param data         数据映射
     * @return 上传到 OSS 的文件路径
     * @throws Exception 可能抛出的异常
     */
    private String buildDocFileAndUploadToOss(String entrustId, String templatePath, Configure config, Map<String, Object> data) throws Exception {
        // 📌 读取模板文件
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ossFile.fileGet(templatePath, bos);
        byte[] templateArray = bos.toByteArray();
        bos.close();

        try (ByteArrayInputStream bis = new ByteArrayInputStream(templateArray);
             ByteArrayOutputStream fosWord = new ByteArrayOutputStream()) {

            // 📌 渲染模板数据
            XWPFTemplate template = XWPFTemplate.compile(bis, config).render(data);
            NiceXWPFDocument document = template.getXWPFDocument();
            List<XWPFTable> tables = document.getTables();

            // 📌 读取数据类型并安全处理
            String type = (String) data.getOrDefault("type", ""); // 避免空指针
            XWPFTable table = tables.get(tables.size() - 1); // 获取最后一个表格

            if ("常规毒品".equals(type)) {
                mergeRegularDrugTable(table);
            } else if ("尿液".equals(type) || "毛发".equals(type)) {
                mergeNonRegularDrugTable(table);
                mergeAdditionalTable(type, tables);
            }

            // 📌 写入文档流
            template.write(fosWord);
            template.close();
            document.close();

            // 📌 构建文件存储路径
            ByteArrayInputStream fisWord = new ByteArrayInputStream(fosWord.toByteArray());
            String path = TestRecordFileUrl.TEST_RECORD_CATALOGUE.getFileUrl() + "/" + entrustId + "/" + "贵阳生物样本检验记录.docx";
            ossFile.fileSave(path, fisWord);

            return path; // 📌 返回 OSS 文件路径
        }
    }

    /**
     * 📌 合并常规毒品检验记录检测数据部分表格的单元格
     */
    private void mergeRegularDrugTable(XWPFTable table) {
        int startRow = 1; // 🔹 从第二行开始（索引从0开始）
        int mergeStep = 4; // 🔹 每 4 行合并一次
        int[] mergeColumns = {0, 1, 2, 3, 4, 9}; // 🔹 需要合并的列

        for (int rowIndex = startRow; rowIndex < table.getNumberOfRows(); rowIndex += mergeStep) {
            int endRow = Math.min(rowIndex + mergeStep - 1, table.getNumberOfRows() - 1); // 防止越界
            for (int col : mergeColumns) {
                TableTools.mergeCellsVertically(table, col, rowIndex, endRow);
            }
        }
    }

    /**
     * 📌 合并生物样本检验记录检测数据部分表格的单元格
     */
    private void mergeNonRegularDrugTable(XWPFTable table) {
        int startRow = 1;
        String currentCompoundName = null;
        int mergeStartRow = -1;

        for (int rowIndex = startRow; rowIndex < table.getNumberOfRows(); rowIndex++) {
            XWPFTableRow row = table.getRow(rowIndex);
            XWPFTableCell cell = row.getCell(1); // 🔹 第二列（索引1）
            String compoundName = cell.getText().trim();

            if (!compoundName.equals(currentCompoundName)) {
                if (mergeStartRow >= 0 && rowIndex > mergeStartRow + 1) {
                    TableTools.mergeCellsVertically(table, 1, mergeStartRow, rowIndex - 1);
                }
                currentCompoundName = compoundName;
                mergeStartRow = rowIndex;
            }
        }

        // 📌 处理最后一组合并
        if (mergeStartRow >= 0 && mergeStartRow < table.getNumberOfRows() - 1) {
            TableTools.mergeCellsVertically(table, 1, mergeStartRow, table.getNumberOfRows() - 1);
        }
    }

    /**
     * 📌 处理毛发或尿液表格目标物定性离子对、碰撞能量 (CE)等参数表格的合并逻辑
     * 📌 因为毛发和尿液的配置表格数量不同，因此下标不同
     */
    private void mergeAdditionalTable(String type, List<XWPFTable> tables) {
        if (!"毛发".equals(type) && !"尿液".equals(type)) {
            return; // 如果不是毛发或尿液类型，则不处理
        }

        int tableIndex = "毛发".equals(type) ? 2 : 1;
        if (tables.size() <= tableIndex) {
            return; // 避免索引越界
        }

        XWPFTable xwpfTable = tables.get(tableIndex);
        List<XWPFTableRow> rows = xwpfTable.getRows();

        for (int i = 1; i < rows.size() - 1; i++) { // 🔹 从第二行开始
            XWPFTableRow currentRow = rows.get(i);
            XWPFTableRow nextRow = rows.get(i + 1);

            String currentCompoundName = currentRow.getCell(0).getText().trim();
            String nextCompoundName = nextRow.getCell(0).getText().trim();

            if (currentCompoundName.equals(nextCompoundName)) {
                // 📌 合并单元格
                currentRow.getCell(0).getCTTc().addNewTcPr().addNewVMerge()
                        .setVal(org.openxmlformats.schemas.wordprocessingml.x2006.main.STMerge.RESTART);
                nextRow.getCell(0).getCTTc().addNewTcPr().addNewVMerge()
                        .setVal(org.openxmlformats.schemas.wordprocessingml.x2006.main.STMerge.CONTINUE);

                // 📌 清空被合并行的内容
                nextRow.getCell(0).setText("");
            }
        }
    }


    /**
     * 生成常规毒品的检验记录（贵阳禁毒）
     */
    public String generateCommonDrugInpectRecord(String businessId) throws Exception {
        Map<String, Object> docMap = buildCommonDrugDocMap(businessId);
        EntrustInfo entrustInfo = entrustInfoService.getById(businessId);
        TestRecord testRecord = testRecordService.lambdaQuery()
                .eq(TestRecord::getBusinessId, businessId)
                .one();
        Map<String, Object> data = this.buildInVitroRecordData(entrustInfo, testRecord, docMap);
        String templatePath = "/template" + "/贵阳禁毒常规毒品检验记录模板.docx";
        LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();
        Configure config = Configure.builder().
                bind("dataDtos", policy)
                .build();
        return buildDocFileAndUploadToOss(businessId, templatePath, config, docMap);
    }

    /**
     * 构建常规毒品的检验记录数据
     *
     * @return
     */
    private Map<String, Object> buildCommonDrugDocMap(String businessId) {
        EntrustInfo entrustInfo = entrustInfoService.getById(businessId);

        // 这里因为检验记录实体并未关联业务id, 只能先查询实验数据，在通过实验数据获取到检验记录信息
        TestRecord testRecord = testRecordService.lambdaQuery()
                .eq(TestRecord::getBusinessId, businessId)
                .one();

        // 封装数据到map中
        Map<String, Object> docMap = buildCommonInspectRecordDocMap(entrustInfo, testRecord, "");


        return docMap;
    }


    /**
     * 构建通用的检验记录文档映射
     *
     * @param entrustInfo  委托信息对象
     * @param materialType
     * @return 包含委托信息和检验记录信息的HashMap对象
     */
    private HashMap<String, Object> buildCommonInspectRecordDocMap(EntrustInfo entrustInfo, TestRecord
            testRecord, String materialType) {
        HashMap<String, Object> data = new HashMap<>();

        data.put("entrustDepartment", entrustInfo.getEntrustDepartment());
        data.put("deliver1Name", entrustInfo.getDeliver1Name());
        data.put("deliver2Name", entrustInfo.getDeliver2Name());
        // 受理日期
        data.put("acceptYear", entrustInfo.getAcceptDate().getYear());
        data.put("acceptMonth", entrustInfo.getAcceptDate().getMonthValue());
        data.put("acceptDay", entrustInfo.getAcceptDate().getDayOfMonth());

        // 检验日期
        data.put("inspectYear", testRecord.getTestStartDate().getYear());
        data.put("inspectMonth", testRecord.getTestStartDate().getMonthValue());
        data.put("inspectDay", testRecord.getTestStartDate().getDayOfMonth());
        String[] split = entrustInfo.getAcceptNo().split("-");
        if (split.length == 2) {
            data.put("acceptNo", "[" + split[0] + "]" + split[1] + "号");
        }

        // 检材性状描述
        String materialCharacterDesc = buildMaterialCharacterDesc(sampleInfoService
                .lambdaQuery()
                .in(SampleInfo::getId, testRecord.getSampleTestList())
                .list());
        data.put("materialCharacterDesc", materialCharacterDesc);

        // 获取仪器设备数据（批量查询，减少数据库压力）
        List<String> deviceIdList = testRecord.getDeviceIdList();
        List<TestRecordInstrument> instruments = CollectionUtils.isEmpty(deviceIdList)
                ? Collections.emptyList()
                : testRecordInstrumentService.list(Wrappers.<TestRecordInstrument>lambdaQuery()
                .in(TestRecordInstrument::getId, deviceIdList));

        if (materialType.isEmpty()) {
            data.put("instrumentName", CollectionUtils.isEmpty(instruments)
                    ? "\u3000\u3000未找到仪器设备数据!"
                    : instruments.stream()
                    .map(i -> "\u3000\u3000\u2611" + i.getInstrumentName()) // 使用 Unicode 全角空格
                    .collect(Collectors.joining("\n")));
        } else {
            data.put("instrumentName", CollectionUtils.isEmpty(instruments)
                    ? "\u3000\u3000未找到仪器设备数据!"
                    : instruments.stream()
                    .map(i -> "\u3000\u3000" + i.getInstrumentName()) // 使用 Unicode 全角空格
                    .collect(Collectors.joining("\n")));
        }


        // 设置使用的标准物质和试剂
        List<TestRecordReagent> references = testRecordReagentService.list(Wrappers.<TestRecordReagent>lambdaQuery()
                .in(TestRecordReagent::getId, testRecord.getReagentConsumablesList())
                .eq(TestRecordReagent::getCategory, "标准物质"));


        if (references == null || references.isEmpty()) {
            data.put("referenceMaterialName", "未找到标准物质数据!");
        } else {
            String referenceMaterialName = "";
            if (references.size() == 1) {
                referenceMaterialName = "\u2611" + " " + references.get(0).getReagentConsumableName();
            } else {
                TestRecordReagent firstReagent = references.get(0);
                String firstStr = "\u2611" + " " + references.get(0).getReagentConsumableName() + "\n";
                references.remove(references.get(0));
                referenceMaterialName = firstStr + references.stream()
                        .map(reagent -> "\u3000\u3000\u2611" + " " + reagent.getReagentConsumableName())
                        .collect(Collectors.joining("\n"));
                references.add(0, firstReagent);
            }
            data.put("referenceMaterialName", referenceMaterialName);
            data.put("materialIngredient", references.stream()
                    .map(reagent -> reagent.getReagentConsumableName())
                    .collect(Collectors.joining("、")));
        }
        List<TestRecordReagent> reagents = testRecordReagentService.list(Wrappers.<TestRecordReagent>lambdaQuery()
                .in(TestRecordReagent::getId, testRecord.getReagentConsumablesList())
                .eq(TestRecordReagent::getCategory, "试剂"));

        if (reagents == null || reagents.isEmpty()) {
            data.put("reagentConsumableName", " 未找到试剂耗材数据!");
        } else {
            String reagentConsumableName = reagents.stream()
                    .map(TestRecordReagent::getReagentConsumableName)
                    .collect(Collectors.joining("、"));
            data.put("reagentConsumableName", reagentConsumableName);
        }
        return data;
    }

    /**
     * 根据委托信息和检材类型获取检材信息列表
     *
     * @param entrustInfo  委托信息对象
     * @param materialType 检材类型, 毛发或尿液
     * @return 检材信息列表
     */
    private List<SampleInfo> getSampleInfosByMaterialType(EntrustInfo entrustInfo, String materialType) {
        List<SampleInfo> sampleInfoList = sampleInfoService.list(Wrappers.<SampleInfo>lambdaQuery().eq(SampleInfo::getBusinessId, entrustInfo.getId()));
        // 对检材进行筛选，如果materialType 不为空则进行操作
        if (StrUtil.isNotBlank(materialType)) {
            if (materialType.equals("毛发")) {
                sampleInfoList = sampleInfoList.stream()
                        .filter(o -> EntrustBiologyType.HAIR.equals(EntrustBiologyType.isExist(o.getForm())))
                        .collect(Collectors.toList());

            } else {
                // 尿液
                sampleInfoList = sampleInfoList.stream()
                        .filter(o -> EntrustBiologyType.URINE.equals(EntrustBiologyType.isExist(o.getForm())))
                        .collect(Collectors.toList());
            }
        }
        return sampleInfoList;
    }


    /**
     * 构建检材性状描述（使用检材编号尾号，并合并连续编号）
     * <p>
     * 规则：
     * 1. **提取编号尾号**（如 `2025007001` → `1`）
     * 2. **按尾号排序**
     * 3. **连续编号 & 物质性状相同时合并**
     * 4. **只有一个检材时，省略编号，直接描述**
     * 5. **输出格式示例：**
     * - `"1号至4号检材为粉状"`
     * - `"检材为粉状"`（仅有一个检材时）
     *
     * @param sampleInfoList 样本信息列表
     * @return 物料特性描述（如果列表为空，则返回空字符串）
     */
    @Override
    public String buildMaterialCharacterDesc(List<SampleInfo> sampleInfoList) {
        if (CollUtil.isEmpty(sampleInfoList)) {
            return StrUtil.EMPTY;
        }

        // 按尾号排序（提取编号尾号进行比较）
        sampleInfoList.sort(Comparator.comparing(sample -> extractTailNumber(sample.getAcceptNo())));

        List<String> descriptions = new ArrayList<>();
        int start = -1, prev = -1;
        String prevForm = null;
        List<String> mergedOrderNos = new ArrayList<>();

        // 遍历排序后的检材编号，合并相邻且性状相同的编号
        for (SampleInfo sample : sampleInfoList) {
            int current = extractTailNumber(sample.getAcceptNo()); // 提取尾号
            String form = sample.getForm();

            if (start == -1) { // 初始化
                start = prev = current;
                prevForm = form;
            } else if (current == prev + 1 && form.equals(prevForm)) {
                // 只有连续尾号 + 相同性状才能合并
                prev = current;
            } else {
                // 遇到新性状或不连续尾号，存储前一段描述
                mergedOrderNos.add(formatRange(start, prev) + "检材为" + prevForm);
                start = prev = current;
                prevForm = form;
            }
        }
        // 处理最后一组数据
        mergedOrderNos.add(formatRange(start, prev) + "检材为" + prevForm);

        // **如果只有一个检材，省略编号**
        if (mergedOrderNos.size() == 1 && sampleInfoList.size() == 1) {
            return "检材为" + prevForm + "。";
        }

        return String.join("；", mergedOrderNos) + "。";
    }

    /**
     * 提取检材编号的尾号（即最后一个 `-` 之后的数值）
     *
     * @param orderNo 检材编号（格式：2025-89-13）
     * @return 尾号（示例：返回 13）
     */
    private int extractTailNumber(String orderNo) {
        String[] parts = orderNo.split("-");
        return (parts.length > 0) ? Integer.parseInt(parts[parts.length - 1]) : 0;
    }

    /**
     * 对样品编号进行排序
     *
     * @param sampleNumbers 样品编号列表，例如 ["3号", "1号", "2号"]
     * @return 排序后的样品编号列表，例如 ["1号", "2号", "3号"]
     */
    private List<String> sortSampleNumbers(List<String> sampleNumbers) {
        return sampleNumbers.stream()
                .sorted(Comparator.comparingInt(this::extractNumber))
                .collect(Collectors.toList());
    }

    /**
     * 从样品编号中提取数值部分
     *
     * @param sampleNumber 样品编号，例如 "12号"
     * @return 提取出的数字，例如 12
     */
    private int extractNumber(String sampleNumber) {
        return sampleNumber.matches("\\d+号") ? Integer.parseInt(sampleNumber.replace("号", "")) : Integer.MAX_VALUE;
    }

    /**
     * 格式化编号范围：
     * - 单个编号：返回 "1号"
     * - 连续编号：返回 "1号至3号"
     *
     * @param sortedSampleNumbers 已排序的样品编号列表
     * @return 格式化的编号字符串
     */
    private String formatSampleNumbers(List<String> sortedSampleNumbers) {
        List<Integer> numbers = sortedSampleNumbers.stream()
                .map(s -> s.replace("号", "")) // 移除"号"字
                .map(Integer::parseInt) // 转换为整数
                .sorted() // 排序
                .collect(Collectors.toList());

        List<String> formattedList = new ArrayList<>();
        int start = numbers.get(0), prev = start;

        for (int i = 1; i < numbers.size(); i++) {
            int current = numbers.get(i);
            if (current != prev + 1) {
                formattedList.add(formatRange(start, prev));
                start = current;
            }
            prev = current;
        }
        formattedList.add(formatRange(start, prev));

        return String.join("、", formattedList);
    }

    /**
     * 生成编号范围的字符串
     *
     * @param start 起始编号
     * @param end   结束编号
     * @return 生成的范围字符串，例如 "1号至3号" 或 "5号"
     */
    private String formatRange(int start, int end) {
        return (start == end) ? start + "号" : start + "号至" + end + "号";
    }

    /**
     * 构建检测意见，根据样品的检出与未检出情况，生成描述性文本。
     *
     * @param dataList 检测记录数据列表，每个对象包含样品编号、化合物名称及是否检出标识
     * @return 格式化的检测意见字符串
     */
    public String buildInspectOpinion(List<TestRecordSampleData> dataList) {
        // 1. **处理空数据情况**
        if (dataList == null || dataList.isEmpty()) {
            return "";
        }

        // 2. **定义存储结构**
        Map<String, List<String>> detectedMap = new LinkedHashMap<>();  // 记录检出化合物的样品编号映射（样品编号 -> 该样品检出的化合物列表）
        Map<String, List<String>> notDetectedMap = new LinkedHashMap<>();  // 记录未检出化合物的样品编号映射（样品编号 -> 该样品未检出的化合物列表）
        Set<String> allCompoundNames = new HashSet<>();  // 存储所有涉及的化合物名称（用于后续分析）

        // 3. **遍历数据列表，整理样品的检出与未检出情况**
        for (TestRecordSampleData record : dataList) {
            String sampleNo = record.getSampleNo();  // 样品编号
            String compoundName = record.getCompoundCnName();  // 化合物名称

            // **跳过无效数据**
            if (sampleNo == null || compoundName == null || sampleNo.trim().isEmpty()) {
                continue;
            }

            // **记录化合物名称**
            allCompoundNames.add(compoundName);

            // **分类存储检出与未检出数据**
            if (record.getIsDetected() != null && record.getIsDetected() == 1) {
                detectedMap.computeIfAbsent(sampleNo, k -> new ArrayList<>()).add(compoundName);
            } else {
                notDetectedMap.computeIfAbsent(sampleNo, k -> new ArrayList<>()).add(compoundName);
            }
        }

        // 4. **定义样品编号格式化方法**
        // 目标是将形如 "A12"、"B34" 这样的样品编号转换成 "12号"、"34号"
        Function<String, String> extractSampleNumber = sampleNo -> sampleNo.matches(".*\\d+")
                ? sampleNo.replaceAll(".*?(\\d+)$", "$1号")  // 只提取最后的数字并添加 "号"
                : sampleNo;

        // 5. **构建检出化合物的描述**
        List<String> detectedSentences = new ArrayList<>();
        Map<Set<String>, List<String>> groupedDetectedSamples = new LinkedHashMap<>();  // 用于按相同化合物组合分组样品

        // **分组整理检出样品**
        for (Map.Entry<String, List<String>> entry : detectedMap.entrySet()) {
            Set<String> compoundSet = new HashSet<>(entry.getValue());  // 将检出的化合物集合化（去重）
            groupedDetectedSamples.computeIfAbsent(compoundSet, k -> new ArrayList<>()).add(extractSampleNumber.apply(entry.getKey()));
        }

        // **遍历分组后的数据，生成描述文本**
        for (Map.Entry<Set<String>, List<String>> entry : groupedDetectedSamples.entrySet()) {
            List<String> sortedSampleNumbers = sortSampleNumbers(entry.getValue());  // 对样品编号排序
            String sampleNumbers = formatSampleNumbers(sortedSampleNumbers);  // 处理编号连续情况
            String compounds = String.join("、", entry.getKey());  // 多个化合物用 "、" 连接

            if (sortedSampleNumbers.size() > 1) {
                detectedSentences.add(String.format("从%s检材样品中均检出%s成分", sampleNumbers, compounds));
            } else {
                detectedSentences.add(String.format("从%s检材样品中检出%s成分", sampleNumbers, compounds));
            }
        }

        // 6. **构建未检出化合物的描述**
        List<String> notDetectedSentences = new ArrayList<>();
        Map<Set<String>, List<String>> groupedNotDetectedSamples = new LinkedHashMap<>();  // 用于按相同化合物组合分组未检出样品

        // **分组整理未检出样品**
        for (Map.Entry<String, List<String>> entry : notDetectedMap.entrySet()) {
            String sampleNo = entry.getKey();
            Set<String> compoundSet = new HashSet<>(entry.getValue());

            // 只有当该样品编号未出现在 detectedMap（即没有检出任何化合物）时，才记录未检出的情况
            if (!detectedMap.containsKey(sampleNo)) {
                groupedNotDetectedSamples.computeIfAbsent(compoundSet, k -> new ArrayList<>()).add(extractSampleNumber.apply(sampleNo));
            }
        }

        // **遍历分组后的数据，生成描述文本**
        for (Map.Entry<Set<String>, List<String>> entry : groupedNotDetectedSamples.entrySet()) {
            List<String> sortedSampleNumbers = sortSampleNumbers(entry.getValue());  // 对样品编号排序
            String sampleNumbers = formatSampleNumbers(sortedSampleNumbers);  // 处理编号连续情况
            String compounds = String.join("、", entry.getKey());  // 多个化合物用 "、" 连接

            if (sortedSampleNumbers.size() > 1) {
                notDetectedSentences.add(String.format("从%s检材样品中均未检出%s成分", sampleNumbers, compounds));
            } else {
                notDetectedSentences.add(String.format("从%s检材样品中未检出%s成分", sampleNumbers, compounds));
            }
        }

        // 7. **最终结果合并**
        List<String> finalSentences = new ArrayList<>(detectedSentences);  // 先添加检出的描述
        finalSentences.addAll(notDetectedSentences);  // 再添加未检出的描述

        // 使用 "；" 连接所有描述，并返回最终的检测意见字符串
        return String.join("；", finalSentences);
    }
}
