package com.njcn.gather.aireport.testreportmulti.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.fasterxml.jackson.core.type.TypeReference;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.aireport.clientunit.mapper.ClientUnitMapper;
import com.njcn.gather.aireport.clientunit.pojo.constant.ClientUnitConst;
import com.njcn.gather.aireport.clientunit.pojo.po.ClientUnitPO;
import com.njcn.gather.aireport.testdevice.mapper.TestDeviceMapper;
import com.njcn.gather.aireport.testdevice.pojo.constant.TestDeviceConst;
import com.njcn.gather.aireport.testdevice.pojo.po.TestDevicePO;
import com.njcn.gather.aireport.testdevice.pojo.vo.TestDeviceTypeNameVO;
import com.njcn.gather.aireport.testreport.component.TestReportLedgerExcelParser;
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.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.TestReportVO;
import com.njcn.gather.aireport.testreport.service.TestReportService;
import com.njcn.gather.aireport.testreport.component.TestReportStorageService;
import com.njcn.gather.aireport.testreportmulti.component.TestReportMultiStorageService;
import com.njcn.gather.aireport.testreportmulti.component.TestReportMultiZipParser;
import com.njcn.gather.aireport.testreportmulti.mapper.TestReportMultiMapper;
import com.njcn.gather.aireport.testreportmulti.pojo.constant.TestReportMultiConst;
import com.njcn.gather.aireport.testreportmulti.pojo.param.TestReportMultiParam;
import com.njcn.gather.aireport.testreportmulti.pojo.po.TestReportMultiPO;
import com.njcn.gather.aireport.testreportmulti.pojo.vo.TestReportMultiTemplateDirVO;
import com.njcn.gather.aireport.testreportmulti.pojo.vo.TestReportMultiVO;
import com.njcn.gather.aireport.testreportmulti.pojo.vo.TestReportMultiValidateResultVO;
import com.njcn.gather.aireport.testreportmulti.service.TestReportMultiService;
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.RequestUtil;
import lombok.Data;
import lombok.RequiredArgsConstructor;
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.ByteArrayOutputStream;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.io.ByteArrayInputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

@Service
@RequiredArgsConstructor
public class TestReportMultiServiceImpl extends ServiceImpl<TestReportMultiMapper, TestReportMultiPO> implements TestReportMultiService {

    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    private final TestReportMapper testReportMapper;
    private final TestReportPointMapper testReportPointMapper;
    private final TestReportGroupReportMapper testReportGroupReportMapper;
    private final ClientUnitMapper clientUnitMapper;
    private final TestDeviceMapper testDeviceMapper;
    private final IDictTypeService dictTypeService;
    private final IDictDataService dictDataService;
    private final TestReportService testReportService;
    private final TestReportStorageService testReportStorageService;
    private final TestReportMultiStorageService testReportMultiStorageService;
    private final TestReportMultiZipParser testReportMultiZipParser;
    private final TestReportLedgerExcelParser testReportLedgerExcelParser;

    @Override
    public Page<TestReportMultiVO> list(TestReportMultiParam.QueryParam param) {
        TestReportMultiParam.QueryParam query = param == null ? new TestReportMultiParam.QueryParam() : param;
        return this.baseMapper.selectPage(new Page<TestReportMultiVO>(PageFactory.getPageNum(query), PageFactory.getPageSize(query)), query);
    }

