package com.njcn.rdms.module.project.service.workreport.export;

import com.fasterxml.jackson.core.type.TypeReference;
import com.njcn.rdms.framework.common.biz.system.dict.dto.DictDataRespDTO;
import com.njcn.rdms.framework.common.pojo.CommonResult;
import com.njcn.rdms.framework.common.pojo.PageParam;
import com.njcn.rdms.framework.common.util.json.JsonUtils;
import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.PersonalReportPlanItemRespVO;
import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.PersonalReportReviewItemRespVO;
import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.WorkReportMemberSnapshotRespVO;
import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportApprovalRecordRespVO;
import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportContentExportReqVO;
import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportRespVO;
import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportContentExportReqVO;
import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportItemRespVO;
import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportRespVO;
import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportContentExportReqVO;
import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportRespVO;
import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportTravelSegmentRespVO;
import com.njcn.rdms.module.project.enums.ProjectDictTypeConstants;
import com.njcn.rdms.module.project.service.workreport.common.WorkReportCommonService;
import com.njcn.rdms.module.system.api.dict.DictDataApi;
import jakarta.annotation.Resource;
import org.docx4j.XmlUtils;
import org.docx4j.jaxb.Context;
import org.docx4j.model.table.TblFactory;
import org.docx4j.openpackaging.exceptions.Docx4JException;
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
import org.docx4j.wml.*;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.IsoFields;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;

@Service
public class WorkReportContentExportService {

    private static final String DOCX_CONTENT_TYPE =
            "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
    private static final String ZIP_CONTENT_TYPE = "application/zip";
    private static final String WEEKLY_TEMPLATE_PATH = "templates/work-report/weekly-report-template.docx";
    private static final String MONTHLY_TEMPLATE_PATH = "templates/work-report/monthly-report-template.docx";
    private static final String PROJECT_TEMPLATE_PATH = "templates/work-report/project-report-template.docx";
    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy.MM.dd");
    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm");
    private static final ObjectFactory WML_FACTORY = Context.getWmlObjectFactory();
    private static final BigInteger DEFAULT_FONT_SIZE = BigInteger.valueOf(18);
    private static final BigInteger TITLE_FONT_SIZE = BigInteger.valueOf(36);
    private static final BigInteger SECTION_TITLE_FONT_SIZE = BigInteger.valueOf(24);

    @Resource
    private WorkReportCommonService workReportCommonService;
    @Resource
    private DictDataApi dictDataApi;

    public WorkReportExportFile exportWeekly(WeeklyReportContentExportReqVO reqVO) throws IOException {
        List<WeeklyReportRespVO> reports = resolveWeeklyReports(reqVO);
        return buildFile("个人周报", reports.stream().map(this::buildWeeklyDocument).toList());
    }

    public WorkReportExportFile exportMonthly(MonthlyReportContentExportReqVO reqVO) throws IOException {
        List<MonthlyReportRespVO> reports = resolveMonthlyReports(reqVO);
        List<DocumentContent> documents = new ArrayList<>(reports.size());
        for (MonthlyReportRespVO report : reports) {
            MonthlyReportApprovalRecordRespVO approvalRecord = latestMonthlyApprovalRecord(report.getId());
            documents.add(buildMonthlyDocument(report, approvalRecord));
        }
        return buildFile("个人月报", documents);
    }

    public WorkReportExportFile exportProject(ProjectReportContentExportReqVO reqVO) throws IOException {
        List<ProjectReportRespVO> reports = resolveProjectReports(reqVO);
        return buildFile("项目半月报", reports.stream().map(this::buildProjectDocument).toList());
    }

    private List<WeeklyReportRespVO> resolveWeeklyReports(WeeklyReportContentExportReqVO reqVO) {
        if (Boolean.TRUE.equals(reqVO.getExportAll())) {
            reqVO.setPageNo(1);
            reqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
            return workReportCommonService.getWeeklyReportExportPage(reqVO).getList().stream()
                    .map(item -> workReportCommonService.getWeeklyReport(item.getId()))
                    .toList();
        }
        return distinctIds(reqVO.getIds()).stream()
                .map(workReportCommonService::getWeeklyReport)
                .toList();
    }

    private List<MonthlyReportRespVO> resolveMonthlyReports(MonthlyReportContentExportReqVO reqVO) {
        if (Boolean.TRUE.equals(reqVO.getExportAll())) {
            reqVO.setPageNo(1);
            reqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
            return workReportCommonService.getMonthlyReportExportPage(reqVO).getList().stream()
                    .map(item -> workReportCommonService.getMonthlyReport(item.getId()))
                    .toList();
        }
        return distinctIds(reqVO.getIds()).stream()
                .map(workReportCommonService::getMonthlyReport)
                .toList();
    }

    private List<ProjectReportRespVO> resolveProjectReports(ProjectReportContentExportReqVO reqVO) {
        if (Boolean.TRUE.equals(reqVO.getExportAll())) {
            reqVO.setPageNo(1);
            reqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
            return workReportCommonService.getProjectReportExportPage(reqVO).getList().stream()
                    .map(item -> workReportCommonService.getProjectReport(item.getId()))
                    .toList();
        }
        return distinctIds(reqVO.getIds()).stream()
                .map(workReportCommonService::getProjectReport)
                .toList();
    }

    private List<Long> distinctIds(List<Long> ids) {
        if (ids == null || ids.isEmpty()) {
            throw invalidParamException("请选择要导出的报告");
        }
        Set<Long> result = new LinkedHashSet<>();
        for (Long id : ids) {
            if (id != null) {
                result.add(id);
            }
        }
        if (result.isEmpty()) {
            throw invalidParamException("请选择要导出的报告");
        }
        return new ArrayList<>(result);
    }

    private MonthlyReportApprovalRecordRespVO latestMonthlyApprovalRecord(Long reportId) {
        List<MonthlyReportApprovalRecordRespVO> records = workReportCommonService.getMonthlyApprovalRecords(reportId);
        if (records == null || records.isEmpty()) {
            return null;
        }
        return records.stream()
                .max(Comparator.comparing(MonthlyReportApprovalRecordRespVO::getApprovalRound,
                        Comparator.nullsFirst(Integer::compareTo)))
                .orElse(null);
    }

