package com.njcn.gather.aireport.testreport.service.impl;

import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.aireport.reportmodel.component.ReportModelFileStorageService;
import com.njcn.gather.aireport.reportmodel.pojo.po.ReportModelPO;
import com.njcn.gather.aireport.reportmodel.service.ReportModelService;
import com.njcn.gather.aireport.testreport.component.TestReportPointExcelParser;
import com.njcn.gather.aireport.testreport.component.TestReportLedgerImportService;
import com.njcn.gather.aireport.testreport.component.TestReportLedgerTemplateService;
import com.njcn.gather.aireport.testreport.component.TestReportStorageService;
import com.njcn.gather.aireport.testreport.mapper.TestReportGroupReportMapper;
import com.njcn.gather.aireport.testreport.mapper.TestReportMapper;
import com.njcn.gather.aireport.testreport.mapper.TestReportPointMapper;
import com.njcn.gather.aireport.testreport.pojo.constant.TestReportConst;
import com.njcn.gather.aireport.testreport.pojo.param.TestReportGroupSaveParam;
import com.njcn.gather.aireport.testreport.pojo.param.TestReportParam;
import com.njcn.gather.aireport.testreport.pojo.po.TestReportGroupReportPO;
import com.njcn.gather.aireport.testreport.pojo.po.TestReportPO;
import com.njcn.gather.aireport.testreport.pojo.po.TestReportPointPO;
import com.njcn.gather.aireport.testreport.pojo.vo.TestReportExcelVO;
import com.njcn.gather.aireport.testreport.pojo.vo.TestReportGroupReportVO;
import com.njcn.gather.aireport.testreport.pojo.vo.TestReportLedgerImportResultVO;
import com.njcn.gather.aireport.testreport.pojo.vo.TestReportPointVO;
import com.njcn.gather.aireport.testreport.pojo.vo.TestReportVO;
import com.njcn.gather.aireport.testreport.service.TestReportService;
import com.njcn.gather.comservice.filepreview.FilePreviewService;
import com.njcn.gather.system.dictionary.pojo.po.DictData;
import com.njcn.gather.system.dictionary.pojo.po.DictType;
import com.njcn.gather.system.dictionary.service.IDictDataService;
import com.njcn.gather.system.dictionary.service.IDictTypeService;
import com.njcn.gather.system.pojo.constant.DictConst;
import com.njcn.gather.system.util.ExportFileNameUtil;
import com.njcn.web.factory.PageFactory;
import com.njcn.web.utils.ExcelUtil;
import com.njcn.web.utils.RequestUtil;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;

@Service
@Slf4j
public class TestReportServiceImpl extends ServiceImpl<TestReportMapper, TestReportPO> implements TestReportService {

    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    private static final Integer DICT_STATE_NORMAL = 1;
    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    private static final String MSG_ID_REQUIRED = "普测报告ID不能为空";
    private static final String MSG_REPORT_NOT_FOUND = "普测报告不存在或已删除";
    private static final String MSG_ONLY_EDITABLE_CAN_UPDATE =
            "仅编制或退回状态的普测报告允许编辑";
    private static final String MSG_ONLY_EDITABLE_CAN_AUDIT =
            "仅编制或退回状态的普测报告允许提交审核";
    private static final String MSG_START_TIME_INVALID = "开始时间格式不正确";
    private static final String MSG_END_TIME_INVALID = "结束时间格式不正确";
    private static final String MSG_PARAM_REQUIRED = "普测报告参数不能为空";
    private static final String MSG_NO_REQUIRED = "普测报告编号不能为空";
    private static final String MSG_NO_TOO_LONG = "普测报告编号不能超过32个字符";
    private static final String MSG_CLIENT_UNIT_REQUIRED = "委托单位不能为空";
    private static final String MSG_REPORT_MODEL_REQUIRED = "报告模板不能为空";
    private static final String MSG_CREATE_UNIT_REQUIRED = "检测公司不能为空";
    private static final String MSG_CONTRACT_REQUIRED = "合同编号不能为空";
    private static final String MSG_CONTRACT_TOO_LONG = "合同编号不能超过32个字符";
    private static final String MSG_STANDARD_REQUIRED = "检测依据不能为空";
    private static final String MSG_DATA_REQUIRED = "受资数据不能为空";
    private static final String MSG_DATA_INVALID = "受资数据格式不正确";
    private static final String MSG_PHONE_TOO_LONG = "联系电话不能超过32个字符";
    private static final String MSG_NO_EXISTS = "普测报告编号已存在";
    private static final String MSG_COMPANY_NOT_FOUND = "检测公司不存在或已删除";
    private static final String MSG_STANDARD_INVALID = "测试依据格式不正确";
    private static final String MSG_STANDARD_NOT_FOUND = "测试依据不存在或已删除";
    private static final String MSG_CLIENT_UNIT_NOT_FOUND = "委托单位不存在或已删除";
    private static final String MSG_REPORT_MODEL_NOT_FOUND = "报告模板不存在或已删除";
    private static final String MSG_PREVIEW_NOT_READY = "报告模板预览服务未初始化";
    private static final String MSG_TEMPLATE_FILE_NOT_FOUND = "报告模板文件不存在";
    private static final String MSG_ENCODE_FILE_NAME_FAILED = "编码报告模板文件名失败";
    private static final String MSG_ONLY_PENDING_CAN_IMPORT = "仅未生成任务允许导入";
    private static final String MSG_IMPORT_FILES_REQUIRED = "原始Excel文件不能为空";
    private static final String MSG_IMPORT_FILE_NAME_DUPLICATE = "原始Excel文件名不能重复";
    private static final String MSG_BATCH_ID_REQUIRED = "导入批次ID不能为空";
    private static final String MSG_GROUPS_REQUIRED = "分组列表不能为空";
    private static final String MSG_GROUP_POINT_DUPLICATE = "同一监测点不能重复分组";
    private static final String MSG_GROUP_POINT_MISSING = "所有监测点都必须完成分组";
    private static final String MSG_GROUP_NO_INVALID = "组号必须为正整数";
    private static final String MSG_GROUP_REPORT_NOT_FOUND = "分组报告不存在或已删除";
    private static final String MSG_GROUPS_NOT_READY = "请先完成监测点分组";

    private final ReportModelService reportModelService;
    private final ReportModelFileStorageService reportModelFileStorageService;
    private final FilePreviewService filePreviewService;
    private final IDictTypeService dictTypeService;
    private final IDictDataService dictDataService;
    private final TestReportPointMapper testReportPointMapper;
    private final TestReportGroupReportMapper testReportGroupReportMapper;
    private final TestReportPointExcelParser testReportPointExcelParser;
    private final TestReportStorageService testReportStorageService;
    private final TestReportLedgerImportService testReportLedgerImportService;
    private final TestReportLedgerTemplateService testReportLedgerTemplateService;