    @Override
    public TestReportMultiValidateResultVO validateZip(MultipartFile file) {
        TestReportMultiZipParser.ParsedArchive archive = testReportMultiZipParser.parse(file);
        TestReportMultiValidateResultVO result = toValidateResult(archive);
        if (!Boolean.TRUE.equals(result.getSuccess())) {
            return result;
        }

        String sessionId = UUID.randomUUID().toString();
        try {
            SessionMeta sessionMeta = new SessionMeta();
            sessionMeta.setSessionId(sessionId);
            sessionMeta.setZipFileName(archive.getZipFileName());
            sessionMeta.setSummaryFileName(archive.getSummaryFileName());
            sessionMeta.setSummaryRelativePath(archive.getSummaryRelativePath());
            sessionMeta.setDirCount(archive.getDirCount());
            sessionMeta.setChildReportCount(archive.getChildReportCount());
            sessionMeta.setPointCount(archive.getPointCount());
            sessionMeta.setExcelAttachmentCount(archive.getExcelAttachmentCount());
            sessionMeta.setWordAttachmentCount(archive.getWordAttachmentCount());
            sessionMeta.setTemplateDirs(archive.getTemplateDirs());
            sessionMeta.setPoints(archive.getPoints());

            sessionMeta.setZipStoragePath(testReportMultiStorageService.saveSessionZip(sessionId, file).getObjectName());
            for (Map.Entry<String, byte[]> entry : archive.getFileContentMap().entrySet()) {
                testReportMultiStorageService.saveSessionExtractedFile(sessionId, entry.getKey(), entry.getValue(), resolveContentType(entry.getKey()));
            }
            testReportMultiStorageService.writeSessionMeta(sessionId, OBJECT_MAPPER.writeValueAsString(sessionMeta));
            result.setSessionId(sessionId);
            return result;
        } catch (BusinessException exception) {
            testReportMultiStorageService.deleteSessionQuietly(sessionId);
            throw exception;
        } catch (Exception exception) {
            testReportMultiStorageService.deleteSessionQuietly(sessionId);
            throw new BusinessException(CommonResponseEnum.FAIL, "保存压缩包校验会话失败");
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean create(TestReportMultiParam.CreateParam param) {
        if (param == null) {
            throw new BusinessException(CommonResponseEnum.FAIL, "创建参数不能为空");
        }
        String taskNo = requireText(param.getNo(), "批任务编号不能为空", 64, "批任务编号不能超过64个字符");
        checkMultiNoUnique(taskNo, null);
        SessionMeta sessionMeta = requireSessionMeta(param.getSessionId());
        TestReportLedgerExcelParser.ParsedLedger ledger = parseSessionLedger(sessionMeta);
        List<ResolvedChildConfig> childConfigs = resolveChildConfigs(param.getChildReports(), sessionMeta);
        String userId = resolveCurrentUserId();
        BaseDataSummary baseDataSummary = maintainBaseData(ledger, userId);

        String multiId = UUID.randomUUID().toString();
        LocalDateTime now = LocalDateTime.now();
        List<String> createdStoragePaths = new ArrayList<String>();
        try {
            byte[] zipBytes = testReportMultiStorageService.readSessionZipBytes(sessionMeta.getZipStoragePath());
            byte[] summaryBytes = testReportMultiStorageService.readSessionExtractedBytes(sessionMeta.getSessionId(), sessionMeta.getSummaryRelativePath());
            String zipStoragePath = testReportMultiStorageService.saveTaskZipBytes(multiId, sessionMeta.getZipFileName(), zipBytes);
            String summaryStoragePath = testReportMultiStorageService.saveTaskSummaryBytes(multiId, sessionMeta.getSummaryFileName(), summaryBytes);
            createdStoragePaths.add(zipStoragePath);
            createdStoragePaths.add(summaryStoragePath);

            TestReportMultiPO multi = new TestReportMultiPO();
            multi.setId(multiId);
            multi.setNo(taskNo);
            multi.setZipFileName(sessionMeta.getZipFileName());
            multi.setZipStoragePath(zipStoragePath);
            multi.setSummaryFileName(sessionMeta.getSummaryFileName());
            multi.setSummaryStoragePath(summaryStoragePath);
            multi.setDirCount(sessionMeta.getDirCount());
            multi.setChildReportCount(sessionMeta.getChildReportCount());
            multi.setPointCount(sessionMeta.getPointCount());
            multi.setImportState(TestReportMultiConst.IMPORT_STATE_CREATED);
            multi.setGenerateState(TestReportMultiConst.GENERATE_STATE_PENDING);
            multi.setData(buildMultiSnapshotJson(sessionMeta, baseDataSummary, childConfigs));
            multi.setStatus(TestReportMultiConst.STATUS_NORMAL);
            multi.setCreateBy(userId);
            multi.setCreateTime(now);
            multi.setUpdateBy(userId);
            multi.setUpdateTime(now);
            this.baseMapper.insert(multi);

            int childSortNo = 1;
            for (ResolvedChildConfig childConfig : childConfigs) {
                String childId = UUID.randomUUID().toString();
                TestReportPO report = new TestReportPO();
                report.setId(childId);
                report.setMultiId(multiId);
                report.setNo(childConfig.getNo());
                report.setClientUnitId(childConfig.getClientUnitId());
                report.setReportModelId(childConfig.getReportModelId());
                report.setTemplateDirName(childConfig.getTemplateDirName());
                report.setTemplateDirPath(childConfig.getTemplateDirPath());
                report.setSortNo(childSortNo++);
                report.setCreateUnit(childConfig.getCreateUnit());
                report.setContractNumber(childConfig.getContractNumber());
                report.setStandard(writeJsonArray(childConfig.getStandard(), "检测依据不能为空", "检测依据格式不正确"));
                report.setData(buildChildSnapshotJson(multiId, summaryStoragePath, sessionMeta, baseDataSummary, childConfig));
                report.setPhonenumber(trimToNull(childConfig.getPhonenumber()));
                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);
                testReportMapper.insert(report);

                saveChildPoints(report, childConfig.getPoints(), sessionMeta.getSessionId(), userId, now, createdStoragePaths);
                initGroupReports(report.getId(), childConfig.getPoints(), userId, now);
            }
            testReportMultiStorageService.deleteSessionQuietly(sessionMeta.getSessionId());
            return true;
        } catch (RuntimeException exception) {
            for (String storagePath : createdStoragePaths) {
                deleteQuietly(storagePath);
            }
            throw exception;
        }
    }

    @Override
    public TestReportMultiVO detail(String id) {
        TestReportMultiVO detail = this.baseMapper.selectDetail(requireText(id, "批任务ID不能为空"));
        if (detail == null) {
            throw new BusinessException(CommonResponseEnum.FAIL, "多模板批任务不存在或已删除");
        }
        return detail;
    }

    @Override
    public List<TestReportVO> listChildReports(String id) {
        requireNormal(id);
        return testReportMapper.selectByMultiId(id);
    }

    @Override
    public ResponseEntity<byte[]> download(String id) {
        TestReportMultiPO task = requireNormal(id);
        String childReportPrefix = testReportMultiStorageService.buildTaskChildReportDir(task.getId());
        String summaryPrefix = testReportMultiStorageService.buildTaskSummaryDir(task.getId());
        List<String> childReportObjectNames = testReportMultiStorageService.listTaskChildReportObjectNames(task.getId());
        List<String> summaryObjectNames = testReportMultiStorageService.listTaskSummaryObjectNames(task.getId());
        if (childReportObjectNames.isEmpty() && summaryObjectNames.isEmpty()) {
            throw new BusinessException(CommonResponseEnum.FAIL, "当前批任务暂无可下载文件");
        }
        String fileName = ExportFileNameUtil.appendToday(resolveDownloadFileName(task));
        try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
             ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream, StandardCharsets.UTF_8)) {
            writeDirectoryEntry(zipOutputStream, TestReportMultiConst.STORAGE_MULTI_CHILD_REPORT_DIR + "/");
            writeDirectoryEntry(zipOutputStream, TestReportMultiConst.STORAGE_MULTI_SUMMARY_DIR + "/");
            writeObjectEntries(zipOutputStream, childReportObjectNames, childReportPrefix,
                    TestReportMultiConst.STORAGE_MULTI_CHILD_REPORT_DIR);
            writeObjectEntries(zipOutputStream, summaryObjectNames, summaryPrefix,
                    TestReportMultiConst.STORAGE_MULTI_SUMMARY_DIR);
            zipOutputStream.finish();
            return ResponseEntity.ok()
                    .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encodeFileName(fileName))
                    .contentType(MediaType.parseMediaType("application/zip"))
                    .body(outputStream.toByteArray());
        } catch (BusinessException exception) {
            throw exception;
        } catch (Exception exception) {
            throw new BusinessException(CommonResponseEnum.FAIL, "下载批任务压缩包失败");
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean delete(List<String> ids) {
        List<String> validIds = normalizeIds(ids);
        if (validIds.isEmpty()) {
            throw new BusinessException(CommonResponseEnum.FAIL, "批任务ID不能为空");
        }
        List<TestReportMultiPO> tasks = this.lambdaQuery()
                .in(TestReportMultiPO::getId, validIds)
                .eq(TestReportMultiPO::getStatus, TestReportMultiConst.STATUS_NORMAL)
                .list();
        if (tasks.isEmpty()) {
            return false;
        }
        Map<String, List<String>> childIdMap = new HashMap<String, List<String>>();
        for (TestReportMultiPO task : tasks) {
            List<String> childIds = loadNormalChildReports(task.getId()).stream().map(TestReportPO::getId).collect(Collectors.toList());
            childIdMap.put(task.getId(), childIds);
        }
        String userId = resolveCurrentUserId();
        LocalDateTime now = LocalDateTime.now();
        boolean updated = this.lambdaUpdate()
                .set(TestReportMultiPO::getStatus, TestReportMultiConst.STATUS_DELETED)
                .set(TestReportMultiPO::getUpdateBy, userId)
                .set(TestReportMultiPO::getUpdateTime, now)
                .in(TestReportMultiPO::getId, validIds)
                .eq(TestReportMultiPO::getStatus, TestReportMultiConst.STATUS_NORMAL)
                .update();
        if (!updated) {
            return false;
        }
        for (TestReportMultiPO task : tasks) {
            List<String> childIds = childIdMap.get(task.getId());
            if (childIds != null && !childIds.isEmpty()) {
                testReportService.deleteTestReports(childIds);
            }
            deleteQuietly(task.getZipStoragePath());
            deleteQuietly(task.getSummaryStoragePath());
        }
        return true;
    }

    @Override
    public boolean generate(String id) {
        TestReportMultiPO task = requireNormal(id);
        List<TestReportPO> childReports = loadNormalChildReports(id);
        if (childReports.isEmpty()) {
            throw new BusinessException(CommonResponseEnum.FAIL, "当前批任务下没有可生成的子报告");
        }
        task.setGenerateState(TestReportMultiConst.GENERATE_STATE_GENERATING);
        task.setUpdateBy(resolveCurrentUserId());
        task.setUpdateTime(LocalDateTime.now());
        this.updateById(task);

        int successCount = 0;
        List<String> failMessages = new ArrayList<String>();
        for (TestReportPO childReport : childReports) {
            try {
                testReportService.generateGroupedReports(childReport.getId());
                successCount++;
            } catch (RuntimeException exception) {
                failMessages.add(childReport.getNo() + "：" + exception.getMessage());
            }
        }
        if (failMessages.isEmpty()) {
            task.setGenerateState(TestReportMultiConst.GENERATE_STATE_ALL_SUCCESS);
            task.setUpdateTime(LocalDateTime.now());
            this.updateById(task);
            return true;
        }
        task.setGenerateState(successCount > 0
                ? TestReportMultiConst.GENERATE_STATE_PARTIAL_SUCCESS
                : TestReportMultiConst.GENERATE_STATE_FAILED);
        task.setUpdateTime(LocalDateTime.now());
        this.updateById(task);
        throw new BusinessException(CommonResponseEnum.FAIL, String.join("；", failMessages));
    }

    private String resolveDownloadFileName(TestReportMultiPO task) {
        String taskNo = task == null ? null : trimToNull(task.getNo());
        return (taskNo == null ? "批任务结果包" : taskNo) + ".zip";
    }

    private void writeObjectEntries(ZipOutputStream zipOutputStream, List<String> objectNames, String prefix, String zipRootDir)
            throws Exception {
        if (objectNames == null || objectNames.isEmpty()) {
            return;
        }
        String normalizedPrefix = prefix + "/";
        for (String objectName : objectNames) {
            String normalizedObjectName = trimToNull(objectName);
            if (normalizedObjectName == null || !normalizedObjectName.startsWith(normalizedPrefix)) {
                continue;
            }
            String relativePath = normalizedObjectName.substring(normalizedPrefix.length());
            String zipEntryName = zipRootDir + "/" + normalizeZipRelativePath(relativePath);
            zipEntryName = replaceStoredFileName(zipEntryName);
            ZipEntry zipEntry = new ZipEntry(zipEntryName);
            zipOutputStream.putNextEntry(zipEntry);
            zipOutputStream.write(testReportMultiStorageService.readStoredBytes(normalizedObjectName));
            zipOutputStream.closeEntry();
        }
    }

    private void writeDirectoryEntry(ZipOutputStream zipOutputStream, String entryName) throws Exception {
        ZipEntry zipEntry = new ZipEntry(entryName);
        zipOutputStream.putNextEntry(zipEntry);
        zipOutputStream.closeEntry();
    }

    private String normalizeZipRelativePath(String relativePath) {
        String normalized = relativePath == null ? "" : relativePath.replace("\\", "/");
        while (normalized.startsWith("/")) {
            normalized = normalized.substring(1);
        }
        return normalized;
    }

    private String replaceStoredFileName(String zipEntryName) {
        int slashIndex = zipEntryName.lastIndexOf('/');
        if (slashIndex < 0) {
            return restoreOriginalFileName(zipEntryName);
        }
        return zipEntryName.substring(0, slashIndex + 1) + restoreOriginalFileName(zipEntryName.substring(slashIndex + 1));
    }

    private String restoreOriginalFileName(String fileName) {
        String normalizedFileName = trimToNull(fileName);
        if (normalizedFileName == null || normalizedFileName.length() <= 33) {
            return fileName;
        }
        String prefix = normalizedFileName.substring(0, 33);
        if (!prefix.matches("[0-9a-fA-F]{32}-")) {
            return fileName;
        }
        return normalizedFileName.substring(33);
    }

    private String encodeFileName(String fileName) {
        try {
            return URLEncoder.encode(fileName, StandardCharsets.UTF_8.name()).replace("+", "%20");
        } catch (Exception exception) {
            throw new BusinessException(CommonResponseEnum.FAIL, "下载文件名编码失败");
        }
    }

    private void saveChildPoints(TestReportPO report, List<TestReportMultiZipParser.ParsedPointBinding> points, String sessionId,
                                 String userId, LocalDateTime now, List<String> createdStoragePaths) {
        int sortNo = 1;
        for (TestReportMultiZipParser.ParsedPointBinding pointBinding : points) {
            TestReportPointPO point = new TestReportPointPO();
            point.setId(UUID.randomUUID().toString());
            point.setReportId(report.getId());
            point.setGroupNo(pointBinding.getGroupNo());
            point.setSubstationName(pointBinding.getSubstationName());
            point.setPointName(pointBinding.getPointName());
            point.setExcelAttachmentName(pointBinding.getExcelAttachmentName());
            point.setExcelAttachmentRelativePath(pointBinding.getExcelAttachmentRelativePath());
            String excelStoragePath = testReportStorageService.savePointExcelBytes(report.getId(), report.getMultiId(),
                    pointBinding.getExcelAttachmentName(),
                    testReportMultiStorageService.readSessionExtractedBytes(sessionId, pointBinding.getExcelAttachmentRelativePath()));
            point.setExcelStoragePath(excelStoragePath);
            createdStoragePaths.add(excelStoragePath);
            point.setWordAttachmentName(pointBinding.getWordAttachmentName());
            point.setWordAttachmentRelativePath(pointBinding.getWordAttachmentRelativePath());
            if (StrUtil.isNotBlank(pointBinding.getWordAttachmentRelativePath())) {
                String wordStoragePath = testReportStorageService.savePointWordBytes(report.getId(), report.getMultiId(),
                        pointBinding.getWordAttachmentName(),
                        testReportMultiStorageService.readSessionExtractedBytes(sessionId, pointBinding.getWordAttachmentRelativePath()));
                point.setWordStoragePath(wordStoragePath);
                createdStoragePaths.add(wordStoragePath);
            }
            point.setSortNo(sortNo++);
            point.setDeleted(TestReportConst.DELETED_NO);
            point.setCreateBy(userId);
            point.setCreateTime(now);
            point.setUpdateBy(userId);
            point.setUpdateTime(now);
            testReportPointMapper.insert(point);
        }
    }

    private void initGroupReports(String reportId, List<TestReportMultiZipParser.ParsedPointBinding> points, String userId, LocalDateTime now) {
        List<Integer> groupNos = points.stream()
                .map(TestReportMultiZipParser.ParsedPointBinding::getGroupNo)
                .filter(item -> item != null && item > 0)
                .distinct()
                .sorted()
                .collect(Collectors.toList());
        int sortNo = 1;
        for (Integer groupNo : groupNos) {
            TestReportGroupReportPO groupReport = new TestReportGroupReportPO();
            groupReport.setId(UUID.randomUUID().toString());
            groupReport.setReportId(reportId);
            groupReport.setGroupNo(groupNo);
            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 BaseDataSummary maintainBaseData(TestReportLedgerExcelParser.ParsedLedger ledger, String userId) {
        if (ledger == null) {
            return new BaseDataSummary();
        }
        Map<String, TestDevicePO> dbDeviceMap = loadExistingDevices(collectDeviceNos(ledger));
        Map<String, ClientUnitPO> dbClientMap = loadExistingClients(collectClientNames(ledger));
        Map<String, String> typeNameIdMap = loadDeviceTypeNameIdMap();
        BaseDataSummary summary = new BaseDataSummary();
        LocalDateTime now = LocalDateTime.now();

        for (TestReportLedgerExcelParser.ParsedDeviceRow deviceRow : ledger.getDevices()) {
            if (deviceRow == null || StrUtil.isBlank(deviceRow.getNo())) {
                continue;
            }
            TestDevicePO existingDevice = dbDeviceMap.get(deviceRow.getNo());
            if (existingDevice != null) {
                summary.setDeviceReuseCount(summary.getDeviceReuseCount() + 1);
                continue;
            }
            String normalizedTypeName = trimToNull(deviceRow.getTypeName());
            String typeId = normalizedTypeName == null ? null : typeNameIdMap.get(normalizedTypeName);
            if (normalizedTypeName != null && typeId == null) {
                throw new BusinessException(CommonResponseEnum.FAIL,
                        "基础信息维护阶段：检测设备sheet第" + deviceRow.getRowNo() + "行设备型号不存在");
            }
            TestDevicePO device = new TestDevicePO();
            device.setId(UUID.randomUUID().toString());
            device.setType(typeId);
            device.setNo(deviceRow.getNo());
            device.setValidityPeriod(deviceRow.getValidityPeriod() == null ? LocalDate.now() : deviceRow.getValidityPeriod());
            device.setValidityReport(deviceRow.getValidityReportName());
            device.setState(resolveDeviceState(deviceRow.getStateName(), deviceRow.getRowNo()));
            device.setStatus(TestDeviceConst.STATUS_NORMAL);
            device.setCreateBy(userId);
            device.setCreateTime(now);
            device.setUpdateBy(userId);
            device.setUpdateTime(now);
            testDeviceMapper.insert(device);
            dbDeviceMap.put(device.getNo(), device);
            summary.setDeviceAddedCount(summary.getDeviceAddedCount() + 1);
        }

        for (TestReportLedgerExcelParser.ParsedClientRow clientRow : ledger.getClients()) {
            if (clientRow == null || StrUtil.isBlank(clientRow.getName())) {
                continue;
            }
            ClientUnitPO existingClient = dbClientMap.get(clientRow.getName());
            if (existingClient != null) {
                summary.setClientReuseCount(summary.getClientReuseCount() + 1);
                continue;
            }
            ClientUnitPO client = new ClientUnitPO();
            client.setId(UUID.randomUUID().toString());
            client.setName(clientRow.getName());
            client.setContactName(clientRow.getContactName() == null ? "" : clientRow.getContactName());
            client.setPhonenumber(clientRow.getPhonenumber() == null ? "" : clientRow.getPhonenumber());
            client.setAddress(clientRow.getAddress());
            client.setStatus(ClientUnitConst.STATUS_NORMAL);
            client.setCreateBy(userId);
            client.setCreateTime(now);
            client.setUpdateBy(userId);
            client.setUpdateTime(now);
            clientUnitMapper.insert(client);
            dbClientMap.put(client.getName(), client);
            summary.setClientAddedCount(summary.getClientAddedCount() + 1);
        }
        return summary;
    }

    private String buildMultiSnapshotJson(SessionMeta sessionMeta, BaseDataSummary baseDataSummary, List<ResolvedChildConfig> childConfigs) {
        ObjectNode root = OBJECT_MAPPER.createObjectNode();
        root.put("version", "3.0");
        root.put("importMode", "multi-template-zip");
        root.put("zipFileName", sessionMeta.getZipFileName());
        root.put("summaryFileName", sessionMeta.getSummaryFileName());
        root.put("dirCount", sessionMeta.getDirCount());
        root.put("childReportCount", sessionMeta.getChildReportCount());
        root.put("pointCount", sessionMeta.getPointCount());

        ArrayNode dirArray = root.putArray("templateDirs");
        for (TestReportMultiTemplateDirVO templateDir : sessionMeta.getTemplateDirs()) {
            ObjectNode dirNode = dirArray.addObject();
            dirNode.put("dirName", templateDir.getDirName());
            dirNode.put("dirPath", templateDir.getDirPath());
            dirNode.put("pointCount", templateDir.getPointCount() == null ? 0 : templateDir.getPointCount());
            dirNode.put("groupCount", templateDir.getGroupCount() == null ? 0 : templateDir.getGroupCount());
        }

        ArrayNode childArray = root.putArray("childReports");
        for (ResolvedChildConfig childConfig : childConfigs) {
            ObjectNode childNode = childArray.addObject();
            childNode.put("templateDirName", childConfig.getTemplateDirName());
            childNode.put("templateDirPath", childConfig.getTemplateDirPath());
            childNode.put("reportNo", childConfig.getNo());
            childNode.put("pointCount", childConfig.getPoints().size());
        }

        ObjectNode baseDataNode = root.putObject("baseDataSummary");
        baseDataNode.put("deviceAddedCount", baseDataSummary.getDeviceAddedCount());
        baseDataNode.put("deviceReuseCount", baseDataSummary.getDeviceReuseCount());
        baseDataNode.put("clientAddedCount", baseDataSummary.getClientAddedCount());
        baseDataNode.put("clientReuseCount", baseDataSummary.getClientReuseCount());
        return root.toString();
    }

    private String buildChildSnapshotJson(String multiId, String summaryStoragePath, SessionMeta sessionMeta,
                                          BaseDataSummary baseDataSummary, ResolvedChildConfig childConfig) {
        ObjectNode root = OBJECT_MAPPER.createObjectNode();
        root.put("version", "3.0");
        root.put("importMode", "multi-template-zip");
        root.put("multiId", multiId);
        root.put("summaryFileName", sessionMeta.getSummaryFileName());
        root.put("summaryStoragePath", summaryStoragePath);
        root.put("templateDirName", childConfig.getTemplateDirName());
        root.put("templateDirPath", childConfig.getTemplateDirPath());
        root.put("pointCount", childConfig.getPoints().size());
        root.put("groupCount", childConfig.getGroupNos().size());

        ArrayNode groupArray = root.putArray("groupSummary");
        for (Integer groupNo : childConfig.getGroupNos()) {
            ObjectNode groupNode = groupArray.addObject();
            groupNode.put("groupNo", groupNo);
            groupNode.put("count", childConfig.getPoints().stream()
                    .filter(point -> groupNo.equals(point.getGroupNo()))
                    .count());
        }

        ObjectNode baseDataNode = root.putObject("baseDataSummary");
        baseDataNode.put("deviceAddedCount", baseDataSummary.getDeviceAddedCount());
        baseDataNode.put("deviceReuseCount", baseDataSummary.getDeviceReuseCount());
        baseDataNode.put("clientAddedCount", baseDataSummary.getClientAddedCount());
        baseDataNode.put("clientReuseCount", baseDataSummary.getClientReuseCount());
        return root.toString();
    }

    private List<ResolvedChildConfig> resolveChildConfigs(List<TestReportMultiParam.ChildReportItem> childReports, SessionMeta sessionMeta) {
        if (childReports == null || childReports.isEmpty()) {
            throw new BusinessException(CommonResponseEnum.FAIL, "子报告配置不能为空");
        }
        Map<String, TestReportMultiParam.ChildReportItem> configMap = new HashMap<String, TestReportMultiParam.ChildReportItem>();
        Set<String> childNos = new HashSet<String>();
        for (TestReportMultiParam.ChildReportItem childReport : childReports) {
            if (childReport == null) {
                throw new BusinessException(CommonResponseEnum.FAIL, "子报告配置不能为空");
            }
            String dirPath = requireText(childReport.getTemplateDirPath(), "模板目录路径不能为空", 255, "模板目录路径不能超过255个字符");
            if (configMap.put(dirPath, childReport) != null) {
                throw new BusinessException(CommonResponseEnum.FAIL, "模板目录配置重复：" + dirPath);
            }
            String childNo = requireText(childReport.getNo(), "普测报告编号不能为空", 32, "普测报告编号不能超过32个字符");
            if (!childNos.add(childNo)) {
                throw new BusinessException(CommonResponseEnum.FAIL, "子报告编号不能重复");
            }
            checkChildReportParam(childReport);
            checkReportNoUnique(childNo);
        }
        List<ResolvedChildConfig> result = new ArrayList<ResolvedChildConfig>();
        for (TestReportMultiTemplateDirVO templateDir : sessionMeta.getTemplateDirs()) {
            TestReportMultiParam.ChildReportItem childReport = configMap.get(templateDir.getDirPath());
            if (childReport == null) {
                throw new BusinessException(CommonResponseEnum.FAIL, "缺少模板目录配置：" + templateDir.getDirPath());
            }
            ResolvedChildConfig childConfig = new ResolvedChildConfig();
            childConfig.setTemplateDirName(templateDir.getDirName());
            childConfig.setTemplateDirPath(templateDir.getDirPath());
            childConfig.setNo(requireText(childReport.getNo(), "普测报告编号不能为空", 32, "普测报告编号不能超过32个字符"));
            childConfig.setClientUnitId(requireText(childReport.getClientUnitId(), "委托单位不能为空"));
            childConfig.setReportModelId(requireText(childReport.getReportModelId(), "报告模板不能为空"));
            childConfig.setCreateUnit(requireText(childReport.getCreateUnit(), "检测公司不能为空"));
            childConfig.setContractNumber(requireText(childReport.getContractNumber(), "合同编号不能为空", 32, "合同编号不能超过32个字符"));
            childConfig.setStandard(normalizeStandardIds(childReport.getStandard()));
            childConfig.setPhonenumber(trimToNull(childReport.getPhonenumber()));
            childConfig.setPoints(sessionMeta.getPoints().stream()
                    .filter(point -> templateDir.getDirPath().equals(point.getTemplateDirPath()))
                    .sorted(Comparator.comparing(TestReportMultiZipParser.ParsedPointBinding::getRowNo))
                    .collect(Collectors.toList()));
            childConfig.setGroupNos(childConfig.getPoints().stream()
                    .map(TestReportMultiZipParser.ParsedPointBinding::getGroupNo)
                    .filter(item -> item != null && item > 0)
                    .distinct()
                    .sorted()
                    .collect(Collectors.toList()));
            result.add(childConfig);
        }
        if (configMap.size() != sessionMeta.getTemplateDirs().size()) {
            throw new BusinessException(CommonResponseEnum.FAIL, "子报告配置与模板目录数量不一致");
        }
        return result;
    }

    private void checkChildReportParam(TestReportMultiParam.ChildReportItem childReport) {
        if (testReportMapper.countNormalClientUnit(requireText(childReport.getClientUnitId(), "委托单位不能为空")) <= 0) {
            throw new BusinessException(CommonResponseEnum.FAIL, "委托单位不存在或已删除");
        }
        if (testReportMapper.countNormalReportModel(requireText(childReport.getReportModelId(), "报告模板不能为空")) <= 0) {
            throw new BusinessException(CommonResponseEnum.FAIL, "报告模板不存在或已删除");
        }
        Map<String, String> companyNameMap = buildDictNameMap(DictConst.TEST_REPORT_COMPANY_DICT_CODE);
        if (!companyNameMap.containsKey(requireText(childReport.getCreateUnit(), "检测公司不能为空"))) {
            throw new BusinessException(CommonResponseEnum.FAIL, "检测公司不存在或已删除");
        }
        List<String> standardIds = normalizeStandardIds(childReport.getStandard());
        Map<String, String> standardNameMap = buildDictNameMap(DictConst.TEST_REPORT_STANDARD_DICT_CODE);
        for (String standardId : standardIds) {
            if (!standardNameMap.containsKey(standardId)) {
                throw new BusinessException(CommonResponseEnum.FAIL, "测试依据不存在或已删除");
            }
        }
    }

    private SessionMeta requireSessionMeta(String sessionId) {
        String normalizedSessionId = requireText(sessionId, "会话ID不能为空");
        String sessionMetaText = testReportMultiStorageService.readSessionMeta(normalizedSessionId);
        if (StrUtil.isBlank(sessionMetaText)) {
            throw new BusinessException(CommonResponseEnum.FAIL, "压缩包校验会话不存在或已失效");
        }
        try {
            JsonNode root = OBJECT_MAPPER.readTree(sessionMetaText);
            SessionMeta sessionMeta = new SessionMeta();
            sessionMeta.setSessionId(trimToNull(root.path("sessionId").asText(null)));
            sessionMeta.setZipFileName(trimToNull(root.path("zipFileName").asText(null)));
            sessionMeta.setZipStoragePath(trimToNull(root.path("zipStoragePath").asText(null)));
            sessionMeta.setSummaryFileName(trimToNull(root.path("summaryFileName").asText(null)));
            sessionMeta.setSummaryRelativePath(trimToNull(root.path("summaryRelativePath").asText(null)));
            sessionMeta.setDirCount(readNullableInteger(root, "dirCount"));
            sessionMeta.setChildReportCount(readNullableInteger(root, "childReportCount"));
            sessionMeta.setPointCount(readNullableInteger(root, "pointCount"));
            sessionMeta.setExcelAttachmentCount(readNullableInteger(root, "excelAttachmentCount"));
            sessionMeta.setWordAttachmentCount(readNullableInteger(root, "wordAttachmentCount"));
            sessionMeta.setTemplateDirs(readJsonList(root.path("templateDirs"),
                    new TypeReference<List<TestReportMultiTemplateDirVO>>() {
                    }));
            sessionMeta.setPoints(readJsonList(root.path("points"),
                    new TypeReference<List<TestReportMultiZipParser.ParsedPointBinding>>() {
                    }));
            if (sessionMeta == null || StrUtil.isBlank(sessionMeta.getSessionId())) {
                throw new BusinessException(CommonResponseEnum.FAIL, "压缩包校验会话不存在或已失效");
            }
            return sessionMeta;
        } catch (BusinessException exception) {
            throw exception;
        } catch (Exception exception) {
            throw new BusinessException(CommonResponseEnum.FAIL, "压缩包校验会话数据损坏");
        }
    }

    private TestReportLedgerExcelParser.ParsedLedger parseSessionLedger(SessionMeta sessionMeta) {
        if (sessionMeta == null || StrUtil.isBlank(sessionMeta.getSessionId())
                || StrUtil.isBlank(sessionMeta.getSummaryRelativePath())
                || StrUtil.isBlank(sessionMeta.getSummaryFileName())) {
            throw new BusinessException(CommonResponseEnum.FAIL, "压缩包校验会话数据损坏");
        }
        try {
            byte[] summaryBytes = testReportMultiStorageService.readSessionExtractedBytes(
                    sessionMeta.getSessionId(), sessionMeta.getSummaryRelativePath());
            return testReportLedgerExcelParser.parseForValidate(
                    sessionMeta.getSummaryFileName(), new ByteArrayInputStream(summaryBytes));
        } catch (BusinessException exception) {
            throw exception;
        } catch (Exception exception) {
            throw new BusinessException(CommonResponseEnum.FAIL, "读取压缩包校验会话台账失败");
        }
    }

    private Integer readNullableInteger(JsonNode root, String fieldName) {
        if (root == null || StrUtil.isBlank(fieldName)) {
            return null;
        }
        JsonNode fieldNode = root.path(fieldName);
        if (fieldNode.isMissingNode() || fieldNode.isNull()) {
            return null;
        }
        return fieldNode.asInt();
    }

    private <T> List<T> readJsonList(JsonNode jsonNode, TypeReference<List<T>> typeReference) {
        if (jsonNode == null || jsonNode.isMissingNode() || jsonNode.isNull()) {
            return new ArrayList<T>();
        }
        try {
            List<T> result = OBJECT_MAPPER.convertValue(jsonNode, typeReference);
            return result == null ? new ArrayList<T>() : result;
        } catch (IllegalArgumentException exception) {
            throw new BusinessException(CommonResponseEnum.FAIL, "压缩包校验会话数据损坏");
        }
    }

    private TestReportMultiPO requireNormal(String id) {
        String normalizedId = requireText(id, "批任务ID不能为空");
        TestReportMultiPO task = this.lambdaQuery()
                .eq(TestReportMultiPO::getId, normalizedId)
                .eq(TestReportMultiPO::getStatus, TestReportMultiConst.STATUS_NORMAL)
                .one();
        if (task == null) {
            throw new BusinessException(CommonResponseEnum.FAIL, "多模板批任务不存在或已删除");
        }
        return task;
    }

    private TestReportMultiValidateResultVO toValidateResult(TestReportMultiZipParser.ParsedArchive archive) {
        TestReportMultiValidateResultVO result = new TestReportMultiValidateResultVO();
        result.setSuccess(archive.getSuccess());
        result.setZipFileName(archive.getZipFileName());
        result.setSummaryFileName(archive.getSummaryFileName());
        result.setDirCount(archive.getDirCount());
        result.setChildReportCount(archive.getChildReportCount());
        result.setPointCount(archive.getPointCount());
        result.setExcelAttachmentCount(archive.getExcelAttachmentCount());
        result.setWordAttachmentCount(archive.getWordAttachmentCount());
        result.setTemplateDirs(archive.getTemplateDirs());
        result.setOrphanAttachmentPaths(archive.getOrphanAttachmentPaths());
        result.setFailReasons(archive.getFailReasons());
        return result;
    }

    private void checkMultiNoUnique(String no, String excludeId) {
        LambdaQueryWrapper<TestReportMultiPO> wrapper = new LambdaQueryWrapper<TestReportMultiPO>();
        wrapper.eq(TestReportMultiPO::getNo, no)
                .eq(TestReportMultiPO::getStatus, TestReportMultiConst.STATUS_NORMAL)
                .ne(StrUtil.isNotBlank(excludeId), TestReportMultiPO::getId, excludeId);
        if (this.count(wrapper) > 0) {
            throw new BusinessException(CommonResponseEnum.FAIL, "批任务编号已存在");
        }
    }

    private void checkReportNoUnique(String no) {
        LambdaQueryWrapper<TestReportPO> wrapper = new LambdaQueryWrapper<TestReportPO>();
        wrapper.eq(TestReportPO::getNo, no)
                .eq(TestReportPO::getStatus, TestReportConst.STATUS_NORMAL);
        if (testReportMapper.selectCount(wrapper) > 0) {
            throw new BusinessException(CommonResponseEnum.FAIL, "普测报告编号已存在");
        }
    }

    private List<TestReportPO> loadNormalChildReports(String multiId) {
        LambdaQueryWrapper<TestReportPO> wrapper = new LambdaQueryWrapper<TestReportPO>();
        wrapper.eq(TestReportPO::getMultiId, multiId)
                .eq(TestReportPO::getStatus, TestReportConst.STATUS_NORMAL)
                .orderByAsc(TestReportPO::getSortNo)
                .orderByAsc(TestReportPO::getCreateTime);
        return testReportMapper.selectList(wrapper);
    }

    private Collection<String> collectDeviceNos(TestReportLedgerExcelParser.ParsedLedger ledger) {
        Set<String> result = new HashSet<String>();
        for (TestReportLedgerExcelParser.ParsedPointRow point : ledger.getPoints()) {
            if (StrUtil.isNotBlank(point.getTestDeviceNo())) {
                result.add(point.getTestDeviceNo());
            }
        }
        for (TestReportLedgerExcelParser.ParsedDeviceRow device : ledger.getDevices()) {
            if (device != null && StrUtil.isNotBlank(device.getNo())) {
                result.add(device.getNo());
            }
        }
        return result;
    }

    private Collection<String> collectClientNames(TestReportLedgerExcelParser.ParsedLedger ledger) {
        Set<String> result = new HashSet<String>();
        for (TestReportLedgerExcelParser.ParsedPointRow point : ledger.getPoints()) {
            if (StrUtil.isNotBlank(point.getClientUnitName())) {
                result.add(point.getClientUnitName());
            }
        }
        for (TestReportLedgerExcelParser.ParsedClientRow client : ledger.getClients()) {
            if (client != null && StrUtil.isNotBlank(client.getName())) {
                result.add(client.getName());
            }
        }
        return result;
    }

    private Map<String, TestDevicePO> loadExistingDevices(Collection<String> deviceNos) {
        if (deviceNos == null || deviceNos.isEmpty()) {
            return new HashMap<String, TestDevicePO>();
        }
        LambdaQueryWrapper<TestDevicePO> wrapper = new LambdaQueryWrapper<TestDevicePO>();
        wrapper.in(TestDevicePO::getNo, deviceNos).eq(TestDevicePO::getStatus, TestDeviceConst.STATUS_NORMAL);
        return testDeviceMapper.selectList(wrapper).stream()
                .collect(Collectors.toMap(TestDevicePO::getNo, item -> item, (first, second) -> first, HashMap::new));
    }

    private Map<String, ClientUnitPO> loadExistingClients(Collection<String> clientNames) {
        if (clientNames == null || clientNames.isEmpty()) {
            return new HashMap<String, ClientUnitPO>();
        }
        LambdaQueryWrapper<ClientUnitPO> wrapper = new LambdaQueryWrapper<ClientUnitPO>();
        wrapper.in(ClientUnitPO::getName, clientNames).eq(ClientUnitPO::getStatus, ClientUnitConst.STATUS_NORMAL);
        return clientUnitMapper.selectList(wrapper).stream()
                .collect(Collectors.toMap(ClientUnitPO::getName, item -> item, (first, second) -> first, HashMap::new));
    }

    private Map<String, String> loadDeviceTypeNameIdMap() {
        List<TestDeviceTypeNameVO> rows = testDeviceMapper.selectNormalDeviceTypes();
        if (rows == null || rows.isEmpty()) {
            return Collections.emptyMap();
        }
        Map<String, String> result = new HashMap<String, String>();
        for (TestDeviceTypeNameVO row : rows) {
            if (row != null && StrUtil.isNotBlank(row.getName()) && StrUtil.isNotBlank(row.getId())) {
                result.put(row.getName(), row.getId());
            }
        }
        return result;
    }

    private String resolveDeviceState(String stateName, Integer rowNo) {
        String normalizedState = trimToNull(stateName);
        if (normalizedState == null) {
            return null;
        }
        if ("01".equals(normalizedState) || "在运".equals(normalizedState)) {
            return TestDeviceConst.STATE_RUNNING;
        }
        if ("02".equals(normalizedState) || "退运".equals(normalizedState)) {
            return TestDeviceConst.STATE_RETIRED;
        }
        throw new BusinessException(CommonResponseEnum.FAIL, "基础信息维护阶段：检测设备sheet第" + rowNo + "行设备状态不正确");
    }

    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()) || !Integer.valueOf(1).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.isNotBlank(dictData.getId()) && StrUtil.isNotBlank(dictData.getName())
                    && Integer.valueOf(1).equals(dictData.getState())) {
                result.put(dictData.getId(), dictData.getName());
            }
        }
        return result;
    }

    private String writeJsonArray(List<String> values, String blankMessage, String invalidMessage) {
        List<String> normalizedValues = normalizeStandardIds(values);
        if (normalizedValues.isEmpty()) {
            throw new BusinessException(CommonResponseEnum.FAIL, blankMessage);
        }
        try {
            return OBJECT_MAPPER.writeValueAsString(normalizedValues);
        } catch (Exception exception) {
            throw new BusinessException(CommonResponseEnum.FAIL, invalidMessage);
        }
    }

    private List<String> normalizeStandardIds(List<String> values) {
        if (values == null || values.isEmpty()) {
            throw new BusinessException(CommonResponseEnum.FAIL, "检测依据不能为空");
        }
        List<String> result = new ArrayList<String>();
        for (String value : values) {
            String normalizedValue = trimToNull(value);
            if (normalizedValue != null && !result.contains(normalizedValue)) {
                result.add(normalizedValue);
            }
        }
        if (result.isEmpty()) {
            throw new BusinessException(CommonResponseEnum.FAIL, "检测依据不能为空");
        }
        return result;
    }

    private String resolveContentType(String fileName) {
        String lowerFileName = fileName == null ? "" : fileName.toLowerCase(Locale.ENGLISH);
        if (lowerFileName.endsWith(".xls")) {
            return "application/vnd.ms-excel";
        }
        if (lowerFileName.endsWith(".xlsx")) {
            return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
        }
        if (lowerFileName.endsWith(".doc")) {
            return "application/msword";
        }
        if (lowerFileName.endsWith(".docx")) {
            return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
        }
        return MediaType.APPLICATION_OCTET_STREAM_VALUE;
    }

    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 void deleteQuietly(String storagePath) {
        testReportMultiStorageService.deleteQuietly(storagePath);
    }

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

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

    private String trimToNull(String value) {
        if (value == null) {
            return null;
        }
        String text = value.trim();
        return text.isEmpty() ? null : text;
    }

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

    @Data
    private static class SessionMeta {
        private String sessionId;
        private String zipFileName;
        private String zipStoragePath;
        private String summaryFileName;
        private String summaryRelativePath;
        private Integer dirCount;
        private Integer childReportCount;
        private Integer pointCount;
        private Integer excelAttachmentCount;
        private Integer wordAttachmentCount;
        private List<TestReportMultiTemplateDirVO> templateDirs = new ArrayList<TestReportMultiTemplateDirVO>();
        private List<TestReportMultiZipParser.ParsedPointBinding> points = new ArrayList<TestReportMultiZipParser.ParsedPointBinding>();
    }

    @Data
    private static class ResolvedChildConfig {
        private String templateDirName;
        private String templateDirPath;
        private String no;
        private String clientUnitId;
        private String reportModelId;
        private String createUnit;
        private String contractNumber;
        private List<String> standard = new ArrayList<String>();
        private String phonenumber;
        private List<TestReportMultiZipParser.ParsedPointBinding> points = new ArrayList<TestReportMultiZipParser.ParsedPointBinding>();
        private List<Integer> groupNos = new ArrayList<Integer>();
    }

    @Data
    private static class BaseDataSummary {
        private int deviceAddedCount;
        private int deviceReuseCount;
        private int clientAddedCount;
        private int clientReuseCount;
    }
}