    private WorkReportExportFile buildFile(String title, List<DocumentContent> documents) throws IOException {
        if (documents.isEmpty()) {
            throw invalidParamException("没有可导出的报告");
        }
        if (documents.size() == 1) {
            DocumentContent document = documents.get(0);
            return new WorkReportExportFile(document.filename(), DOCX_CONTENT_TYPE, document.content());
        }
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        try (ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)) {
            for (int i = 0; i < documents.size(); i++) {
                DocumentContent document = documents.get(i);
                zipOutputStream.putNextEntry(new ZipEntry((i + 1) + "_" + document.filename()));
                zipOutputStream.write(document.content());
                zipOutputStream.closeEntry();
            }
        }
        return new WorkReportExportFile(title + "_" + LocalDate.now() + ".zip", ZIP_CONTENT_TYPE,
                outputStream.toByteArray());
    }

    private DocumentContent buildWeeklyDocument(WeeklyReportRespVO report) {
        ClassPathResource templateResource = new ClassPathResource(WEEKLY_TEMPLATE_PATH);
        if (templateResource.exists()) {
            return buildWeeklyTemplateDocument(report, templateResource);
        }
        return buildWeeklyFallbackDocument(report);
    }

    private DocumentContent buildWeeklyTemplateDocument(WeeklyReportRespVO report, ClassPathResource templateResource) {
        try (InputStream inputStream = templateResource.getInputStream()) {
            WordprocessingMLPackage document = WordprocessingMLPackage.load(inputStream);
            replacePlaceholders(document, buildWeeklyTemplateData(report));
            renderPersonalReviewTable(document, report.getReviewItems());
            renderPersonalPlanTable(document, report.getPlanItems());
            return new DocumentContent(buildWeeklyFilename("个人周报", report.getReporterName(),
                    report.getPeriodStartDate(), report.getPeriodLabel()),
                    toBytes(document));
        } catch (IOException | Docx4JException e) {
            throw new IllegalStateException("生成周报模板导出文件失败", e);
        }
    }

    private DocumentContent buildWeeklyFallbackDocument(WeeklyReportRespVO report) {
        try {
            WordprocessingMLPackage document = WordprocessingMLPackage.createPackage();
            addTitle(document, "个人周报");
            addParagraph(document, "填报周期：" + formatPeriod(report), false);
            addPersonalBaseTable(document, report.getReporterName(), report.getReporterDeptName(),
                    report.getReporterPostName(), report.getSupervisorName());

            addSectionTitle(document, "第一部分：当期重点工作回顾");
            addPersonalReviewTable(document, report.getReviewItems(), false);

            if (Boolean.TRUE.equals(report.getIsBusinessTrip())) {
                addSectionTitle(document, "出差信息");
                addKeyValueTable(document, List.of(
                        row("出差天数", formatDecimal(report.getTotalTravelDays())),
                        row("出差明细", buildTravelText(report.getTravelSegments()))));
            }

            addSectionTitle(document, "下周期重点工作计划");
            addPersonalPlanTable(document, report.getPlanItems(), false);
            return new DocumentContent(buildWeeklyFilename("个人周报", report.getReporterName(),
                    report.getPeriodStartDate(), report.getPeriodLabel()),
                    toBytes(document));
        } catch (Docx4JException | IOException e) {
            throw new IllegalStateException("生成周报导出文件失败", e);
        }
    }

    private Map<String, String> buildWeeklyTemplateData(WeeklyReportRespVO report) {
        Map<String, String> data = new HashMap<>();
        data.put("reporterName", text(report.getReporterName()));
        data.put("reporterDeptName", text(report.getReporterDeptName()));
        data.put("reporterPostName", text(report.getReporterPostName()));
        data.put("periodText", formatPeriod(report));
        data.put("periodLabel", text(report.getPeriodLabel()));
        data.put("supervisorName", text(report.getSupervisorName()));
        data.put("businessTripText", Boolean.TRUE.equals(report.getIsBusinessTrip()) ? "是" : "否");
        data.put("totalTravelDaysText", formatDecimal(report.getTotalTravelDays()));
        data.put("totalWorkHoursText", formatDecimal(report.getTotalWorkHours()));
        data.put("submitTimeText", formatDateTime(report.getSubmitTime()));
        data.put("approvalTimeText", formatDateTime(report.getApprovalTime()));
        return data;
    }

    private void replacePlaceholders(WordprocessingMLPackage document, Map<String, String> data) {
        replacePlaceholdersInObject(document.getMainDocumentPart(), data);
    }

    private void replacePlaceholdersInObject(Object root, Map<String, String> data) {
        for (P paragraph : findAllParagraphs(root)) {
            replaceParagraphPlaceholders(paragraph, data);
        }
    }

    private void replaceParagraphPlaceholders(P paragraph, Map<String, String> data) {
        List<Text> textNodes = findAllTextNodes(paragraph);
        if (textNodes.isEmpty()) {
            return;
        }
        String source = textNodes.stream()
                .map(Text::getValue)
                .filter(Objects::nonNull)
                .collect(java.util.stream.Collectors.joining());
        if (!StringUtils.hasText(source) || !source.contains("{{")) {
            return;
        }
        String replaced = source;
        for (Map.Entry<String, String> entry : data.entrySet()) {
            replaced = replaced.replace("{{" + entry.getKey() + "}}", entry.getValue());
        }
        rewriteParagraphText(paragraph, replaced, true);
        applyParagraphFormatting(paragraph, replaced, false);
    }

    private void renderPersonalReviewTable(WordprocessingMLPackage document, List<PersonalReportReviewItemRespVO> items) {
        TemplateRowLocation location = findTemplateRowByPlaceholders(document, List.of(
                "{{itemNumber}}", "{{itemTitleWithHours}}", "{{contentText}}", "{{reflectionText}}"));
        if (location == null) {
            return;
        }
        List<Map<String, String>> rows = safeList(items).stream()
                .map(item -> placeholderValues(Map.of(
                        "itemNumber", formatNumber(item.getItemNumber()),
                        "itemTitleWithHours", appendHours(item.getItemTitle(), item.getWorkHours()),
                        "contentText", buildStructuredReviewText(item, false),
                        "reflectionText", text(item.getReflectionText()))))
                .toList();
        List<Tr> renderedRows = renderRowsByTemplateRow(location, rows, List.of(
                "itemNumber", "itemTitleWithHours", "contentText", "reflectionText"));
        rewriteReviewRows(renderedRows, items, false);
    }

    private void renderPersonalPlanTable(WordprocessingMLPackage document, List<PersonalReportPlanItemRespVO> items) {
        TemplateRowLocation location = findTemplateRowByPlaceholders(document, List.of(
                "{{itemNumber}}", "{{itemTitle}}", "{{targetText}}", "{{supportNeed}}"));
        if (location == null) {
            return;
        }
        List<Map<String, String>> rows = safeList(items).stream()
                .map(item -> placeholderValues(Map.of(
                        "itemNumber", formatNumber(item.getItemNumber()),
                        "itemTitle", text(item.getItemTitle()),
                        "targetText", buildStructuredPlanText(item, false),
                        "supportNeed", text(item.getSupportNeed()))))
                .toList();
        List<Tr> renderedRows = renderRowsByTemplateRow(location, rows, List.of(
                "itemNumber", "itemTitle", "targetText", "supportNeed"));
        rewritePlanRows(renderedRows, items, false);
    }

    private TemplateRowLocation findTemplateRowByPlaceholders(WordprocessingMLPackage document, List<String> placeholders) {
        for (Tbl table : findAllTables(document.getMainDocumentPart())) {
            List<Object> rows = table.getContent();
            for (int i = 0; i < rows.size(); i++) {
                Object rowObject = XmlUtils.unwrap(rows.get(i));
                if (!(rowObject instanceof Tr row)) {
                    continue;
                }
                String rowXml = XmlUtils.marshaltoString(row, true, true, Context.jc, "", "", Tr.class);
                if (placeholders.stream().allMatch(rowXml::contains)) {
                    return new TemplateRowLocation(table, i);
                }
            }
        }
        return null;
    }

    private List<Tr> renderRowsByTemplateRow(TemplateRowLocation location,
                                             List<Map<String, String>> dataRows, List<String> placeholderNames) {
        Tbl table = location.table();
        int templateRowIndex = location.rowIndex();
        int dataEndIndex = findPlaceholderBlockEndRowIndex(table, templateRowIndex, placeholderNames);
        Tr templateRow = tableRow(table, templateRowIndex);
        for (int i = dataEndIndex - 1; i >= templateRowIndex; i--) {
            table.getContent().remove(i);
        }

        List<Map<String, String>> rows = dataRows == null || dataRows.isEmpty()
                ? List.of(emptyPlaceholderValues(placeholderNames))
                : dataRows;
        int insertIndex = templateRowIndex;
        List<Tr> insertedRows = new ArrayList<>(rows.size());
        for (Map<String, String> rowData : rows) {
            Tr row = (Tr) XmlUtils.deepCopy(templateRow);
            replacePlaceholdersInObject(row, rowData);
            table.getContent().add(insertIndex++, row);
            insertedRows.add(row);
        }
        return insertedRows;
    }

    private Map<String, String> placeholderValues(Map<String, String> values) {
        Map<String, String> result = new HashMap<>();
        for (Map.Entry<String, String> entry : values.entrySet()) {
            result.put("{{" + entry.getKey() + "}}", text(entry.getValue()));
        }
        return result;
    }

    private Map<String, String> emptyPlaceholderValues(List<String> placeholderNames) {
        Map<String, String> result = new HashMap<>();
        for (String placeholderName : placeholderNames) {
            result.put("{{" + placeholderName + "}}", "");
        }
        return result;
    }

    private int findPlaceholderBlockEndRowIndex(Tbl table, int templateRowIndex, List<String> placeholderNames) {
        List<String> placeholderTokens = placeholderNames.stream()
                .map(name -> "{{" + name + "}}")
                .toList();
        for (int i = templateRowIndex; i < table.getContent().size(); i++) {
            Object rowObject = XmlUtils.unwrap(table.getContent().get(i));
            if (!(rowObject instanceof Tr row)) {
                return i;
            }
            String rowXml = marshalTr(row);
            boolean containsPlaceholder = placeholderTokens.stream().anyMatch(rowXml::contains);
            if (!containsPlaceholder) {
                return i;
            }
        }
        return table.getContent().size();
    }

    private String marshalTr(Tr row) {
        return XmlUtils.marshaltoString(row, true, true, Context.jc, "", "", Tr.class);
    }

    private Tr tableRow(Tbl table, int rowIndex) {
        return (Tr) XmlUtils.unwrap(table.getContent().get(rowIndex));
    }

    private List<Text> findAllTextNodes(Object root) {
        List<Text> result = new ArrayList<>();
        new org.docx4j.TraversalUtil(root, new org.docx4j.TraversalUtil.CallbackImpl() {
            @Override
            public List<Object> apply(Object o) {
                Object value = XmlUtils.unwrap(o);
                if (value instanceof Text textNode) {
                    result.add(textNode);
                }
                return null;
            }
        });
        return result;
    }

    private List<P> findAllParagraphs(Object root) {
        List<P> result = new ArrayList<>();
        new org.docx4j.TraversalUtil(root, new org.docx4j.TraversalUtil.CallbackImpl() {
            @Override
            public List<Object> apply(Object o) {
                Object value = XmlUtils.unwrap(o);
                if (value instanceof P paragraph) {
                    result.add(paragraph);
                }
                return null;
            }
        });
        return result;
    }

    private List<Tbl> findAllTables(Object root) {
        List<Tbl> result = new ArrayList<>();
        new org.docx4j.TraversalUtil(root, new org.docx4j.TraversalUtil.CallbackImpl() {
            @Override
            public List<Object> apply(Object o) {
                Object value = XmlUtils.unwrap(o);
                if (value instanceof Tbl table) {
                    result.add(table);
                }
                return null;
            }
        });
        return result;
    }

    private record TemplateRowLocation(Tbl table, int rowIndex) {
    }

    private DocumentContent buildMonthlyDocument(MonthlyReportRespVO report,
                                                 MonthlyReportApprovalRecordRespVO approvalRecord) {
        ClassPathResource templateResource = new ClassPathResource(MONTHLY_TEMPLATE_PATH);
        if (templateResource.exists()) {
            return buildMonthlyTemplateDocument(report, approvalRecord, templateResource);
        }
        return buildMonthlyFallbackDocument(report, approvalRecord);
    }

    private DocumentContent buildMonthlyTemplateDocument(MonthlyReportRespVO report,
                                                         MonthlyReportApprovalRecordRespVO approvalRecord,
                                                         ClassPathResource templateResource) {
        try (InputStream inputStream = templateResource.getInputStream()) {
            WordprocessingMLPackage document = WordprocessingMLPackage.load(inputStream);
            replacePlaceholders(document, buildMonthlyTemplateData(report, approvalRecord));
            renderMonthlyReviewTable(document, report.getReviewItems());
            renderMonthlyFeedbackTables(document, approvalRecord);
            renderMonthlyPlanTable(document, report.getPlanItems());
            return new DocumentContent(buildFilename("个人月报", report.getReporterName(), report.getPeriodLabel()),
                    toBytes(document));
        } catch (IOException | Docx4JException e) {
            throw new IllegalStateException("生成月报模板导出文件失败", e);
        }
    }

    private DocumentContent buildMonthlyFallbackDocument(MonthlyReportRespVO report,
                                                         MonthlyReportApprovalRecordRespVO approvalRecord) {
        try {
            WordprocessingMLPackage document = WordprocessingMLPackage.createPackage();
            addTitle(document, "灿能电力绩效反馈面谈表");
            addPersonalBaseTable(document, report.getReporterName(), report.getReporterDeptName(),
                    report.getReporterPostName(), report.getSupervisorName());
            addKeyValueTable(document, List.of(
                    row("考核周期", formatPeriod(report)),
                    row("面谈时间", formatDate(approvalRecord == null ? null : approvalRecord.getMeetingDate()))));

            addSectionTitle(document, "第一部分：当期重点工作回顾（个人填写）");
            addPersonalReviewTable(document, report.getReviewItems(), false);

            addSectionTitle(document, "第二部分：当期工作反馈（直接上级/分管领导填写）");
            addFeedbackTable(document, approvalRecord);

            addSectionTitle(document, "第三部分：当期绩效考核结果");
            addKeyValueTable(document, List.of(row("绩效考核结果",
                    approvalRecord == null ? null : approvalRecord.getPerformanceResult())));

            addSectionTitle(document, "第四部分：下周期重点工作计划");
            addPersonalPlanTable(document, report.getPlanItems(), false);

            addParagraph(document, "被考核人签名：" + text(approvalRecord == null ? null : approvalRecord.getEmployeeSignName())
                    + "    日期：" + formatDate(approvalRecord == null ? null : approvalRecord.getEmployeeSignedDate()), false);
            addParagraph(document, "上级签名：" + text(approvalRecord == null ? null : approvalRecord.getSupervisorSignName())
                    + "    日期：" + formatDate(approvalRecord == null ? null : approvalRecord.getSupervisorSignedDate()), false);
            return new DocumentContent(buildFilename("个人月报", report.getReporterName(), report.getPeriodLabel()),
                    toBytes(document));
        } catch (Docx4JException | IOException e) {
            throw new IllegalStateException("生成月报导出文件失败", e);
        }
    }

    private DocumentContent buildProjectDocument(ProjectReportRespVO report) {
        ClassPathResource templateResource = new ClassPathResource(PROJECT_TEMPLATE_PATH);
        if (templateResource.exists()) {
            return buildProjectTemplateDocument(report, templateResource);
        }
        return buildProjectFallbackDocument(report);
    }

    private DocumentContent buildProjectTemplateDocument(ProjectReportRespVO report,
                                                         ClassPathResource templateResource) {
        try (InputStream inputStream = templateResource.getInputStream()) {
            WordprocessingMLPackage document = WordprocessingMLPackage.load(inputStream);
            Map<String, String> templateData = buildProjectTemplateData(report);
            List<String> currentItemLines = splitTextLinesPreserveBlank(templateData.remove("currentItems"));
            List<String> nextItemLines = splitTextLinesPreserveBlank(templateData.remove("nextItems"));
            replacePlaceholders(document, templateData);
            renderTemplateCellParagraphsByPlaceholder(document, "{{currentItems}}", currentItemLines);
            renderTemplateCellParagraphsByPlaceholder(document, "{{nextItems}}", nextItemLines);
            return new DocumentContent(buildFilename("项目半月报", report.getProjectName(), report.getPeriodLabel()),
                    toBytes(document));
        } catch (IOException | Docx4JException e) {
            throw new IllegalStateException("生成项目半月报模板导出文件失败", e);
        }
    }

    private DocumentContent buildProjectFallbackDocument(ProjectReportRespVO report) {
        try {
            WordprocessingMLPackage document = WordprocessingMLPackage.createPackage();
            addTitle(document, "研发工作情况简报");
            addParagraph(document, "研发工作情况简报（" + formatPeriod(report) + "）：", true);
            addSectionTitle(document, "一、" + text(report.getProjectName()));
            addKeyValueTable(document, List.of(
                    row("项目名称", report.getProjectName()),
                    row("项目负责人", report.getProjectOwnerName()),
                    row("技术负责人", report.getTechnicalOwnerName()),
                    row("项目组成员", buildMemberText(report.getProjectMemberSnapshot())),
                    row("项目状况", report.getProjectStatusDesc()),
                    row("整体计划进度", report.getProjectProgressPlan())));

            addSectionTitle(document, "本期工作内容");
            addProjectItemTable(document, report.getCurrentItems(), true);

            addSectionTitle(document, "下期计划工作内容");
            addProjectItemTable(document, report.getNextItems(), false);

            addKeyValueTable(document, List.of(
                    row("要点描述", report.getProjectKeyPoints()),
                    row("项目问题", report.getProjectProblems())));
            return new DocumentContent(buildFilename("项目半月报", report.getProjectName(), report.getPeriodLabel()),
                    toBytes(document));
        } catch (Docx4JException | IOException e) {
            throw new IllegalStateException("生成项目半月报导出文件失败", e);
        }
    }
    private Map<String, String> buildMonthlyTemplateData(MonthlyReportRespVO report,
                                                         MonthlyReportApprovalRecordRespVO approvalRecord) {
        Map<String, String> data = new HashMap<>();
        data.put("reporterName", text(report.getReporterName()));
        data.put("reporterDeptName", text(report.getReporterDeptName()));
        data.put("reporterPostName", text(report.getReporterPostName()));
        data.put("periodText", formatPeriod(report));
        data.put("supervisorName", text(report.getSupervisorName()));
        data.put("meetingDateText", formatDate(approvalRecord == null ? null : approvalRecord.getMeetingDate()));
        data.put("strengthDesc", text(approvalRecord == null ? null : approvalRecord.getStrengthDesc()));
        data.put("strengthExample", text(approvalRecord == null ? null : approvalRecord.getStrengthExample()));
        data.put("weaknessDesc", text(approvalRecord == null ? null : approvalRecord.getWeaknessDesc()));
        data.put("weaknessExample", text(approvalRecord == null ? null : approvalRecord.getWeaknessExample()));
        data.put("improvementSuggestion", text(approvalRecord == null ? null : approvalRecord.getImprovementSuggestion()));
        data.put("performanceResult", text(approvalRecord == null ? null : approvalRecord.getPerformanceResult()));
        data.put("employeeSignName", text(approvalRecord == null ? null : approvalRecord.getEmployeeSignName()));
        data.put("employeeSignedDateText", formatDate(approvalRecord == null ? null : approvalRecord.getEmployeeSignedDate()));
        data.put("supervisorSignName", text(approvalRecord == null ? null : approvalRecord.getSupervisorSignName()));
        data.put("supervisorSignedDateText", formatDate(approvalRecord == null ? null : approvalRecord.getSupervisorSignedDate()));
        return data;
    }

    private Map<String, String> buildProjectTemplateData(ProjectReportRespVO report) {
        Map<String, String> data = new HashMap<>();
        data.put("briefDateText", report.getPeriodEndDate() == null ? "" :
                report.getPeriodEndDate().format(DateTimeFormatter.ofPattern("yyyyMMdd")));
        data.put("periodText", formatPeriod(report));
        data.put("projectName", text(report.getProjectName()));
        data.put("projectOwnerName", text(report.getProjectOwnerName()));
        data.put("technicalOwnerName", text(report.getTechnicalOwnerName()));
        data.put("projectMembersText", buildMemberText(report.getProjectMemberSnapshot()));
        data.put("projectStatusDesc", text(report.getProjectStatusDesc()));
        data.put("projectProgressPlan", text(report.getProjectProgressPlan()));
        data.put("currentItems", buildProjectItemsText(report.getCurrentItems(), true));
        data.put("nextItems", buildProjectItemsText(report.getNextItems(), false));
        data.put("projectKeyPoints", text(report.getProjectKeyPoints()));
        data.put("projectProblems", text(report.getProjectProblems()));
        return data;
    }

    private void renderTemplateCellParagraphsByPlaceholder(WordprocessingMLPackage document,
                                                           String placeholder, List<String> lines) {
        if (!StringUtils.hasText(placeholder)) {
            return;
        }
        for (Tbl table : findAllTables(document.getMainDocumentPart())) {
            for (Object rowObject : table.getContent()) {
                Object rowValue = XmlUtils.unwrap(rowObject);
                if (!(rowValue instanceof Tr row)) {
                    continue;
                }
                for (Tc cell : rowCells(row)) {
                    P templateParagraph = findParagraphByPlaceholder(cell, placeholder);
                    if (templateParagraph == null) {
                        continue;
                    }
                    setTemplateCellParagraphs(cell, templateParagraph, lines, false);
                    return;
                }
            }
        }
    }

    private void renderMonthlyReviewTable(WordprocessingMLPackage document, List<PersonalReportReviewItemRespVO> items) {
        TemplateRowLocation location = findTemplateRowByPlaceholders(document, List.of(
                "{{itemNumber}}", "{{itemTitleWithHours}}", "{{contentText}}", "{{reflectionText}}"));
        if (location == null) {
            return;
        }
        List<Map<String, String>> rows = safeList(items).stream()
                .map(item -> placeholderValues(Map.of(
                        "itemNumber", formatNumber(item.getItemNumber()),
                        "itemTitleWithHours", appendHours(item.getItemTitle(), item.getWorkHours()),
                        "contentText", buildStructuredReviewText(item, false),
                        "reflectionText", text(item.getReflectionText()))))
                .toList();
        List<Tr> renderedRows = renderRowsByTemplateRow(location, rows, List.of(
                "itemNumber", "itemTitleWithHours", "contentText", "reflectionText"));
        rewriteReviewRows(renderedRows, items, false);
    }

    private void renderMonthlyPlanTable(WordprocessingMLPackage document, List<PersonalReportPlanItemRespVO> items) {
        TemplateRowLocation location = findTemplateRowByPlaceholders(document, List.of(
                "{{itemNumber}}", "{{itemTitle}}", "{{targetText}}", "{{supportNeed}}"));
        if (location == null) {
            return;
        }
        List<Map<String, String>> rows = safeList(items).stream()
                .map(item -> placeholderValues(Map.of(
                        "itemNumber", formatNumber(item.getItemNumber()),
                        "itemTitle", text(item.getItemTitle()),
                        "targetText", buildStructuredPlanText(item, false),
                        "supportNeed", text(item.getSupportNeed()))))
                .toList();
        List<Tr> renderedRows = renderRowsByTemplateRow(location, rows, List.of(
                "itemNumber", "itemTitle", "targetText", "supportNeed"));
        rewritePlanRows(renderedRows, items, false);
    }

    private void renderMonthlyFeedbackTables(WordprocessingMLPackage document, MonthlyReportApprovalRecordRespVO approvalRecord) {
        TemplateRowLocation strengthLocation = findTemplateRowByPlaceholders(document, List.of(
                "优势", "{{strengthDesc}}", "{{strengthExample}}", "{{improvementSuggestion}}"));
        if (strengthLocation != null) {
            renderSingleTemplateRow(strengthLocation, placeholderValues(Map.of(
                    "strengthDesc", text(approvalRecord == null ? null : approvalRecord.getStrengthDesc()),
                    "strengthExample", text(approvalRecord == null ? null : approvalRecord.getStrengthExample()),
                    "improvementSuggestion", text(approvalRecord == null ? null : approvalRecord.getImprovementSuggestion())
            )));
        }
        TemplateRowLocation weaknessLocation = findTemplateRowByPlaceholders(document, List.of(
                "劣势", "{{weaknessDesc}}", "{{weaknessExample}}"));
        if (weaknessLocation != null) {
            renderSingleTemplateRow(weaknessLocation, placeholderValues(Map.of(
                    "weaknessDesc", text(approvalRecord == null ? null : approvalRecord.getWeaknessDesc()),
                    "weaknessExample", text(approvalRecord == null ? null : approvalRecord.getWeaknessExample())
            )));
        }
    }

    private void renderSingleTemplateRow(TemplateRowLocation location, Map<String, String> values) {
        Tr renderedRow = (Tr) XmlUtils.deepCopy(tableRow(location.table(), location.rowIndex()));
        replacePlaceholdersInObject(renderedRow, values);
        location.table().getContent().set(location.rowIndex(), renderedRow);
    }

    private void rewriteReviewRows(List<Tr> rows, List<PersonalReportReviewItemRespVO> items,
                                   boolean includeDetail) {
        List<PersonalReportReviewItemRespVO> safeItems = safeList(items);
        for (int i = 0; i < rows.size() && i < safeItems.size(); i++) {
            PersonalReportReviewItemRespVO item = safeItems.get(i);
            List<Tc> cells = rowCells(rows.get(i));
            setCenteredCellText(cells.get(0), formatNumber(item.getItemNumber()), false);
            setCenteredCellText(cells.get(1), appendHours(item.getItemTitle(), item.getWorkHours()), false);
            setStructuredReviewCell(cells.get(2), item, includeDetail);
            setCellText(cells.get(3), text(item.getReflectionText()), false);
        }
    }

    private void rewritePlanRows(List<Tr> rows, List<PersonalReportPlanItemRespVO> items,
                                 boolean includeDetail) {
        List<PersonalReportPlanItemRespVO> safeItems = safeList(items);
        for (int i = 0; i < rows.size() && i < safeItems.size(); i++) {
            PersonalReportPlanItemRespVO item = safeItems.get(i);
            List<Tc> cells = rowCells(rows.get(i));
            setCenteredCellText(cells.get(0), formatNumber(item.getItemNumber()), false);
            setCenteredCellText(cells.get(1), text(item.getItemTitle()), false);
            setStructuredPlanCell(cells.get(2), item, includeDetail);
            setCellText(cells.get(3), text(item.getSupportNeed()), false);
        }
    }

    private String buildStructuredReviewText(PersonalReportReviewItemRespVO item, boolean includeDetail) {
        return buildStructuredText(item == null ? null : item.getContentJson(),
                item == null ? null : item.getContentText(), true, includeDetail);
    }

    private String buildStructuredPlanText(PersonalReportPlanItemRespVO item, boolean includeDetail) {
        return buildStructuredText(item == null ? null : item.getTargetJson(),
                item == null ? null : item.getTargetText(), false, includeDetail);
    }

    private void setStructuredReviewCell(Tc cell, PersonalReportReviewItemRespVO item,
                                         boolean includeDetail) {
        setStructuredCellTextForTemplate(cell,
                item == null ? null : item.getContentJson(),
                item == null ? null : item.getContentText(),
                true,
                includeDetail);
    }

    private void setStructuredPlanCell(Tc cell, PersonalReportPlanItemRespVO item,
                                       boolean includeDetail) {
        setStructuredCellTextForTemplate(cell,
                item == null ? null : item.getTargetJson(),
                item == null ? null : item.getTargetText(),
                false,
                includeDetail);
    }

    private void setStructuredCellTextForTemplate(Tc cell, Object jsonValue,
                                                  String fallbackText, boolean includeHours,
                                                  boolean includeDetail) {
        List<String> lines = buildStructuredParagraphsForTemplate(jsonValue, fallbackText, includeHours, includeDetail);
        unsetNoWrap(cell);
        if (lines.isEmpty()) {
            setStructuredTemplateCellParagraphs(cell, splitTextLinesPreserveBlank(normalizeLineBreaks(fallbackText)), false);
            return;
        }
        setStructuredTemplateCellParagraphs(cell, lines, false);
    }
    private List<String> buildStructuredParagraphsForTemplate(Object jsonValue, String fallbackText,
                                                              boolean includeHours, boolean includeDetail) {
        List<StructuredSectionData> sections = parseStructuredSections(jsonValue);
        if (sections.isEmpty()) {
            sections = parseStructuredSectionsForTemplate(fallbackText);
        }
        if (sections.isEmpty()) {
            return List.of();
        }
        Map<String, String> priorityLabelMap = loadPriorityLabelMap();
        List<String> lines = new ArrayList<>();
        for (StructuredSectionData section : sections) {
            String category = normalizeSectionCategory(section.category());
            if (StringUtils.hasText(category)) {
                lines.add("#" + category + "：");
            }
            List<StructuredTaskData> tasks = safeList(section.tasks());
            for (int i = 0; i < tasks.size(); i++) {
                StructuredTaskData task = tasks.get(i);
                String taskLine = (i + 1) + "、" + formatStructuredTaskForTemplate(task, includeHours, priorityLabelMap);
                String detail = includeDetail ? text(task.detail()) : "";
                if (StringUtils.hasText(detail)) {
                    List<String> detailLines = splitTextLines(detail);
                    if (detailLines.isEmpty()) {
                        lines.add(taskLine + "：");
                    } else {
                        lines.add(taskLine + "：" + detailLines.get(0));
                        for (int j = 1; j < detailLines.size(); j++) {
                            lines.add(detailLines.get(j));
                        }
                    }
                } else {
                    lines.add(taskLine);
                }
            }
        }
        while (!lines.isEmpty() && !StringUtils.hasText(lines.get(lines.size() - 1))) {
            lines.remove(lines.size() - 1);
        }
        return lines;
    }

    private String formatStructuredTaskForTemplate(StructuredTaskData task, boolean includeHours,
                                                   Map<String, String> priorityLabelMap) {
        List<String> metrics = new ArrayList<>();
        if (StringUtils.hasText(task.priority())) {
            metrics.add(resolvePriorityLabel(task.priority(), priorityLabelMap));
        }
        if (task.progress() != null) {
            metrics.add("进度 " + formatDecimal(task.progress()) + "%");
        }
        if (includeHours && task.hours() != null) {
            metrics.add(formatDecimal(task.hours()) + "h");
        }
        return text(task.title()) + (metrics.isEmpty() ? "" : "（" + String.join(" / ", metrics) + "）");
    }

    private List<StructuredSectionData> parseStructuredSectionsForTemplate(String textValue) {
        if (!StringUtils.hasText(textValue)) {
            return List.of();
        }
        String[] rawLines = normalizeLineBreaks(text(textValue)).split("\\R");
        Map<String, List<StructuredTaskData>> sectionMap = new LinkedHashMap<>();
        String currentCategory = null;
        for (String rawLine : rawLines) {
            String line = text(rawLine);
            if (!StringUtils.hasText(line)) {
                continue;
            }
            if (line.startsWith("#")) {
                currentCategory = normalizeSectionCategory(line.substring(1));
                if (StringUtils.hasText(currentCategory)) {
                    sectionMap.putIfAbsent(currentCategory, new ArrayList<>());
                }
                continue;
            }
            if (appendDetailContinuation(sectionMap, currentCategory, line)) {
                continue;
            }
            java.util.regex.Matcher legacyMatcher = java.util.regex.Pattern
                    .compile("^(.+?)\\s*[-－]\\s*(.+)$")
                    .matcher(line);
            if (legacyMatcher.matches()) {
                String category = normalizeSectionCategory(legacyMatcher.group(1));
                StructuredTaskData task = parseTemplateStructuredTask(legacyMatcher.group(2));
                if (StringUtils.hasText(category) && task != null) {
                    sectionMap.computeIfAbsent(category, key -> new ArrayList<>()).add(task);
                    continue;
                }
            }
            StructuredTaskData task = parseTemplateStructuredTask(line);
            if (task == null) {
                continue;
            }
            String category = StringUtils.hasText(currentCategory) ? currentCategory : "工作内容";
            sectionMap.computeIfAbsent(category, key -> new ArrayList<>()).add(task);
        }
        return sectionMap.entrySet().stream()
                .map(entry -> new StructuredSectionData(entry.getKey(), entry.getValue()))
                .toList();
    }

    private StructuredTaskData parseTemplateStructuredTask(String line) {
        String normalized = text(line)
                .replaceFirst("^\\d+[.、．]\\s*", "")
                .replaceFirst("[。.!！？]+$", "");
        if (!StringUtils.hasText(normalized)) {
            return null;
        }
        TaskDetailParts parts = splitTaskDetail(normalized);
        java.util.regex.Matcher matcher = java.util.regex.Pattern
                .compile("^(.*?)(?:[（(]([^()（）]*)[)）])?$")
                .matcher(parts.title());
        if (!matcher.matches()) {
            return new StructuredTaskData(parts.title(), parts.detail(), null, null, null);
        }
        String title = text(matcher.group(1));
        if (!StringUtils.hasText(title)) {
            return null;
        }
        StructuredMetricsData metrics = parseTemplateMetrics(text(matcher.group(2)));
        return new StructuredTaskData(title, parts.detail(), metrics.priority(), metrics.progress(), metrics.hours());
    }

    private StructuredMetricsData parseTemplateMetrics(String metricsText) {
        if (!StringUtils.hasText(metricsText)) {
            return new StructuredMetricsData(null, null, null);
        }
        String[] parts = metricsText.split("[/／]");
        String priority = null;
        BigDecimal progress = null;
        BigDecimal hours = null;
        for (String part : parts) {
            String trimmed = text(part);
            if (!StringUtils.hasText(trimmed)) {
                continue;
            }
            java.util.regex.Matcher priorityMatcher = java.util.regex.Pattern
                    .compile("(?i)^P?(\\d+)(?:\\.0+)?$")
                    .matcher(trimmed);
            if (priority == null && priorityMatcher.matches()) {
                priority = normalizePriority(priorityMatcher.group(1));
                continue;
            }
            java.util.regex.Matcher progressMatcher = java.util.regex.Pattern
                    .compile("^(?:进度\\s*)?(\\d+(?:\\.\\d+)?)%$")
                    .matcher(trimmed);
            if (progress == null && progressMatcher.matches()) {
                progress = parseDecimal(progressMatcher.group(1));
                continue;
            }
            java.util.regex.Matcher hoursMatcher = java.util.regex.Pattern
                    .compile("^(\\d+(?:\\.\\d+)?)h$")
                    .matcher(trimmed);
            if (hours == null && hoursMatcher.matches()) {
                hours = parseDecimal(hoursMatcher.group(1));
            }
        }
        return new StructuredMetricsData(priority, progress, hours);
    }

    private String buildStructuredText(Object jsonValue, String fallbackText, boolean includeHours,
                                       boolean includeDetail) {
        List<StructuredSectionData> sections = parseStructuredSections(jsonValue);
        if (sections.isEmpty()) {
            sections = parseStructuredSectionsFromText(fallbackText);
        }
        if (sections.isEmpty()) {
            return text(fallbackText);
        }
        Map<String, String> priorityLabelMap = loadPriorityLabelMap();
        List<String> lines = new ArrayList<>();
        for (StructuredSectionData section : sections) {
            lines.add("#" + normalizeSectionCategory(section.category()) + "：");
            List<StructuredTaskData> tasks = safeList(section.tasks());
            for (int i = 0; i < tasks.size(); i++) {
                StructuredTaskData task = tasks.get(i);
                String taskLine = (i + 1) + "、" + formatStructuredTask(task, includeHours, priorityLabelMap);
                String detail = includeDetail ? text(task.detail()) : "";
                if (StringUtils.hasText(detail)) {
                    List<String> detailLines = splitTextLines(detail);
                    if (detailLines.isEmpty()) {
                        lines.add(taskLine + "：");
                    } else {
                        lines.add(taskLine + "：" + detailLines.get(0));
                        for (int j = 1; j < detailLines.size(); j++) {
                            lines.add(detailLines.get(j));
                        }
                    }
                } else {
                    lines.add(taskLine);
                }
            }
        }
        return String.join("\n", lines);
    }

    private void setStructuredCellText(Tc cell, Object jsonValue, String fallbackText, boolean includeHours) {
        List<String> lines = buildStructuredLines(jsonValue, fallbackText, includeHours);
        if (lines.isEmpty()) {
            setCellText(cell, fallbackText, false);
            return;
        }
        setCellParagraphs(cell, lines, false);
    }

    private List<String> buildStructuredLines(Object jsonValue, String fallbackText, boolean includeHours) {
        List<StructuredSectionData> sections = parseStructuredSections(jsonValue);
        if (sections.isEmpty()) {
            sections = parseStructuredSectionsFromText(fallbackText);
        }
        if (sections.isEmpty()) {
            return List.of();
        }
        Map<String, String> priorityLabelMap = loadPriorityLabelMap();
        List<String> lines = new ArrayList<>();
        for (StructuredSectionData section : sections) {
            lines.add("#" + normalizeSectionCategory(section.category()) + "：");
            List<StructuredTaskData> tasks = safeList(section.tasks());
            for (int i = 0; i < tasks.size(); i++) {
                lines.add((i + 1) + "、" + formatStructuredTask(tasks.get(i), includeHours, priorityLabelMap));
            }
        }
        return lines;
    }

    private List<StructuredSectionData> parseStructuredSections(Object jsonValue) {
        if (jsonValue == null) {
            return List.of();
        }
        Object normalized = jsonValue;
        if (jsonValue instanceof String textValue && StringUtils.hasText(textValue) && JsonUtils.isJson(textValue)) {
            normalized = JsonUtils.parseTree(textValue);
        }
        if (normalized instanceof Map<?, ?> mapValue) {
            Object sectionsValue = mapValue.get("sections");
            if (sectionsValue instanceof List<?> sectionList) {
                return convertStructuredSections(sectionList);
            }
        }
        if (normalized instanceof List<?> sectionList) {
            return convertStructuredSections(sectionList);
        }
        try {
            Map<String, Object> mapValue = JsonUtils.convertObject(normalized, new TypeReference<Map<String, Object>>() {});
            Object sectionsValue = mapValue == null ? null : mapValue.get("sections");
            if (sectionsValue instanceof List<?> sectionList) {
                return convertStructuredSections(sectionList);
            }
        } catch (IllegalArgumentException ignored) {
            return List.of();
        }
        return List.of();
    }

    private List<StructuredSectionData> convertStructuredSections(List<?> rawSections) {
        List<StructuredSectionData> result = new ArrayList<>();
        for (Object rawSection : rawSections) {
            Map<String, Object> sectionMap;
            try {
                sectionMap = JsonUtils.convertObject(rawSection, new TypeReference<Map<String, Object>>() {});
            } catch (IllegalArgumentException ignored) {
                continue;
            }
            if (sectionMap == null) {
                continue;
            }
            String category = textOf(firstNonNull(sectionMap.get("category"), sectionMap.get("title"), sectionMap.get("name")));
            Object tasksValue = sectionMap.get("tasks");
            List<StructuredTaskData> tasks = new ArrayList<>();
            if (tasksValue instanceof List<?> rawTasks) {
                for (Object rawTask : rawTasks) {
                    Map<String, Object> taskMap;
                    try {
                        taskMap = JsonUtils.convertObject(rawTask, new TypeReference<Map<String, Object>>() {});
                    } catch (IllegalArgumentException ignored) {
                        continue;
                    }
                    if (taskMap == null) {
                        continue;
                    }
                    String title = textOf(firstNonNull(taskMap.get("title"), taskMap.get("name")));
                    if (!StringUtils.hasText(title)) {
                        continue;
                    }
                    tasks.add(new StructuredTaskData(
                            title,
                            textOf(firstNonNull(taskMap.get("detail"), taskMap.get("content"))),
                            textOf(taskMap.get("priority")),
                            parseDecimal(taskMap.get("progress")),
                            parseDecimal(taskMap.get("hours"))));
                }
            }
            if (StringUtils.hasText(category) || !tasks.isEmpty()) {
                result.add(new StructuredSectionData(StringUtils.hasText(category) ? category : "工作内容", tasks));
            }
        }
        return result;
    }

    private List<StructuredSectionData> parseStructuredSectionsFromText(String textValue) {
        if (!StringUtils.hasText(textValue)) {
            return List.of();
        }
        String[] lines = normalizeLineBreaks(text(textValue)).split("\\R");
        Map<String, List<StructuredTaskData>> sectionMap = new LinkedHashMap<>();
        String currentCategory = null;
        for (String line : lines) {
            String trimmed = text(line);
            if (!StringUtils.hasText(trimmed)) {
                continue;
            }
            if (trimmed.startsWith("#")) {
                currentCategory = normalizeSectionCategory(trimmed.replaceFirst("^#\\s*", ""));
                sectionMap.putIfAbsent(currentCategory, new ArrayList<>());
                continue;
            }
            if (appendDetailContinuation(sectionMap, currentCategory, trimmed)) {
                continue;
            }
            StructuredLineData legacyLine = parseLegacyStructuredLine(trimmed);
            if (legacyLine != null) {
                sectionMap.computeIfAbsent(legacyLine.category(), key -> new ArrayList<>()).add(legacyLine.task());
                continue;
            }
            StructuredTaskData task = parseSimpleStructuredLine(trimmed);
            if (task != null) {
                sectionMap.computeIfAbsent(StringUtils.hasText(currentCategory) ? currentCategory : "工作内容",
                        key -> new ArrayList<>()).add(task);
            }
        }
        return sectionMap.entrySet().stream()
                .map(entry -> new StructuredSectionData(normalizeSectionCategory(entry.getKey()), entry.getValue()))
                .toList();
    }

    private StructuredLineData parseLegacyStructuredLine(String line) {
        java.util.regex.Matcher matcher = java.util.regex.Pattern
                .compile("^(.+?)\\s*[-－]\\s*(.+)$")
                .matcher(line);
        if (!matcher.matches()) {
            return null;
        }
        String category = text(matcher.group(1));
        StructuredTaskData task = parseSimpleStructuredLine(matcher.group(2));
        if (!StringUtils.hasText(category) || task == null) {
            return null;
        }
        return new StructuredLineData(category, task);
    }

    private StructuredTaskData parseSimpleStructuredLine(String line) {
        String normalized = text(line).replaceFirst("^\\d+[.．、]\\s*", "");
        if (!StringUtils.hasText(normalized)) {
            return null;
        }
        TaskDetailParts parts = splitTaskDetail(normalized);
        java.util.regex.Matcher matcher = java.util.regex.Pattern
                .compile("^(.*?)(?:[（(]([^()（）]*)[）)])?[。.!！？?]*$")
                .matcher(parts.title());
        if (!matcher.matches()) {
            return new StructuredTaskData(parts.title(), parts.detail(), null, null, null);
        }
        String title = text(matcher.group(1));
        if (!StringUtils.hasText(title)) {
            return null;
        }
        StructuredMetricsData metrics = parseMetrics(text(matcher.group(2)));
        return new StructuredTaskData(title, parts.detail(), metrics.priority(), metrics.progress(), metrics.hours());
    }

    private StructuredMetricsData parseMetrics(String metricsText) {
        if (!StringUtils.hasText(metricsText)) {
            return new StructuredMetricsData(null, null, null);
        }
        String[] parts = metricsText.split("/");
        String priority = null;
        BigDecimal progress = null;
        BigDecimal hours = null;
        for (String part : parts) {
            String trimmed = text(part);
            if (!StringUtils.hasText(trimmed)) {
                continue;
            }
            if (priority == null && trimmed.matches("(?i)^P?\\d+$")) {
                priority = normalizePriority(trimmed);
                continue;
            }
            if (progress == null && trimmed.matches("^\\d+(?:\\.\\d+)?%$")) {
                progress = parseDecimal(trimmed.substring(0, trimmed.length() - 1));
                continue;
            }
            if (progress == null && trimmed.matches("^进度\\s*\\d+(?:\\.\\d+)?%$")) {
                progress = parseDecimal(trimmed.replaceFirst("^进度\\s*", "").replace("%", ""));
                continue;
            }
            if (hours == null && trimmed.matches("^\\d+(?:\\.\\d+)?h$")) {
                hours = parseDecimal(trimmed.substring(0, trimmed.length() - 1));
            }
        }
        return new StructuredMetricsData(priority, progress, hours);
    }

    private String formatStructuredTask(StructuredTaskData task, boolean includeHours, Map<String, String> priorityLabelMap) {
        List<String> metrics = new ArrayList<>();
        if (StringUtils.hasText(task.priority())) {
            metrics.add(resolvePriorityLabel(task.priority(), priorityLabelMap));
        }
        if (task.progress() != null) {
            metrics.add("进度 " + formatDecimal(task.progress()) + "%");
        }
        if (includeHours && task.hours() != null) {
            metrics.add(formatDecimal(task.hours()) + "h");
        }
        return text(task.title()) + (metrics.isEmpty() ? "" : "（" + String.join(" / ", metrics) + "）");
    }

    private String normalizePriority(String value) {
        String trimmed = text(value);
        if (!StringUtils.hasText(trimmed)) {
            return "";
        }
        if (trimmed.matches("^\\d+\\.0+$")) {
            trimmed = trimmed.substring(0, trimmed.indexOf('.'));
        }
        if (trimmed.matches("(?i)^P\\d+$")) {
            return trimmed.toUpperCase();
        }
        if (trimmed.matches("^\\d+$")) {
            return "P" + trimmed;
        }
        return trimmed;
    }

    private String resolvePriorityLabel(String value, Map<String, String> priorityLabelMap) {
        String normalized = normalizePriority(value);
        if (!StringUtils.hasText(normalized)) {
            return "";
        }
        if (normalized.matches("(?i)^P\\d+$")) {
            return normalized.toUpperCase();
        }
        String rawValue = text(value);
        String label = priorityLabelMap.get(rawValue);
        if (!StringUtils.hasText(label) && rawValue.matches("^\\d+\\.0+$")) {
            label = priorityLabelMap.get(rawValue.substring(0, rawValue.indexOf('.')));
        }
        if (!StringUtils.hasText(label)) {
            label = priorityLabelMap.get(normalized.replaceFirst("(?i)^P", ""));
        }
        if (!StringUtils.hasText(label)) {
            label = priorityLabelMap.get(normalized);
        }
        return StringUtils.hasText(label) ? label : normalized;
    }

    private boolean appendDetailContinuation(Map<String, List<StructuredTaskData>> sectionMap,
                                             String currentCategory, String line) {
        if (looksLikeStructuredTaskStart(line)) {
            return false;
        }
        String category = StringUtils.hasText(currentCategory) ? currentCategory : lastCategory(sectionMap);
        if (!StringUtils.hasText(category)) {
            return false;
        }
        List<StructuredTaskData> tasks = sectionMap.get(category);
        if (tasks == null || tasks.isEmpty()) {
            return false;
        }
        int lastIndex = tasks.size() - 1;
        StructuredTaskData lastTask = tasks.get(lastIndex);
        if (!StringUtils.hasText(lastTask.detail())) {
            return false;
        }
        tasks.set(lastIndex, new StructuredTaskData(lastTask.title(),
                lastTask.detail() + "\n" + line,
                lastTask.priority(), lastTask.progress(), lastTask.hours()));
        return true;
    }

    private boolean looksLikeStructuredTaskStart(String line) {
        String value = text(line);
        return value.startsWith("#")
                || value.matches("^\\d+[.．、]\\s*.+")
                || value.matches("^.+?\\s*[-－]\\s*.+");
    }

    private String lastCategory(Map<String, List<StructuredTaskData>> sectionMap) {
        if (sectionMap == null || sectionMap.isEmpty()) {
            return null;
        }
        String result = null;
        for (String category : sectionMap.keySet()) {
            result = category;
        }
        return result;
    }

    private TaskDetailParts splitTaskDetail(String value) {
        String source = text(value);
        if (!StringUtils.hasText(source)) {
            return new TaskDetailParts("", "");
        }
        java.util.regex.Matcher matcher = java.util.regex.Pattern
                .compile("^(.*?)(?:[：:]\\s*)(.*)$")
                .matcher(source);
        if (!matcher.matches()) {
            return new TaskDetailParts(source, "");
        }
        return new TaskDetailParts(text(matcher.group(1)), text(matcher.group(2)));
    }

    private List<String> splitTextLines(String value) {
        String normalized = normalizeLineBreaks(text(value));
        if (!StringUtils.hasText(normalized)) {
            return List.of();
        }
        List<String> result = new ArrayList<>();
        for (String line : normalized.split("\\R")) {
            String textLine = text(line);
            if (StringUtils.hasText(textLine)) {
                result.add(textLine);
            }
        }
        return result;
    }

    private String normalizeLineBreaks(String value) {
        return text(value)
                .replace("\\r\\n", "\n")
                .replace("\\n", "\n")
                .replace("\\r", "\n");
    }

    private Map<String, String> loadPriorityLabelMap() {
        CommonResult<List<DictDataRespDTO>> result = dictDataApi.getDictDataList(ProjectDictTypeConstants.REQ_PRIORITY);
        List<DictDataRespDTO> dictDataList = result == null ? null : result.getCheckedData();
        if (dictDataList == null || dictDataList.isEmpty()) {
            return Map.of();
        }
        Map<String, String> labelMap = new HashMap<>();
        for (DictDataRespDTO item : dictDataList) {
            if (item == null) {
                continue;
            }
            String value = text(item.getValue());
            String label = text(item.getLabel());
            if (StringUtils.hasText(value) && StringUtils.hasText(label)) {
                labelMap.put(value, label);
            }
        }
        return labelMap;
    }

    private String normalizeSectionCategory(String category) {
        return text(category).replaceFirst("[：:]+$", "");
    }

    private BigDecimal parseDecimal(Object value) {
        if (value == null) {
            return null;
        }
        if (value instanceof BigDecimal decimal) {
            return decimal;
        }
        if (value instanceof Number number) {
            return BigDecimal.valueOf(number.doubleValue());
        }
        if (value instanceof String textValue && StringUtils.hasText(textValue)) {
            try {
                return new BigDecimal(textValue.trim());
            } catch (NumberFormatException ignored) {
                return null;
            }
        }
        return null;
    }

    private Object firstNonNull(Object... values) {
        if (values == null) {
            return null;
        }
        for (Object value : values) {
            if (value != null) {
                return value;
            }
        }
        return null;
    }

    private String buildProjectItemsText(List<ProjectReportItemRespVO> items, boolean includeHours) {
        if (items == null || items.isEmpty()) {
            return "";
        }
        Map<String, String> priorityLabelMap = loadPriorityLabelMap();
        List<String> lines = new ArrayList<>(items.size());
        int index = 1;
        for (ProjectReportItemRespVO item : items) {
            String itemTitle = text(item == null ? null : item.getItemTitle());
            if (!StringUtils.hasText(itemTitle)) {
                continue;
            }
            List<String> metrics = new ArrayList<>();
            if (item != null && StringUtils.hasText(item.getPriorityCode())) {
                metrics.add(resolvePriorityLabel(item.getPriorityCode(), priorityLabelMap));
            }
            if (item != null && item.getProgressRate() != null) {
                metrics.add(formatProgress(item.getProgressRate()));
            }
            if (includeHours && item != null && item.getWorkHours() != null) {
                metrics.add(formatDecimal(item.getWorkHours()) + "h");
            }
            String suffix = metrics.isEmpty() ? "" : "（" + String.join("/", metrics) + "）";
            lines.add(index++ + "、" + itemTitle + suffix + "。");
        }
        return String.join("\n", lines);
    }

    private void addTitle(WordprocessingMLPackage document, String title) {
        P paragraph = createParagraph(title, true, TITLE_FONT_SIZE, JcEnumeration.CENTER);
        document.getMainDocumentPart().addObject(paragraph);
    }

    private void addSectionTitle(WordprocessingMLPackage document, String title) {
        P paragraph = createParagraph(title, true, SECTION_TITLE_FONT_SIZE, null);
        document.getMainDocumentPart().addObject(paragraph);
    }

    private void addParagraph(WordprocessingMLPackage document, String text, boolean bold) {
        P paragraph = createParagraph(text, bold, DEFAULT_FONT_SIZE, null);
        document.getMainDocumentPart().addObject(paragraph);
    }

    private void addPersonalBaseTable(WordprocessingMLPackage document, String name, String deptName,
                                      String postName, String managerName) {
        addKeyValueTable(document, List.of(
                row("姓名", name),
                row("部门", deptName),
                row("岗位", postName),
                row("直接上级", managerName)));
    }

    private void addPersonalReviewTable(WordprocessingMLPackage document, List<PersonalReportReviewItemRespVO> items,
                                        boolean includeDetail) {
        Tbl table = TblFactory.createTable(1, 4, 2000);
        setTableWidth(table);
        setRowText(tableRow(table, 0), List.of("序号", "工作事项", "具体工作内容及成果描述", "工作感悟"), true);
        for (PersonalReportReviewItemRespVO item : safeList(items)) {
            Tr row = addTableRow(table, 4);
            List<Tc> cells = rowCells(row);
            setCellText(cells.get(0), formatNumber(item.getItemNumber()), false);
            setCellText(cells.get(1), appendHours(item.getItemTitle(), item.getWorkHours()), false);
            setCellText(cells.get(2), buildStructuredReviewText(item, includeDetail), false);
            setCellText(cells.get(3), item.getReflectionText(), false);
        }
        document.getMainDocumentPart().addObject(table);
    }

    private void addPersonalPlanTable(WordprocessingMLPackage document, List<PersonalReportPlanItemRespVO> items,
                                      boolean includeDetail) {
        Tbl table = TblFactory.createTable(1, 4, 2000);
        setTableWidth(table);
        setRowText(tableRow(table, 0), List.of("序号", "工作事项", "具体目标", "对他人协助的需求"), true);
        for (PersonalReportPlanItemRespVO item : safeList(items)) {
            Tr row = addTableRow(table, 4);
            List<Tc> cells = rowCells(row);
            setCellText(cells.get(0), formatNumber(item.getItemNumber()), false);
            setCellText(cells.get(1), item.getItemTitle(), false);
            setCellText(cells.get(2), buildStructuredPlanText(item, includeDetail), false);
            setCellText(cells.get(3), item.getSupportNeed(), false);
        }
        document.getMainDocumentPart().addObject(table);
    }

    private void addFeedbackTable(WordprocessingMLPackage document, MonthlyReportApprovalRecordRespVO record) {
        Tbl table = TblFactory.createTable(1, 4, 2000);
        setTableWidth(table);
        setRowText(tableRow(table, 0), List.of("优势/劣势项", "优势/劣势描述", "行为事例", "改进提升建议"), true);
        Tr strengthRow = addTableRow(table, 4);
        setRowText(strengthRow, values("优势",
                record == null ? null : record.getStrengthDesc(),
                record == null ? null : record.getStrengthExample(),
                ""), false);
        Tr weaknessRow = addTableRow(table, 4);
        setRowText(weaknessRow, values("劣势",
                record == null ? null : record.getWeaknessDesc(),
                record == null ? null : record.getWeaknessExample(),
                record == null ? null : record.getImprovementSuggestion()), false);
        document.getMainDocumentPart().addObject(table);
    }

    private void addProjectItemTable(WordprocessingMLPackage document, List<ProjectReportItemRespVO> items, boolean showHours) {
        Tbl table = TblFactory.createTable(1, showHours ? 4 : 3, 2000);
        setTableWidth(table);
        setRowText(tableRow(table, 0), showHours
                ? List.of("工作内容", "工时", "优先级", "进度")
                : List.of("工作内容", "优先级", "进度"), true);
        for (ProjectReportItemRespVO item : safeList(items)) {
            Tr row = addTableRow(table, showHours ? 4 : 3);
            setRowText(row, showHours
                    ? values(item.getItemTitle(), formatDecimal(item.getWorkHours()), item.getPriorityCode(),
                    formatProgress(item.getProgressRate()))
                    : values(item.getItemTitle(), item.getPriorityCode(), formatProgress(item.getProgressRate())),
                    false);
        }
        document.getMainDocumentPart().addObject(table);
    }

    private void addKeyValueTable(WordprocessingMLPackage document, List<List<String>> rows) {
        Tbl table = TblFactory.createTable(rows.size(), 2, 4000);
        setTableWidth(table);
        for (int i = 0; i < rows.size(); i++) {
            setRowText(tableRow(table, i), rows.get(i), i == 0 && rows.size() == 1);
        }
        document.getMainDocumentPart().addObject(table);
    }

    private void setTableWidth(Tbl table) {
        TblPr tblPr = table.getTblPr();
        if (tblPr == null) {
            tblPr = WML_FACTORY.createTblPr();
            table.setTblPr(tblPr);
        }
        TblWidth width = tblPr.getTblW();
        if (width == null) {
            width = WML_FACTORY.createTblWidth();
            tblPr.setTblW(width);
        }
        width.setType("pct");
        width.setW(BigInteger.valueOf(5000));
    }

    private void setRowText(Tr row, List<String> values, boolean bold) {
        List<Tc> cells = rowCells(row);
        for (int i = 0; i < values.size() && i < cells.size(); i++) {
            setCellText(cells.get(i), values.get(i), bold);
        }
    }

    private void setCellText(Tc cell, String value, boolean bold) {
        clearCellContent(cell);
        cell.getContent().add(createParagraphWithBreaks(value, bold, true));
    }

    private void setCenteredCellText(Tc cell, String value, boolean bold) {
        setCellText(cell, value, bold);
        applyCellAlignment(cell, JcEnumeration.CENTER, STVerticalJc.CENTER, false);
    }

    private void setStructuredTemplateCellParagraphs(Tc cell, List<String> lines, boolean bold) {
        unsetNoWrap(cell);
        clearCellContent(cell);
        List<String> safeLines = (lines == null || lines.isEmpty()) ? List.of("") : lines;
        for (String line : safeLines) {
            P paragraph = createParagraph(line, bold, DEFAULT_FONT_SIZE, null);
            applyParagraphFormatting(paragraph, line, false);
            cell.getContent().add(paragraph);
        }
    }

    private void applyCellAlignment(Tc cell, JcEnumeration horizontal, STVerticalJc vertical, boolean firstLineIndent) {
        if (cell == null) {
            return;
        }
        TcPr tcPr = cell.getTcPr();
        if (tcPr == null) {
            tcPr = WML_FACTORY.createTcPr();
            cell.setTcPr(tcPr);
        }
        if (vertical != null) {
            CTVerticalJc verticalJc = WML_FACTORY.createCTVerticalJc();
            verticalJc.setVal(vertical);
            tcPr.setVAlign(verticalJc);
        }
        for (P paragraph : findAllParagraphs(cell)) {
            ensureParagraphFormatting(paragraph, horizontal, firstLineIndent);
        }
    }

    private void applyParagraphFormatting(P paragraph, String value, boolean firstLineIndent) {
        ensureParagraphFormatting(paragraph, null, firstLineIndent);
        PPr pPr = paragraph.getPPr();
        PPrBase.Spacing spacing = pPr.getSpacing();
        if (spacing == null) {
            spacing = WML_FACTORY.createPPrBaseSpacing();
            pPr.setSpacing(spacing);
        }
        spacing.setBefore(BigInteger.ZERO);
        spacing.setAfter(BigInteger.ZERO);
        if (!StringUtils.hasText(value)) {
            spacing.setLineRule(STLineSpacingRule.AUTO);
        }
    }

    private void ensureParagraphFormatting(P paragraph, JcEnumeration alignment, boolean firstLineIndent) {
        PPr pPr = paragraph.getPPr();
        if (pPr == null) {
            pPr = WML_FACTORY.createPPr();
            paragraph.setPPr(pPr);
        }
        pPr.setNumPr(null);
        if (alignment != null) {
            Jc jc = pPr.getJc();
            if (jc == null) {
                jc = WML_FACTORY.createJc();
                pPr.setJc(jc);
            }
            jc.setVal(alignment);
        }
        PPrBase.Ind ind = pPr.getInd();
        if (ind == null) {
            ind = WML_FACTORY.createPPrBaseInd();
            pPr.setInd(ind);
        }
        ind.setLeft(BigInteger.ZERO);
        ind.setLeftChars(BigInteger.ZERO);
        ind.setRight(BigInteger.ZERO);
        ind.setRightChars(BigInteger.ZERO);
        ind.setStart(BigInteger.ZERO);
        ind.setStartChars(BigInteger.ZERO);
        ind.setEnd(BigInteger.ZERO);
        ind.setEndChars(BigInteger.ZERO);
        ind.setHanging(BigInteger.ZERO);
        ind.setHangingChars(BigInteger.ZERO);
        ind.setFirstLine(firstLineIndent ? BigInteger.valueOf(420) : BigInteger.ZERO);
        ind.setFirstLineChars(BigInteger.ZERO);
    }

    private void unsetNoWrap(Tc cell) {
        if (cell == null) {
            return;
        }
        TcPr tcPr = cell.getTcPr();
        if (tcPr != null) {
            tcPr.setNoWrap(null);
        }
    }

    private void setCellParagraphs(Tc cell, List<String> lines, boolean bold) {
        clearCellContent(cell);
        List<String> safeLines = lines == null || lines.isEmpty() ? List.of("") : lines;
        for (String line : safeLines) {
            P paragraph = createParagraph(line, bold, DEFAULT_FONT_SIZE, null);
            applyParagraphFormatting(paragraph, line, false);
            cell.getContent().add(paragraph);
        }
    }

    private byte[] toBytes(WordprocessingMLPackage document) throws IOException {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        try {
            document.save(outputStream);
        } catch (Docx4JException e) {
            throw new IOException("save docx failed", e);
        }
        return outputStream.toByteArray();
    }

    private P createParagraph(String value, boolean bold, BigInteger fontSize, JcEnumeration alignment) {
        P paragraph = WML_FACTORY.createP();
        ensureParagraphFormatting(paragraph, alignment, false);
        R run = createRun(value, bold, fontSize);
        paragraph.getContent().add(run);
        return paragraph;
    }

    private P createParagraphWithBreaks(String value, boolean bold, boolean preserveEmptyLines) {
        P paragraph = WML_FACTORY.createP();
        R run = WML_FACTORY.createR();
        run.setRPr(createRunProperties(bold, DEFAULT_FONT_SIZE));
        String[] lines = splitRawLines(value);
        for (int i = 0; i < lines.length; i++) {
            if (i > 0) {
                run.getContent().add(WML_FACTORY.createBr());
            }
            if (preserveEmptyLines || StringUtils.hasText(lines[i])) {
                Text textNode = WML_FACTORY.createText();
                textNode.setValue(lines[i]);
                if (lines[i].startsWith(" ") || lines[i].endsWith(" ")) {
                    textNode.setSpace("preserve");
                }
                run.getContent().add(textNode);
            }
        }
        paragraph.getContent().add(run);
        applyParagraphFormatting(paragraph, value, false);
        return paragraph;
    }

    private void rewriteParagraphText(P paragraph, String value, boolean preserveEmptyLines) {
        R firstRun = findFirstRun(paragraph);
        RPr originalRunProperties = firstRun == null ? null : firstRun.getRPr();
        paragraph.getContent().clear();
        R run = WML_FACTORY.createR();
        if (originalRunProperties != null) {
            run.setRPr((RPr) XmlUtils.deepCopy(originalRunProperties));
        }
        String[] lines = splitRawLines(value);
        for (int i = 0; i < lines.length; i++) {
            if (i > 0) {
                run.getContent().add(WML_FACTORY.createBr());
            }
            if (preserveEmptyLines || StringUtils.hasText(lines[i])) {
                Text textNode = WML_FACTORY.createText();
                textNode.setValue(lines[i]);
                if (lines[i].startsWith(" ") || lines[i].endsWith(" ")) {
                    textNode.setSpace("preserve");
                }
                run.getContent().add(textNode);
            }
        }
        paragraph.getContent().add(run);
    }

    private P findParagraphByPlaceholder(Tc cell, String placeholder) {
        if (cell == null || !StringUtils.hasText(placeholder)) {
            return null;
        }
        for (P paragraph : findAllParagraphs(cell)) {
            String paragraphText = findAllTextNodes(paragraph).stream()
                    .map(Text::getValue)
                    .filter(Objects::nonNull)
                    .collect(java.util.stream.Collectors.joining());
            if (paragraphText.contains(placeholder)) {
                return paragraph;
            }
        }
        return null;
    }

    private void setTemplateCellParagraphs(Tc cell, P templateParagraph, List<String> lines, boolean bold) {
        unsetNoWrap(cell);
        clearCellContent(cell);
        List<String> safeLines = (lines == null || lines.isEmpty()) ? List.of("") : lines;
        for (String line : safeLines) {
            P paragraph = templateParagraph == null
                    ? createParagraph(line, bold, DEFAULT_FONT_SIZE, null)
                    : (P) XmlUtils.deepCopy(templateParagraph);
            rewriteParagraphText(paragraph, line, true);
            applyParagraphFormatting(paragraph, line, false);
            cell.getContent().add(paragraph);
        }
    }

    private R findFirstRun(P paragraph) {
        if (paragraph == null) {
            return null;
        }
        for (Object content : paragraph.getContent()) {
            Object value = XmlUtils.unwrap(content);
            if (value instanceof R run) {
                return run;
            }
        }
        return null;
    }

    private String[] splitRawLines(String value) {
        String source = value == null ? "" : value;
        source = source.replace("\r\n", "\n").replace('\r', '\n');
        return source.split("\\R", -1);
    }

    private List<String> splitTextLinesPreserveBlank(String value) {
        String normalized = normalizeLineBreaks(text(value));
        if (!StringUtils.hasText(normalized)) {
            return List.of();
        }
        return Arrays.asList(normalized.split("\\R", -1));
    }

    private R createRun(String value, boolean bold, BigInteger fontSize) {
        R run = WML_FACTORY.createR();
        run.setRPr(createRunProperties(bold, fontSize));
        Text textNode = WML_FACTORY.createText();
        textNode.setValue(text(value));
        run.getContent().add(textNode);
        return run;
    }

    private RPr createRunProperties(boolean bold, BigInteger fontSize) {
        RPr rPr = WML_FACTORY.createRPr();
        if (bold) {
            BooleanDefaultTrue b = WML_FACTORY.createBooleanDefaultTrue();
            b.setVal(Boolean.TRUE);
            rPr.setB(b);
            rPr.setBCs(b);
        }
        HpsMeasure size = WML_FACTORY.createHpsMeasure();
        size.setVal(fontSize);
        rPr.setSz(size);
        HpsMeasure sizeCs = WML_FACTORY.createHpsMeasure();
        sizeCs.setVal(fontSize);
        rPr.setSzCs(sizeCs);
        return rPr;
    }

    private Tr addTableRow(Tbl table, int cellCount) {
        Tr row = WML_FACTORY.createTr();
        for (int i = 0; i < cellCount; i++) {
            Tc cell = WML_FACTORY.createTc();
            cell.getContent().add(WML_FACTORY.createP());
            row.getContent().add(cell);
        }
        table.getContent().add(row);
        return row;
    }

    private List<Tc> rowCells(Tr row) {
        List<Tc> cells = new ArrayList<>();
        for (Object content : row.getContent()) {
            Object value = XmlUtils.unwrap(content);
            if (value instanceof Tc cell) {
                cells.add(cell);
            }
        }
        return cells;
    }

    private void clearCellContent(Tc cell) {
        cell.getContent().clear();
    }
    private String buildTravelText(List<WeeklyReportTravelSegmentRespVO> travelSegments) {
        if (travelSegments == null || travelSegments.isEmpty()) {
            return "";
        }
        return travelSegments.stream()
                .map(item -> formatDate(item.getStartDate()) + "-" + formatDate(item.getEndDate())
                        + "，" + formatDecimal(item.getTravelDays()) + "天，" + text(item.getLocation()))
                .collect(java.util.stream.Collectors.joining("\n"));
    }

    private String buildMemberText(List<WorkReportMemberSnapshotRespVO> members) {
        return safeList(members).stream()
                .map(WorkReportMemberSnapshotRespVO::getUserName)
                .filter(StringUtils::hasText)
                .collect(java.util.stream.Collectors.joining("、"));
    }

    private String appendHours(String title, BigDecimal hours) {
        if (hours == null) {
            return text(title);
        }
        return text(title) + "（" + formatDecimal(hours) + "h）";
    }

    private String formatPeriod(Object report) {
        if (report instanceof WeeklyReportRespVO weeklyReport) {
            return formatDate(weeklyReport.getPeriodStartDate()) + "-" + formatDate(weeklyReport.getPeriodEndDate());
        }
        if (report instanceof MonthlyReportRespVO monthlyReport) {
            return formatDate(monthlyReport.getPeriodStartDate()) + "-" + formatDate(monthlyReport.getPeriodEndDate());
        }
        if (report instanceof ProjectReportRespVO projectReport) {
            return formatDate(projectReport.getPeriodStartDate()) + "-" + formatDate(projectReport.getPeriodEndDate());
        }
        return "";
    }

    private String formatDate(LocalDate value) {
        return value == null ? "" : DATE_FORMATTER.format(value);
    }

    private String formatDateTime(LocalDateTime value) {
        return value == null ? "" : DATE_TIME_FORMATTER.format(value);
    }

    private String formatNumber(Integer value) {
        return value == null ? "" : String.valueOf(value);
    }

    private String formatDecimal(BigDecimal value) {
        return value == null ? "" : value.stripTrailingZeros().toPlainString();
    }

    private String formatProgress(BigDecimal value) {
        return value == null ? "" : value.stripTrailingZeros().toPlainString() + "%";
    }

    private String buildFilename(String reportTypeName, String ownerName, String periodLabel) {
        return sanitizeFilename(reportTypeName + "_" + text(ownerName) + "_" + text(periodLabel)) + ".docx";
    }

    private String buildWeeklyFilename(String reportTypeName, String ownerName, LocalDate periodStartDate, String periodLabel) {
        if (periodStartDate == null) {
            return buildFilename(reportTypeName, ownerName, periodLabel);
        }
        int weekBasedYear = periodStartDate.get(IsoFields.WEEK_BASED_YEAR);
        int weekOfYear = periodStartDate.get(IsoFields.WEEK_OF_WEEK_BASED_YEAR);
        return sanitizeFilename(reportTypeName + "_" + text(ownerName) + "_"
                + weekBasedYear + "年第" + weekOfYear + "周") + ".docx";
    }

    private String sanitizeFilename(String filename) {
        return filename.replaceAll("[\\\\/:*?\"<>|\\r\\n]+", "_");
    }

    private String text(String value) {
        return StringUtils.hasText(value) ? value.trim() : "";
    }

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

    private List<String> row(String label, String value) {
        return List.of(text(label), text(value));
    }

    private List<String> values(String... values) {
        List<String> result = new ArrayList<>(values.length);
        for (String value : values) {
            result.add(text(value));
        }
        return result;
    }

    private <T> List<T> safeList(List<T> source) {
        return source == null ? List.of() : source;
    }

    private record StructuredSectionData(String category, List<StructuredTaskData> tasks) {
    }

    private record StructuredTaskData(String title, String detail, String priority, BigDecimal progress, BigDecimal hours) {
    }

    private record StructuredMetricsData(String priority, BigDecimal progress, BigDecimal hours) {
    }

    private record StructuredLineData(String category, StructuredTaskData task) {
    }

    private record TaskDetailParts(String title, String detail) {
    }

    private record DocumentContent(String filename, byte[] content) {
    }
}