    public TestReportServiceImpl(ReportModelService reportModelService,
                                 ReportModelFileStorageService reportModelFileStorageService,
                                 FilePreviewService filePreviewService,
                                 IDictTypeService dictTypeService,
                                 IDictDataService dictDataService,
                                 TestReportPointMapper testReportPointMapper,
                                 TestReportGroupReportMapper testReportGroupReportMapper,
                                 TestReportPointExcelParser testReportPointExcelParser,
                                 TestReportStorageService testReportStorageService,
                                 TestReportLedgerImportService testReportLedgerImportService,
                                 TestReportLedgerTemplateService testReportLedgerTemplateService) {
        this.reportModelService = reportModelService;
        this.reportModelFileStorageService = reportModelFileStorageService;
        this.filePreviewService = filePreviewService;
        this.dictTypeService = dictTypeService;
        this.dictDataService = dictDataService;
        this.testReportPointMapper = testReportPointMapper;
        this.testReportGroupReportMapper = testReportGroupReportMapper;
        this.testReportPointExcelParser = testReportPointExcelParser;
        this.testReportStorageService = testReportStorageService;
        this.testReportLedgerImportService = testReportLedgerImportService;
        this.testReportLedgerTemplateService = testReportLedgerTemplateService;
    }

    @Override
    public Page<TestReportVO> listTestReports(TestReportParam.QueryParam param) {
        TestReportParam.QueryParam query = normalizeQueryParam(param);
        Page<TestReportVO> page = this.baseMapper.selectReportPage(
                new Page<TestReportVO>(PageFactory.getPageNum(query), PageFactory.getPageSize(query)), query);
        fillDisplayUserNames(page.getRecords(), buildUserNameMap(page.getRecords()));
        fillDisplayFields(page.getRecords());
        return page;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean addTestReport(TestReportParam.AddParam param) {
        NormalizedReport data = normalizeParam(param);
        checkNoUnique(data.getNo(), null);
        checkClientUnit(data.getClientUnitId());
        checkReportModel(data.getReportModelId());
        checkCreateUnit(data.getCreateUnit());
        checkStandard(data.getStandard());

        LocalDateTime now = LocalDateTime.now();
        String userId = resolveCurrentUserId();
        TestReportPO report = new TestReportPO();
        report.setId(resolveAddReportId(param));
        applyBusinessFields(report, data);
        report.setState(TestReportConst.STATE_EDITING);
        report.setReportGenerateState(TestReportConst.REPORT_GENERATE_STATE_PENDING);
        report.setStatus(TestReportConst.STATUS_NORMAL);
        report.setCreateBy(userId);
        report.setCreateTime(now);
        report.setUpdateBy(userId);
        report.setUpdateTime(now);
        boolean saved = this.save(report);
        if (!saved) {
            return false;
        }
        importLedgerIfNeeded(report, param.getBatchId(), userId);
        return true;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean updateTestReport(TestReportParam.UpdateParam param) {
        if (param == null || StrUtil.isBlank(param.getId())) {
            throw new BusinessException(CommonResponseEnum.FAIL, MSG_ID_REQUIRED);
        }
        TestReportPO report = requireNormal(param.getId());
        if (!allowEdit(report.getState())) {
            throw new BusinessException(CommonResponseEnum.FAIL, MSG_ONLY_EDITABLE_CAN_UPDATE);
        }
        NormalizedReport data = normalizeParam(param);
        checkNoUnique(data.getNo(), report.getId());
        checkClientUnit(data.getClientUnitId());
        checkReportModel(data.getReportModelId());
        checkCreateUnit(data.getCreateUnit());
        checkStandard(data.getStandard());

        applyBusinessFields(report, data);
        String userId = resolveCurrentUserId();
        report.setUpdateBy(userId);
        report.setUpdateTime(LocalDateTime.now());
        boolean updated = this.updateById(report);
        if (!updated) {
            return false;
        }
        importLedgerIfNeeded(report, param.getBatchId(), userId);
        return true;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean deleteTestReports(List<String> ids) {
        List<String> validIds = normalizeIds(ids);
        if (validIds.isEmpty()) {
            throw new BusinessException(CommonResponseEnum.FAIL, MSG_ID_REQUIRED);
        }
        List<TestReportPO> reports = this.lambdaQuery()
                .in(TestReportPO::getId, validIds)
                .eq(TestReportPO::getStatus, TestReportConst.STATUS_NORMAL)
                .list();
        String userId = resolveCurrentUserId();
        LocalDateTime now = LocalDateTime.now();
        boolean deleted = this.lambdaUpdate()
                .set(TestReportPO::getState, TestReportConst.STATE_DELETED)
                .set(TestReportPO::getStatus, TestReportConst.STATUS_DELETED)
                .set(TestReportPO::getUpdateBy, userId)
                .set(TestReportPO::getUpdateTime, now)
                .in(TestReportPO::getId, validIds)
                .eq(TestReportPO::getStatus, TestReportConst.STATUS_NORMAL)
                .update();
        if (deleted) {
            for (TestReportPO report : reports) {
                deletePointFiles(loadNormalPoints(report.getId()));
                deleteGroupReportFiles(loadNormalGroupReports(report.getId()));
                testReportStorageService.deleteQuietly(resolveSummaryStoragePath(report.getData()));
            }
        }
        return deleted;
    }

    @Override
    public TestReportVO getTestReport(String id) {
        TestReportVO report = this.baseMapper.selectReportDetail(requireText(id, MSG_ID_REQUIRED));
        if (report == null) {
            throw new BusinessException(CommonResponseEnum.FAIL, MSG_REPORT_NOT_FOUND);
        }
        fillDisplayUserNames(report, buildUserNameMap(Collections.singletonList(report)));
        fillDisplayFields(Collections.singletonList(report));
        return report;
    }

    @Override
    public TestReportPO requireNormal(String id) {
        String normalizedId = requireText(id, MSG_ID_REQUIRED);
        TestReportPO report = this.lambdaQuery()
                .eq(TestReportPO::getId, normalizedId)
                .eq(TestReportPO::getStatus, TestReportConst.STATUS_NORMAL)
                .one();
        if (report == null) {
            TestReportPO existingReport = this.getById(normalizedId);
            if (existingReport == null) {
                log.warn("testreport requireNormal failed: report not found, reportId={}", normalizedId);
            } else {
                log.warn("testreport requireNormal failed: report status invalid, reportId={}, status={}, state={}, reportGenerateState={}, updateTime={}",
                        normalizedId, existingReport.getStatus(), existingReport.getState(),
                        existingReport.getReportGenerateState(), existingReport.getUpdateTime());
            }
            throw new BusinessException(CommonResponseEnum.FAIL, MSG_REPORT_NOT_FOUND);
        }
        return report;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean submitAudit(TestReportParam.SubmitAuditParam param) {
        if (param == null || StrUtil.isBlank(param.getId())) {
            throw new BusinessException(CommonResponseEnum.FAIL, MSG_ID_REQUIRED);
        }
        TestReportPO report = requireNormal(param.getId());
        if (!allowEdit(report.getState())) {
            throw new BusinessException(CommonResponseEnum.FAIL, MSG_ONLY_EDITABLE_CAN_AUDIT);
        }
        report.setCheckId(null);
        report.setCheckTime(null);
        report.setCheckResult(null);
        report.setState(TestReportConst.STATE_AUDITING);
        report.setUpdateBy(resolveCurrentUserId());
        report.setUpdateTime(LocalDateTime.now());
        return this.updateById(report);
    }

    @Override
    public void exportTestReports(TestReportParam.QueryParam param) {
        TestReportParam.QueryParam query = normalizeQueryParam(param);
        Page<TestReportVO> page = this.baseMapper.selectReportPage(new Page<TestReportVO>(1, Integer.MAX_VALUE), query);
        fillDisplayUserNames(page.getRecords(), buildUserNameMap(page.getRecords()));
        fillDisplayFields(page.getRecords());
        List<TestReportExcelVO> exportRows = page.getRecords().stream()
                .map(this::toExcelVO)
                .collect(Collectors.toList());
        ExcelUtil.exportExcel(ExportFileNameUtil.appendToday("普测报告列表.xlsx"),
                "普测报告列表", TestReportExcelVO.class, exportRows);
    }

    @Override
    public ResponseEntity<byte[]> previewTemplate(String id) {
        TestReportPO report = requireNormal(id);
        ReportModelPO model = requireReportModel(report.getReportModelId());
        String fileName = resolvePreviewFileName(model);
        Path tempFile = reportModelFileStorageService.downloadToTempFile(model.getPath(), fileName);
        try {
            FilePreviewService.PreviewContent previewContent = filePreviewService.preview(tempFile, fileName);
            return ResponseEntity.ok()
                    .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename*=UTF-8''" + encodeFileName(fileName))
                    .header(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate")
                    .contentType(MediaType.parseMediaType(previewContent.getContentType()))
                    .body(previewContent.getContent());
        } finally {
            reportModelFileStorageService.deleteTempFileQuietly(tempFile);
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public TestReportLedgerImportResultVO validateLedger(String id, MultipartFile[] files) {
        String draftId = requireText(id, MSG_ID_REQUIRED);
        String userId = resolveCurrentUserId();
        log.info("testreport ledger validate start: draftId={}, userId={}, fileCount={}, fileNames={}",
                draftId, userId, files == null ? 0 : files.length, describeUploadedFiles(files));
        TestReportPO draftReport = new TestReportPO();
        draftReport.setId(draftId);
        return testReportLedgerImportService.validateLedger(draftReport, files, userId);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public TestReportLedgerImportResultVO importLedger(String id, TestReportParam.LedgerImportParam param) {
        TestReportPO report = requireNormal(id);
        ensureImportable(report);
        if (param == null || StrUtil.isBlank(param.getBatchId())) {
            throw new BusinessException(CommonResponseEnum.FAIL, MSG_BATCH_ID_REQUIRED);
        }
        log.info("testreport ledger import start: reportId={}, userId={}, batchId={}",
                id, resolveCurrentUserId(), param.getBatchId());
        return testReportLedgerImportService.importLedger(report, param.getBatchId(), resolveCurrentUserId());
    }

    @Override
    public ResponseEntity<byte[]> downloadLedgerTemplate() {
        byte[] content = testReportLedgerTemplateService.buildTemplate();
        return ResponseEntity.ok()
                .header(HttpHeaders.CONTENT_DISPOSITION,
                        "attachment; filename*=UTF-8''" + encodeFileName(TestReportConst.LEDGER_SUMMARY_FILE_NAME))
                .contentType(MediaType.parseMediaType(
                        "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"))
                .body(content);
    }

    @Override
    public List<TestReportPointVO> listPoints(String id) {
        requireNormal(id);
        return loadNormalPoints(id).stream().map(this::toPointVO).collect(Collectors.toList());
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean saveGroups(String id, TestReportGroupSaveParam param) {
        TestReportPO report = requireNormal(id);
        ensureImportable(report);
        List<TestReportPointPO> points = loadNormalPoints(id);
        validateGroupingPayload(points, param);
        String userId = resolveCurrentUserId();
        LocalDateTime now = LocalDateTime.now();
        Map<String, Integer> pointGroupMap = buildPointGroupMap(param);
        for (TestReportPointPO point : points) {
            point.setGroupNo(pointGroupMap.get(point.getId()));
            point.setUpdateBy(userId);
            point.setUpdateTime(now);
            testReportPointMapper.updateById(point);
        }
        rebuildGroupReports(report, param, userId, now);
        report.setData(updateImportSnapshotGroupCount(report.getData(), param.getGroups().size()));
        report.setUpdateBy(userId);
        report.setUpdateTime(now);
        return this.updateById(report);
    }

    @Override
    public List<TestReportGroupReportVO> listGroupReports(String id) {
        requireNormal(id);
        return loadNormalGroupReports(id).stream().map(this::toGroupReportVO).collect(Collectors.toList());
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean generateGroupedReports(String id) {
        TestReportPO report = requireNormal(id);
        ensureImportable(report);
        List<TestReportGroupReportPO> groupReports = loadNormalGroupReports(id);
        if (groupReports.isEmpty()) {
            throw new BusinessException(CommonResponseEnum.FAIL, MSG_GROUPS_NOT_READY);
        }
        report.setReportGenerateState(TestReportConst.REPORT_GENERATE_STATE_GENERATING);
        report.setUpdateBy(resolveCurrentUserId());
        report.setUpdateTime(LocalDateTime.now());
        this.updateById(report);
        try {
            for (TestReportGroupReportPO groupReport : groupReports) {
                generateSingleGroupReport(report, groupReport);
            }
            report.setReportGenerateState(TestReportConst.REPORT_GENERATE_STATE_FINISHED);
            report.setUpdateTime(LocalDateTime.now());
            return this.updateById(report);
        } catch (RuntimeException exception) {
            report.setReportGenerateState(TestReportConst.REPORT_GENERATE_STATE_PENDING);
            report.setUpdateTime(LocalDateTime.now());
            this.updateById(report);
            throw exception;
        }
    }

    @Override
    public ResponseEntity<byte[]> downloadGroupReport(String groupReportId) {
        TestReportGroupReportPO groupReport = requireNormalGroupReport(groupReportId);
        if (StrUtil.isBlank(groupReport.getReportStoragePath())) {
            throw new BusinessException(CommonResponseEnum.FAIL, MSG_TEMPLATE_FILE_NOT_FOUND);
        }
        try {
            byte[] bytes = testReportStorageService.readBusinessBytes(groupReport.getReportStoragePath());
            return ResponseEntity.ok()
                    .header(HttpHeaders.CONTENT_DISPOSITION,
                            "attachment; filename*=UTF-8''" + encodeFileName(groupReport.getReportFileName()))
                    .contentType(MediaType.APPLICATION_OCTET_STREAM)
                    .body(bytes);
        } catch (Exception exception) {
            throw new BusinessException(CommonResponseEnum.FAIL, MSG_TEMPLATE_FILE_NOT_FOUND);
        }
    }

    private TestReportParam.QueryParam normalizeQueryParam(TestReportParam.QueryParam param) {
        TestReportParam.QueryParam query = param == null ? new TestReportParam.QueryParam() : param;
        query.setKeyword(trimToNull(query.getKeyword()));
        if (StrUtil.isNotBlank(query.getSearchBeginTime())) {
            query.setSearchBeginTime(formatDateTime(parseDateTime(query.getSearchBeginTime(), MSG_START_TIME_INVALID)));
        }
        if (StrUtil.isNotBlank(query.getSearchEndTime())) {
            query.setSearchEndTime(formatDateTime(parseDateTime(query.getSearchEndTime(), MSG_END_TIME_INVALID)));
        }
        return query;
    }

    private NormalizedReport normalizeParam(TestReportParam.AddParam param) {
        if (param == null) {
            throw new BusinessException(CommonResponseEnum.FAIL, MSG_PARAM_REQUIRED);
        }
        NormalizedReport data = new NormalizedReport();
        data.setNo(requireText(param.getNo(), MSG_NO_REQUIRED, 32, MSG_NO_TOO_LONG));
        data.setClientUnitId(requireText(param.getClientUnitId(), MSG_CLIENT_UNIT_REQUIRED));
        data.setReportModelId(requireText(param.getReportModelId(), MSG_REPORT_MODEL_REQUIRED));
        data.setCreateUnit(requireText(param.getCreateUnit(), MSG_CREATE_UNIT_REQUIRED));
        data.setContractNumber(requireText(param.getContractNumber(), MSG_CONTRACT_REQUIRED, 32, MSG_CONTRACT_TOO_LONG));
        data.setStandard(requireJsonArrayText(param.getStandard(), MSG_STANDARD_REQUIRED, MSG_STANDARD_INVALID));
        data.setData(requireJsonObjectOrArrayText(param.getData(), MSG_DATA_REQUIRED, MSG_DATA_INVALID));
        data.setPhonenumber(trimToNull(param.getPhonenumber()));
        if (data.getPhonenumber() != null && data.getPhonenumber().length() > 32) {
            throw new BusinessException(CommonResponseEnum.FAIL, MSG_PHONE_TOO_LONG);
        }
        return data;
    }

    private void applyBusinessFields(TestReportPO report, NormalizedReport data) {
        report.setNo(data.getNo());
        report.setClientUnitId(data.getClientUnitId());
        report.setReportModelId(data.getReportModelId());
        report.setCreateUnit(data.getCreateUnit());
        report.setContractNumber(data.getContractNumber());
        report.setStandard(data.getStandard());
        report.setData(data.getData());
        report.setPhonenumber(data.getPhonenumber());
    }

    private String resolveAddReportId(TestReportParam.AddParam param) {
        String frontendId = param == null ? null : trimToNull(param.getId());
        return frontendId == null ? UUID.randomUUID().toString() : frontendId;
    }

    private boolean allowEdit(String state) {
        return TestReportConst.STATE_EDITING.equals(state) || TestReportConst.STATE_REJECTED.equals(state);
    }

    private void ensureImportable(TestReportPO report) {
        Integer reportGenerateState = report.getReportGenerateState();
        if (reportGenerateState != null && !TestReportConst.REPORT_GENERATE_STATE_PENDING.equals(reportGenerateState)) {
            throw new BusinessException(CommonResponseEnum.FAIL, MSG_ONLY_PENDING_CAN_IMPORT);
        }
    }

    private List<MultipartFile> normalizeImportFiles(MultipartFile[] files) {
        if (files == null || files.length == 0) {
            throw new BusinessException(CommonResponseEnum.FAIL, MSG_IMPORT_FILES_REQUIRED);
        }
        List<MultipartFile> result = new ArrayList<MultipartFile>();
        Set<String> fileNames = new HashSet<String>();
        for (MultipartFile file : files) {
            if (file == null || file.isEmpty()) {
                continue;
            }
            String originalFileName = trimToNull(file.getOriginalFilename());
            if (originalFileName == null) {
                throw new BusinessException(CommonResponseEnum.FAIL, MSG_IMPORT_FILES_REQUIRED);
            }
            String normalizedFileName = originalFileName.toLowerCase();
            if (!normalizedFileName.endsWith(".xls") && !normalizedFileName.endsWith(".xlsx")) {
                throw new BusinessException(CommonResponseEnum.FAIL, "原始Excel仅允许xls/xlsx格式");
            }
            if (!fileNames.add(originalFileName)) {
                throw new BusinessException(CommonResponseEnum.FAIL, MSG_IMPORT_FILE_NAME_DUPLICATE);
            }
            result.add(file);
        }
        if (result.isEmpty()) {
            throw new BusinessException(CommonResponseEnum.FAIL, MSG_IMPORT_FILES_REQUIRED);
        }
        return result;
    }

    private List<TestReportPointExcelParser.ParsedPoint> parseAllPoints(List<MultipartFile> files) {
        List<TestReportPointExcelParser.ParsedPoint> result = new ArrayList<TestReportPointExcelParser.ParsedPoint>();
        for (MultipartFile file : files) {
            try {
                result.add(testReportPointExcelParser.parse(file.getOriginalFilename(), file.getInputStream()));
            } catch (IOException exception) {
                throw new BusinessException(CommonResponseEnum.FAIL, "读取原始Excel失败");
            }
        }
        return result;
    }

    private void overwriteReportPoints(TestReportPO report, List<MultipartFile> files,
                                       List<TestReportPointExcelParser.ParsedPoint> parsedPoints) {
        List<TestReportPointPO> oldPoints = loadNormalPoints(report.getId());
        List<TestReportGroupReportPO> oldGroupReports = loadNormalGroupReports(report.getId());
        deletePointFiles(oldPoints);
        deleteGroupReportFiles(oldGroupReports);
        removeOldRows(report.getId());

        LocalDateTime now = LocalDateTime.now();
        String userId = resolveCurrentUserId();
        for (int i = 0; i < files.size(); i++) {
            MultipartFile file = files.get(i);
            TestReportPointExcelParser.ParsedPoint parsedPoint = parsedPoints.get(i);
            String storagePath = testReportStorageService.saveSourceExcel(report.getId(), file);
            TestReportPointPO point = new TestReportPointPO();
            point.setId(UUID.randomUUID().toString());
            point.setReportId(report.getId());
            point.setGroupNo(null);
            point.setSubstationName(parsedPoint.getSubstationName());
            point.setPointName(parsedPoint.getPointName());
            point.setMonitorTime(parsedPoint.getMonitorTime());
            point.setVoltageLevel(parsedPoint.getVoltageLevel());
            point.setPtRatio(parsedPoint.getPtRatio());
            point.setCtRatio(parsedPoint.getCtRatio());
            point.setBaseShortCircuitCapacity(parsedPoint.getBaseShortCircuitCapacity());
            point.setMinShortCircuitCapacity(parsedPoint.getMinShortCircuitCapacity());
            point.setProtocolCapacity(parsedPoint.getProtocolCapacity());
            point.setPowerSupplyCapacity(parsedPoint.getPowerSupplyCapacity());
            point.setExcelAttachmentName(parsedPoint.getExcelAttachmentName());
            point.setExcelStoragePath(storagePath);
            point.setSortNo(i + 1);
            point.setDeleted(TestReportConst.DELETED_NO);
            point.setCreateBy(userId);
            point.setCreateTime(now);
            point.setUpdateBy(userId);
            point.setUpdateTime(now);
            testReportPointMapper.insert(point);
        }
        report.setData(buildImportSnapshotJson(parsedPoints.size(), 0));
        report.setReportGenerateState(TestReportConst.REPORT_GENERATE_STATE_PENDING);
        report.setUpdateBy(userId);
        report.setUpdateTime(now);
        this.updateById(report);
    }

    private void deletePointFiles(List<TestReportPointPO> oldPoints) {
        for (TestReportPointPO oldPoint : oldPoints) {
            testReportStorageService.deleteQuietly(oldPoint.getExcelStoragePath());
            testReportStorageService.deleteQuietly(oldPoint.getWordStoragePath());
        }
    }

    private void deleteGroupReportFiles(List<TestReportGroupReportPO> oldGroupReports) {
        for (TestReportGroupReportPO oldGroupReport : oldGroupReports) {
            testReportStorageService.deleteQuietly(oldGroupReport.getReportStoragePath());
        }
    }

    private String resolveSummaryStoragePath(String data) {
        if (StrUtil.isBlank(data)) {
            return null;
        }
        try {
            String summaryStoragePath = OBJECT_MAPPER.readTree(data).path("summaryStoragePath").asText();
            return StrUtil.isBlank(summaryStoragePath) ? null : summaryStoragePath;
        } catch (Exception exception) {
            return null;
        }
    }

    private void removeOldRows(String reportId) {
        LambdaQueryWrapper<TestReportPointPO> pointWrapper = new LambdaQueryWrapper<TestReportPointPO>();
        pointWrapper.eq(TestReportPointPO::getReportId, reportId);
        testReportPointMapper.delete(pointWrapper);

        LambdaQueryWrapper<TestReportGroupReportPO> groupReportWrapper = new LambdaQueryWrapper<TestReportGroupReportPO>();
        groupReportWrapper.eq(TestReportGroupReportPO::getReportId, reportId);
        testReportGroupReportMapper.delete(groupReportWrapper);
    }

    private String buildImportSnapshotJson(int pointCount, int groupCount) {
        ObjectNode root = OBJECT_MAPPER.createObjectNode();
        root.put("version", "1.0");
        root.put("importMode", "multi-source-excel-auto-parse");
        root.put("fileCount", pointCount);
        root.put("pointCount", pointCount);
        root.put("groupCount", groupCount);
        return root.toString();
    }

    private String updateImportSnapshotGroupCount(String data, int groupCount) {
        try {
            JsonNode jsonNode = StrUtil.isBlank(data) ? OBJECT_MAPPER.createObjectNode() : OBJECT_MAPPER.readTree(data);
            ObjectNode root = jsonNode instanceof ObjectNode ? (ObjectNode) jsonNode : OBJECT_MAPPER.createObjectNode();
            if (!root.has("version")) {
                root.put("version", "1.0");
            }
            if (!root.has("importMode")) {
                root.put("importMode", "multi-source-excel-auto-parse");
            }
            root.put("groupCount", groupCount);
            return root.toString();
        } catch (Exception exception) {
            return buildImportSnapshotJson(loadNormalPointsCountFromData(data), groupCount);
        }
    }

    private int loadNormalPointsCountFromData(String data) {
        try {
            if (StrUtil.isBlank(data)) {
                return 0;
            }
            JsonNode jsonNode = OBJECT_MAPPER.readTree(data);
            JsonNode pointCountNode = jsonNode.get("pointCount");
            return pointCountNode == null ? 0 : pointCountNode.asInt(0);
        } catch (Exception exception) {
            return 0;
        }
    }

    private List<TestReportPointPO> loadNormalPoints(String reportId) {
        LambdaQueryWrapper<TestReportPointPO> wrapper = new LambdaQueryWrapper<TestReportPointPO>();
        wrapper.eq(TestReportPointPO::getReportId, reportId)
                .eq(TestReportPointPO::getDeleted, TestReportConst.DELETED_NO)
                .orderByAsc(TestReportPointPO::getSortNo)
                .orderByAsc(TestReportPointPO::getCreateTime);
        return testReportPointMapper.selectList(wrapper);
    }

    private List<TestReportGroupReportPO> loadNormalGroupReports(String reportId) {
        LambdaQueryWrapper<TestReportGroupReportPO> wrapper = new LambdaQueryWrapper<TestReportGroupReportPO>();
        wrapper.eq(TestReportGroupReportPO::getReportId, reportId)
                .eq(TestReportGroupReportPO::getDeleted, TestReportConst.DELETED_NO)
                .orderByAsc(TestReportGroupReportPO::getSortNo)
                .orderByAsc(TestReportGroupReportPO::getGroupNo);
        return testReportGroupReportMapper.selectList(wrapper);
    }

    private TestReportGroupReportPO requireNormalGroupReport(String groupReportId) {
        LambdaQueryWrapper<TestReportGroupReportPO> wrapper = new LambdaQueryWrapper<TestReportGroupReportPO>();
        wrapper.eq(TestReportGroupReportPO::getId, requireText(groupReportId, MSG_ID_REQUIRED))
                .eq(TestReportGroupReportPO::getDeleted, TestReportConst.DELETED_NO);
        TestReportGroupReportPO groupReport = testReportGroupReportMapper.selectOne(wrapper);
        if (groupReport == null) {
            throw new BusinessException(CommonResponseEnum.FAIL, MSG_GROUP_REPORT_NOT_FOUND);
        }
        return groupReport;
    }

    private void validateGroupingPayload(List<TestReportPointPO> points, TestReportGroupSaveParam param) {
        if (param == null || param.getGroups() == null || param.getGroups().isEmpty()) {
            throw new BusinessException(CommonResponseEnum.FAIL, MSG_GROUPS_REQUIRED);
        }
        Set<String> pointIds = points.stream().map(TestReportPointPO::getId).collect(Collectors.toSet());
        Set<String> payloadPointIds = new HashSet<String>();
        for (TestReportGroupSaveParam.GroupItem group : param.getGroups()) {
            if (group.getGroupNo() == null || group.getGroupNo() <= 0) {
                throw new BusinessException(CommonResponseEnum.FAIL, MSG_GROUP_NO_INVALID);
            }
            for (String pointId : group.getPointIds()) {
                if (!payloadPointIds.add(pointId)) {
                    throw new BusinessException(CommonResponseEnum.FAIL, MSG_GROUP_POINT_DUPLICATE);
                }
            }
        }
        if (!payloadPointIds.equals(pointIds)) {
            throw new BusinessException(CommonResponseEnum.FAIL, MSG_GROUP_POINT_MISSING);
        }
    }

    private Map<String, Integer> buildPointGroupMap(TestReportGroupSaveParam param) {
        Map<String, Integer> result = new LinkedHashMap<String, Integer>();
        for (TestReportGroupSaveParam.GroupItem group : param.getGroups()) {
            for (String pointId : group.getPointIds()) {
                result.put(pointId, group.getGroupNo());
            }
        }
        return result;
    }

    private void rebuildGroupReports(TestReportPO report, TestReportGroupSaveParam param, String userId, LocalDateTime now) {
        deleteGroupReportFiles(loadNormalGroupReports(report.getId()));
        LambdaQueryWrapper<TestReportGroupReportPO> wrapper = new LambdaQueryWrapper<TestReportGroupReportPO>();
        wrapper.eq(TestReportGroupReportPO::getReportId, report.getId());
        testReportGroupReportMapper.delete(wrapper);
        int sortNo = 1;
        for (TestReportGroupSaveParam.GroupItem group : param.getGroups()) {
            TestReportGroupReportPO groupReport = new TestReportGroupReportPO();
            groupReport.setId(UUID.randomUUID().toString());
            groupReport.setReportId(report.getId());
            groupReport.setGroupNo(group.getGroupNo());
            groupReport.setGenerateState(TestReportConst.GROUP_REPORT_GENERATE_STATE_PENDING);
            groupReport.setSortNo(sortNo++);
            groupReport.setDeleted(TestReportConst.DELETED_NO);
            groupReport.setCreateBy(userId);
            groupReport.setCreateTime(now);
            groupReport.setUpdateBy(userId);
            groupReport.setUpdateTime(now);
            testReportGroupReportMapper.insert(groupReport);
        }
    }

    private void generateSingleGroupReport(TestReportPO report, TestReportGroupReportPO groupReport) {
        try {
            groupReport.setGenerateState(TestReportConst.GROUP_REPORT_GENERATE_STATE_GENERATING);
            groupReport.setGenerateMessage(null);
            groupReport.setUpdateBy(resolveCurrentUserId());
            groupReport.setUpdateTime(LocalDateTime.now());
            testReportGroupReportMapper.updateById(groupReport);

            String reportName = report.getNo() + "-第" + groupReport.getGroupNo() + "组检测报告";
            String reportFileName = ExportFileNameUtil.appendToday(reportName + ".docx");
            String relativePath = testReportStorageService.saveGeneratedPlaceholder(report.getId(), report.getMultiId(), reportFileName,
                    ("group:" + groupReport.getGroupNo()).getBytes(StandardCharsets.UTF_8));
            groupReport.setReportName(reportName);
            groupReport.setReportFileName(reportFileName);
            groupReport.setReportStoragePath(relativePath);
            groupReport.setGenerateState(TestReportConst.GROUP_REPORT_GENERATE_STATE_SUCCESS);
            groupReport.setGenerateTime(LocalDateTime.now());
            groupReport.setUpdateTime(LocalDateTime.now());
            testReportGroupReportMapper.updateById(groupReport);
        } catch (RuntimeException exception) {
            groupReport.setGenerateState(TestReportConst.GROUP_REPORT_GENERATE_STATE_FAILED);
            groupReport.setGenerateMessage(exception.getMessage());
            groupReport.setUpdateTime(LocalDateTime.now());
            testReportGroupReportMapper.updateById(groupReport);
            throw exception;
        }
    }

    private TestReportPointVO toPointVO(TestReportPointPO point) {
        TestReportPointVO vo = new TestReportPointVO();
        vo.setId(point.getId());
        vo.setGroupNo(point.getGroupNo());
        vo.setSubstationName(defaultDash(point.getSubstationName()));
        vo.setPointName(defaultDash(point.getPointName()));
        vo.setMonitorTime(defaultDash(point.getMonitorTime()));
        vo.setVoltageLevel(defaultDash(point.getVoltageLevel()));
        vo.setPtRatio(defaultDash(point.getPtRatio()));
        vo.setCtRatio(defaultDash(point.getCtRatio()));
        vo.setBaseShortCircuitCapacity(defaultDash(point.getBaseShortCircuitCapacity()));
        vo.setMinShortCircuitCapacity(defaultDash(point.getMinShortCircuitCapacity()));
        vo.setProtocolCapacity(defaultDash(point.getProtocolCapacity()));
        vo.setPowerSupplyCapacity(defaultDash(point.getPowerSupplyCapacity()));
        vo.setExcelAttachmentName(defaultDash(point.getExcelAttachmentName()));
        vo.setExcelAttachmentRelativePath(defaultDash(point.getExcelAttachmentRelativePath()));
        vo.setWordAttachmentName(defaultDash(point.getWordAttachmentName()));
        vo.setWordAttachmentRelativePath(defaultDash(point.getWordAttachmentRelativePath()));
        return vo;
    }

    private TestReportGroupReportVO toGroupReportVO(TestReportGroupReportPO groupReport) {
        TestReportGroupReportVO vo = new TestReportGroupReportVO();
        vo.setId(groupReport.getId());
        vo.setGroupNo(groupReport.getGroupNo());
        vo.setReportName(defaultDash(groupReport.getReportName()));
        vo.setReportFileName(defaultDash(groupReport.getReportFileName()));
        vo.setReportStoragePath(defaultDash(groupReport.getReportStoragePath()));
        vo.setGenerateState(groupReport.getGenerateState());
        vo.setGenerateMessage(defaultDash(groupReport.getGenerateMessage()));
        vo.setGenerateTime(groupReport.getGenerateTime());
        vo.setSortNo(groupReport.getSortNo());
        return vo;
    }

    private String defaultDash(String value) {
        return StrUtil.isBlank(value) ? "-" : value;
    }

    private void checkNoUnique(String no, String excludeId) {
        LambdaQueryWrapper<TestReportPO> wrapper = new LambdaQueryWrapper<TestReportPO>();
        wrapper.eq(TestReportPO::getNo, no)
                .eq(TestReportPO::getStatus, TestReportConst.STATUS_NORMAL)
                .ne(StrUtil.isNotBlank(excludeId), TestReportPO::getId, excludeId);
        if (this.count(wrapper) > 0) {
            throw new BusinessException(CommonResponseEnum.FAIL, MSG_NO_EXISTS);
        }
    }

    private TestReportExcelVO toExcelVO(TestReportVO vo) {
        TestReportExcelVO excelVO = new TestReportExcelVO();
        excelVO.setNo(vo.getNo());
        excelVO.setClientUnitName(StrUtil.isNotBlank(vo.getClientUnitName()) ? vo.getClientUnitName() : vo.getClientUnitId());
        excelVO.setReportModelName(StrUtil.isNotBlank(vo.getReportModelName()) ? vo.getReportModelName() : vo.getReportModelId());
        excelVO.setCreateUnit(vo.getCreateUnit());
        excelVO.setContractNumber(vo.getContractNumber());
        excelVO.setStandard(vo.getStandard());
        excelVO.setData(vo.getData());
        excelVO.setPhonenumber(vo.getPhonenumber());
        excelVO.setCheckerName(StrUtil.isNotBlank(vo.getCheckerName()) ? vo.getCheckerName() : vo.getCheckId());
        excelVO.setCheckTime(vo.getCheckTime() == null ? null : vo.getCheckTime().toString());
        excelVO.setCheckResult(vo.getCheckResult());
        excelVO.setState(vo.getState());
        excelVO.setCreateTime(vo.getCreateTime() == null ? null : DATE_TIME_FORMATTER.format(vo.getCreateTime()));
        excelVO.setCreateByName(StrUtil.isNotBlank(vo.getCreateByName()) ? vo.getCreateByName() : vo.getCreateBy());
        return excelVO;
    }

    void fillDisplayUserNames(List<TestReportVO> reports, Map<String, String> userNameMap) {
        if (reports == null || reports.isEmpty()) {
            return;
        }
        for (TestReportVO report : reports) {
            fillDisplayUserNames(report, userNameMap);
        }
    }

    void fillDisplayUserNames(TestReportVO report, Map<String, String> userNameMap) {
        if (report == null) {
            return;
        }
        report.setCreateBy(resolveDisplayUserName(report.getCreateBy(), userNameMap));
        report.setUpdateBy(resolveDisplayUserName(report.getUpdateBy(), userNameMap));
    }

    void fillDisplayFields(List<TestReportVO> reports) {
        if (reports == null || reports.isEmpty()) {
            return;
        }
        Map<String, String> companyNameMap = buildDictNameMap(DictConst.TEST_REPORT_COMPANY_DICT_CODE);
        Map<String, String> standardNameMap = buildDictNameMap(DictConst.TEST_REPORT_STANDARD_DICT_CODE);
        for (TestReportVO report : reports) {
            if (report == null) {
                continue;
            }
            report.setCreateUnit(resolveDictDisplayValue(report.getCreateUnit(), companyNameMap));
            report.setStandard(resolveStandardDisplayValue(report.getStandard(), standardNameMap));
        }
    }

    private Map<String, String> buildUserNameMap(Collection<TestReportVO> reports) {
        if (reports == null || reports.isEmpty()) {
            return Collections.emptyMap();
        }
        Map<String, String> userNameMap = new LinkedHashMap<String, String>();
        for (TestReportVO report : reports) {
            if (report == null) {
                continue;
            }
            putUserName(userNameMap, report.getCreateBy(), report.getCreateByName());
            putUserName(userNameMap, report.getUpdateBy(), report.getUpdateByName());
            putUserName(userNameMap, report.getCheckId(), report.getCheckerName());
        }
        return userNameMap;
    }

    private Map<String, String> buildDictNameMap(String typeCode) {
        if (dictTypeService == null || dictDataService == null || StrUtil.isBlank(typeCode)) {
            return Collections.emptyMap();
        }
        DictType dictType = dictTypeService.getByCode(typeCode);
        if (dictType == null || StrUtil.isBlank(dictType.getId()) || !DICT_STATE_NORMAL.equals(dictType.getState())) {
            return Collections.emptyMap();
        }
        List<DictData> dictDataList = dictDataService.listDictDataByTypeId(dictType.getId());
        if (dictDataList == null || dictDataList.isEmpty()) {
            return Collections.emptyMap();
        }
        Map<String, String> result = new LinkedHashMap<String, String>();
        for (DictData dictData : dictDataList) {
            if (dictData == null || StrUtil.isBlank(dictData.getId()) || StrUtil.isBlank(dictData.getName())
                    || !DICT_STATE_NORMAL.equals(dictData.getState())) {
                continue;
            }
            result.put(dictData.getId(), dictData.getName());
        }
        return result;
    }

    private void putUserName(Map<String, String> userNameMap, String userId, String userName) {
        String validUserId = trimToNull(userId);
        String validUserName = trimToNull(userName);
        if (validUserId == null || validUserName == null || "-".equals(validUserName)) {
            return;
        }
        userNameMap.put(validUserId, validUserName);
    }

    private String resolveDisplayUserName(String userId, Map<String, String> userNameMap) {
        String validUserId = trimToNull(userId);
        if (validUserId == null) {
            return null;
        }
        if (userNameMap == null || userNameMap.isEmpty()) {
            return validUserId;
        }
        String userName = trimToNull(userNameMap.get(validUserId));
        return userName == null ? validUserId : userName;
    }

    private String resolveDictDisplayValue(String rawValue, Map<String, String> dictNameMap) {
        String validValue = trimToNull(rawValue);
        if (validValue == null) {
            return null;
        }
        if (dictNameMap == null || dictNameMap.isEmpty()) {
            return validValue;
        }
        String displayValue = trimToNull(dictNameMap.get(validValue));
        return displayValue == null ? validValue : displayValue;
    }

    private String resolveStandardDisplayValue(String rawValue, Map<String, String> dictNameMap) {
        String validValue = trimToNull(rawValue);
        if (validValue == null) {
            return null;
        }
        List<String> standardIds = parseJsonArray(validValue);
        if (standardIds.isEmpty()) {
            return validValue;
        }
        List<String> displayValues = new ArrayList<String>();
        for (String standardId : standardIds) {
            String displayValue = resolveDictDisplayValue(standardId, dictNameMap);
            if (displayValue != null) {
                displayValues.add(displayValue);
            }
        }
        if (displayValues.isEmpty()) {
            return validValue;
        }
        return String.join(", ", displayValues);
    }

    private List<String> parseJsonArray(String jsonArrayText) {
        String text = trimToNull(jsonArrayText);
        if (text == null) {
            return Collections.emptyList();
        }
        try {
            List<String> parsedItems = OBJECT_MAPPER.readValue(text, new TypeReference<List<String>>() {
            });
            if (parsedItems == null || parsedItems.isEmpty()) {
                return Collections.emptyList();
            }
            List<String> result = new ArrayList<String>();
            for (String item : parsedItems) {
                String normalizedItem = trimToNull(item);
                if (normalizedItem != null) {
                    result.add(normalizedItem);
                }
            }
            return result;
        } catch (Exception exception) {
            return Collections.emptyList();
        }
    }

    private String requireJsonArrayText(List<String> value, String blankMessage, String invalidMessage) {
        if (value == null || value.isEmpty()) {
            throw new BusinessException(CommonResponseEnum.FAIL, blankMessage);
        }
        try {
            ArrayList<String> normalizedItems = new ArrayList<String>();
            for (String item : value) {
                String normalizedItem = trimToNull(item);
                if (normalizedItem != null) {
                    normalizedItems.add(normalizedItem);
                }
            }
            if (normalizedItems.isEmpty()) {
                throw new BusinessException(CommonResponseEnum.FAIL, invalidMessage);
            }
            return OBJECT_MAPPER.writeValueAsString(normalizedItems);
        } catch (BusinessException exception) {
            throw exception;
        } catch (Exception exception) {
            throw new BusinessException(CommonResponseEnum.FAIL, invalidMessage);
        }
    }

    private String requireJsonObjectOrArrayText(JsonNode value, String blankMessage, String invalidMessage) {
        if (value == null || value.isNull()) {
            throw new BusinessException(CommonResponseEnum.FAIL, blankMessage);
        }
        try {
            if (!(value.isObject() || value.isArray())) {
                throw new BusinessException(CommonResponseEnum.FAIL, invalidMessage);
            }
            return OBJECT_MAPPER.writeValueAsString(value);
        } catch (BusinessException exception) {
            throw exception;
        } catch (Exception exception) {
            throw new BusinessException(CommonResponseEnum.FAIL, invalidMessage);
        }
    }

    private void checkCreateUnit(String createUnit) {
        Map<String, String> companyNameMap = buildDictNameMap(DictConst.TEST_REPORT_COMPANY_DICT_CODE);
        if (!companyNameMap.containsKey(createUnit)) {
            throw new BusinessException(CommonResponseEnum.FAIL, MSG_COMPANY_NOT_FOUND);
        }
    }

    private void checkStandard(String standard) {
        List<String> standardIds = parseJsonArray(standard);
        if (standardIds.isEmpty()) {
            throw new BusinessException(CommonResponseEnum.FAIL, MSG_STANDARD_INVALID);
        }
        Map<String, String> standardNameMap = buildDictNameMap(DictConst.TEST_REPORT_STANDARD_DICT_CODE);
        for (String standardId : standardIds) {
            if (!standardNameMap.containsKey(standardId)) {
                throw new BusinessException(CommonResponseEnum.FAIL, MSG_STANDARD_NOT_FOUND);
            }
        }
    }

    private void checkClientUnit(String id) {
        if (this.baseMapper.countNormalClientUnit(id) <= 0) {
            throw new BusinessException(CommonResponseEnum.FAIL, MSG_CLIENT_UNIT_NOT_FOUND);
        }
    }

    private void checkReportModel(String id) {
        if (this.baseMapper.countNormalReportModel(id) <= 0) {
            throw new BusinessException(CommonResponseEnum.FAIL, MSG_REPORT_MODEL_NOT_FOUND);
        }
    }

    private ReportModelPO requireReportModel(String id) {
        if (reportModelService == null || reportModelFileStorageService == null || filePreviewService == null) {
            throw new BusinessException(CommonResponseEnum.FAIL, MSG_PREVIEW_NOT_READY);
        }
        return reportModelService.requireNormal(id);
    }

    private String resolvePreviewFileName(ReportModelPO model) {
        String fileName = trimToNull(model.getFileName());
        if (fileName != null) {
            return fileName;
        }
        return reportModelFileStorageService.resolveDownloadFileName(model.getPath());
    }

    private List<String> normalizeIds(List<String> ids) {
        if (ids == null || ids.isEmpty()) {
            return Collections.emptyList();
        }
        List<String> result = new ArrayList<String>();
        for (String id : ids) {
            String normalizedId = trimToNull(id);
            if (normalizedId != null && !result.contains(normalizedId)) {
                result.add(normalizedId);
            }
        }
        return result;
    }

    private LocalDateTime parseDateTime(String value, String errorMessage) {
        try {
            return LocalDateTime.parse(value.trim(), DATE_TIME_FORMATTER);
        } catch (DateTimeParseException exception) {
            throw new BusinessException(CommonResponseEnum.FAIL, errorMessage);
        }
    }

    private String formatDateTime(LocalDateTime value) {
        return DATE_TIME_FORMATTER.format(value);
    }

    private String requireText(String value, String blankMessage) {
        String text = trimToNull(value);
        if (text == null) {
            throw new BusinessException(CommonResponseEnum.FAIL, blankMessage);
        }
        return text;
    }

    private String requireText(String value, String blankMessage, int maxLength, String lengthMessage) {
        String text = requireText(value, blankMessage);
        if (text.length() > maxLength) {
            throw new BusinessException(CommonResponseEnum.FAIL, lengthMessage);
        }
        return text;
    }

    private String resolveCurrentUserId() {
        String userId = RequestUtil.getUserId();
        return StrUtil.isBlank(userId) ? null : userId;
    }

    private List<String> describeUploadedFiles(MultipartFile[] files) {
        if (files == null || files.length == 0) {
            return Collections.emptyList();
        }
        List<String> fileNames = new ArrayList<String>();
        for (MultipartFile file : files) {
            if (file == null) {
                fileNames.add("null");
                continue;
            }
            String fileName = trimToNull(file.getOriginalFilename());
            fileNames.add(fileName == null ? "(blank)" : fileName);
        }
        return fileNames;
    }

    private void importLedgerIfNeeded(TestReportPO report, String batchId, String userId) {
        if (report == null || StrUtil.isBlank(batchId)) {
            return;
        }
        testReportLedgerImportService.importLedger(report, batchId, userId);
    }

    private String trimToNull(String value) {
        if (value == null) {
            return null;
        }
        String text = value.trim();
        if (text.isEmpty() || "-".equals(text)) {
            return null;
        }
        return text;
    }

    private String encodeFileName(String fileName) {
        try {
            return URLEncoder.encode(fileName, StandardCharsets.UTF_8.name()).replace("+", "%20");
        } catch (Exception exception) {
            throw new BusinessException(CommonResponseEnum.FAIL, MSG_ENCODE_FILE_NAME_FAILED);
        }
    }

    @Data
    private static class NormalizedReport {
        private String no;
        private String clientUnitId;
        private String reportModelId;
        private String createUnit;
        private String contractNumber;
        private String standard;
        private String data;
        private String phonenumber;
    }
}
