from __future__ import annotations

from datetime import datetime
import html as html_lib
from pathlib import Path
import re
from xml.sax.saxutils import escape
from zipfile import ZIP_DEFLATED, ZipFile

from .font_policy import font_runs


_INVALID_XML_RE = re.compile(r"[\x00-\x08\x0B\x0C\x0E-\x1F]")
_WORD_USABLE_WIDTH_DXA = 10800
_WORD_MAX_TABLE_COLS = 8
_SHEET_NAME_INVALID_RE = re.compile(r"[\[\]\*\?/\\:]")


def _export_header_text(project_name: str = "", version: str = "") -> str:
    if project_name or version:
        left = str(project_name or "未命名工程").strip()
        right = str(version or "未标注版本").strip()
        return f"{left} + {right}"
    return "未命名工程  未标注版本"


def _export_watermark_text(watermark: str = "") -> str:
    value = str(watermark or "").strip()
    return value if value in {"送审稿", "报批稿", "正式稿"} else (value or "送审稿")


def _is_table_border(line: str) -> bool:
    stripped = line.strip()
    return (
        len(stripped) >= 3
        and stripped.startswith("+")
        and stripped.endswith("+")
        and set(stripped) <= {"+", "-", "="}
    )


def _split_fixed_width_cells(line: str) -> list[str] | None:
    stripped = line.strip()
    if not stripped or "\t" in line or "|" in line:
        return None
    if not re.search(r"\s{2,}", stripped):
        return None
    cells = [cell.strip() for cell in re.split(r"\s{2,}", stripped) if cell.strip()]
    if len(cells) < 2:
        return None
    return cells


def _parse_blocks(content: str) -> list[tuple[str, object]]:
    blocks: list[tuple[str, object]] = []
    table: list[list[str]] = []
    table_mode: str | None = None

    def flush_table() -> None:
        nonlocal table, table_mode
        if table:
            if table_mode == "fixed" and len(table) == 1:
                blocks.append(("paragraph", "    ".join(table[0])))
            else:
                blocks.append(("table", table))
            table = []
            table_mode = None

    for raw_line in content.splitlines():
        line = raw_line.rstrip("\n")
        stripped = line.strip()
        if _is_table_border(stripped):
            continue
        cells: list[str] | None = None
        mode: str | None = None
        if stripped.startswith("|") and stripped.endswith("|"):
            cells = [cell.strip() for cell in stripped[1:-1].split("|")]
            mode = "pipe"
        elif "\t" in line:
            tab_cells = [cell.strip() for cell in line.split("\t")]
            if len(tab_cells) >= 2 and any(tab_cells):
                cells = tab_cells
                mode = "tab"
        else:
            fixed_cells = _split_fixed_width_cells(line)
            if fixed_cells is not None:
                cells = fixed_cells
                mode = "fixed"

        if cells is not None and mode is not None:
            if table_mode is not None and table_mode != mode:
                flush_table()
            table_mode = mode
            table.append(cells)
        else:
            flush_table()
            blocks.append(("paragraph", line))
    flush_table()
    return blocks


def parse_calculation_blocks(content: str) -> list[tuple[str, object]]:
    return _parse_blocks(content)


def _clean_text(text: object) -> str:
    return _INVALID_XML_RE.sub("", str(text))


def _xml_text(text: object) -> str:
    return escape(_clean_text(text), {'"': "&quot;"})


def _column_name(index: int) -> str:
    name = ""
    while index:
        index, remainder = divmod(index - 1, 26)
        name = chr(65 + remainder) + name
    return name


def _xlsx_cell(row: int, col: int, value: object, style: int = 0) -> str:
    ref = f"{_column_name(col)}{row}"
    style_attr = f' s="{style}"' if style else ""
    return (
        f'<c r="{ref}" t="inlineStr"{style_attr}>'
        f"<is>{_xlsx_text_runs(value, size=16 if style == 2 else 11, bold=style in (1, 2, 3))}</is>"
        "</c>"
    )


def _xlsx_text_runs(value: object, *, size=11, bold=False, italic=False) -> str:
    # Explicit size/emphasis keeps rich text consistent across spreadsheet viewers.
    emphasis = ('<b/>' if bold else '') + ('<i/>' if italic else '')
    return "".join(
        f'<r><rPr><rFont val="{family}"/>{emphasis}<sz val="{size}"/></rPr><t xml:space="preserve">{_xml_text(text)}</t></r>'
        for family, text in font_runs(_clean_text(value))
    ) or '<t/>'


def _xlsx_header_runs(value: object) -> str:
    return _xml_text("".join(
        f'&"{family},Regular"{text.replace("&", "&&")}'
        for family, text in font_runs(_clean_text(value))
    ))


def _is_heading_line(text: str) -> bool:
    stripped = text.strip()
    return bool(
        stripped
        and (
            re.match(r"^[一二三四五六七八九十]+[、.．]", stripped)
            or re.match(r"^\d+[、.．]", stripped)
            or stripped.endswith(("计算", "成果表", "参数表", "过程线", "结果"))
        )
    )


def _safe_sheet_name(name: str, used: set[str]) -> str:
    cleaned = _SHEET_NAME_INVALID_RE.sub("", str(name).strip()) or "Sheet"
    cleaned = cleaned[:31]
    candidate = cleaned
    index = 2
    while candidate in used:
        suffix = f"_{index}"
        candidate = cleaned[: 31 - len(suffix)] + suffix
        index += 1
    used.add(candidate)
    return candidate


def _sheet_xml(
    rows: list[tuple[list[str], int]],
    *,
    project_name: str = "",
    version: str = "",
    watermark: str = "",
) -> str:
    max_cols = max((len(cells) for cells, _style in rows), default=1)
    column_widths = [10] * max_cols
    row_xml: list[str] = []
    for row_idx, (cells, style) in enumerate(rows, 1):
        normalized = [str(cell) for cell in cells]
        for col_idx, value in enumerate(normalized, 1):
            length = max((len(part) for part in str(value).splitlines()), default=0)
            column_widths[col_idx - 1] = min(46, max(column_widths[col_idx - 1], length + 4))
        row_cells = "".join(_xlsx_cell(row_idx, col_idx, value, style) for col_idx, value in enumerate(normalized, 1))
        height_attr = ' ht="30" customHeight="1"' if style == 2 else ""
        row_xml.append(f'<row r="{row_idx}"{height_attr}>{row_cells}</row>')

    cols_xml = "".join(
        f'<col min="{idx}" max="{idx}" width="{width}" customWidth="1"/>'
        for idx, width in enumerate(column_widths, 1)
    )
    max_ref = f"{_column_name(max_cols)}{max(len(rows), 1)}"
    header = _xlsx_header_runs(_export_header_text(project_name, version))
    footer = _xml_text('&"楷体,Regular"第 &"Times New Roman,Regular"&P&"楷体,Regular" 页 / 共 &"Times New Roman,Regular"&N&"楷体,Regular" 页')
    watermark_text = _xlsx_header_runs(_export_watermark_text(watermark))
    return f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
 xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<dimension ref="A1:{max_ref}"/>
<cols>{cols_xml}</cols>
<sheetData>{''.join(row_xml)}</sheetData>
<headerFooter><oddHeader>&amp;L{header}&amp;R{watermark_text}</oddHeader><oddFooter>&amp;C{footer}</oddFooter></headerFooter>
<pageMargins left="0.5" right="0.5" top="0.75" bottom="0.75" header="0.3" footer="0.3"/>
</worksheet>"""


def _build_calculation_sheets(content: str, title: str) -> list[tuple[str, list[tuple[list[str], int]]]]:
    blocks = _parse_blocks(content)
    process_rows: list[tuple[list[str], int]] = [([title], 2), (["导出格式：专项校核式计算过程表"], 3), ([], 0)]
    table_infos: list[tuple[str, list[list[str]]]] = []
    current_heading = title

    for block_type, payload in blocks:
        if block_type == "paragraph":
            text = str(payload)
            if text.strip():
                if _is_heading_line(text):
                    current_heading = text.strip()
                process_rows.append(([text], 3 if _is_heading_line(text) else 0))
            else:
                process_rows.append(([], 0))
            continue

        table_rows = [[str(cell) for cell in row] for row in payload]  # type: ignore[assignment]
        table_infos.append((current_heading, table_rows))
        for row_index, row in enumerate(table_rows):
            process_rows.append((row, 1 if row_index == 0 else 4))
        process_rows.append(([], 0))

    summary_rows: list[tuple[list[str], int]] = [
        ([title], 2),
        (["生成时间", datetime.now().strftime("%Y-%m-%d %H:%M:%S")], 4),
        (["输出说明", "本工作簿按专项校核表样式组织：计算过程保留完整文本，所有表格另列为独立工作表，表格均采用实线网格、定宽居中。"], 4),
        (["表格数量", str(len(table_infos))], 4),
        ([], 0),
        (["序号", "对应章节", "工作表"], 1),
    ]

    used_names: set[str] = set()
    sheets: list[tuple[str, list[tuple[list[str], int]]]] = [
        (_safe_sheet_name("计算过程", used_names), process_rows),
        (_safe_sheet_name("结果总览", used_names), summary_rows),
    ]

    for index, (heading, rows) in enumerate(table_infos, 1):
        sheet_name = _safe_sheet_name(f"表{index}", used_names)
        sheets[1][1].append(([str(index), heading, sheet_name], 4))
        table_sheet_rows: list[tuple[list[str], int]] = [
            ([f"表{index}：{heading}"], 2),
            (["说明", "由计算结果自动拆分生成，便于复核、复制和二次整理。"], 3),
            ([], 0),
        ]
        for row_index, row in enumerate(rows):
            table_sheet_rows.append((row, 1 if row_index == 0 else 4))
        sheets.append((sheet_name, table_sheet_rows))
    return sheets


def export_calculation_book_xlsx(
    content: str,
    path: str | Path,
    title: str = "计算书",
    *,
    project_name: str = "",
    version: str = "",
    watermark: str = "",
    figures: list[tuple[str, str | Path]] | None = None,
) -> None:
    path = Path(path)
    sheets = _build_calculation_sheets(content, title)
    sheet_items = "".join(
        f'<sheet name="{_xml_text(name)}" sheetId="{idx}" r:id="rId{idx}"/>'
        for idx, (name, _rows) in enumerate(sheets, 1)
    )
    workbook_xml = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
 xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<sheets>""" + sheet_items + """</sheets>
</workbook>"""
    styles_xml = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<fonts count="3"><font><sz val="11"/><name val="Times New Roman"/></font><font><b/><sz val="11"/><name val="Times New Roman"/></font><font><b/><sz val="16"/><name val="Times New Roman"/></font></fonts>
<fills count="3"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill><fill><patternFill patternType="solid"><fgColor rgb="FFD9EAD3"/><bgColor indexed="64"/></patternFill></fill></fills>
<borders count="2"><border><left/><right/><top/><bottom/><diagonal/></border><border><left style="thin"/><right style="thin"/><top style="thin"/><bottom style="thin"/><diagonal/></border></borders>
<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>
<cellXfs count="5">
<xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0" applyAlignment="1"><alignment vertical="center" wrapText="1"/></xf>
<xf numFmtId="0" fontId="1" fillId="2" borderId="1" xfId="0" applyAlignment="1" applyBorder="1" applyFill="1"><alignment horizontal="center" vertical="center" wrapText="1"/></xf>
<xf numFmtId="0" fontId="2" fillId="0" borderId="0" xfId="0" applyAlignment="1"><alignment horizontal="center" vertical="center" wrapText="1"/></xf>
<xf numFmtId="0" fontId="1" fillId="0" borderId="0" xfId="0" applyAlignment="1"><alignment vertical="center" wrapText="1"/></xf>
<xf numFmtId="0" fontId="0" fillId="2" borderId="1" xfId="0" applyAlignment="1" applyBorder="1" applyFill="1"><alignment horizontal="center" vertical="center" wrapText="1"/></xf>
</cellXfs>
<cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles>
</styleSheet>"""
    sheet_overrides = "".join(
        f'<Override PartName="/xl/worksheets/sheet{idx}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>'
        for idx in range(1, len(sheets) + 1)
    )
    content_types = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
<Default Extension="xml" ContentType="application/xml"/>
<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>
""" + sheet_overrides + """
<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>
</Types>"""
    root_rels = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
</Relationships>"""
    sheet_rels = "".join(
        f'<Relationship Id="rId{idx}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet{idx}.xml"/>'
        for idx in range(1, len(sheets) + 1)
    )
    workbook_rels = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
""" + sheet_rels + f"""
<Relationship Id="rId{len(sheets) + 1}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
</Relationships>"""

    path.parent.mkdir(parents=True, exist_ok=True)
    with ZipFile(path, "w", ZIP_DEFLATED) as zf:
        zf.writestr("[Content_Types].xml", content_types)
        zf.writestr("_rels/.rels", root_rels)
        zf.writestr("xl/workbook.xml", workbook_xml)
        zf.writestr("xl/_rels/workbook.xml.rels", workbook_rels)
        zf.writestr("xl/styles.xml", styles_xml)
        for idx, (_name, rows) in enumerate(sheets, 1):
            zf.writestr(
                f"xl/worksheets/sheet{idx}.xml",
                _sheet_xml(rows, project_name=project_name, version=version, watermark=watermark),
            )


def _audit_is_number(value: object) -> bool:
    if isinstance(value, bool):
        return False
    return isinstance(value, (int, float))


def _xlsx_audit_cell(row: int, col: int, value: object, style: int = 0) -> str:
    if value is None:
        return ""
    ref = f"{_column_name(col)}{row}"
    style_attr = f' s="{style}"' if style else ""
    if _audit_is_number(value):
        number = f"{float(value):.12g}"
        return f'<c r="{ref}"{style_attr}><v>{number}</v></c>'
    return (
        f'<c r="{ref}" t="inlineStr"{style_attr}>'
        f"<is>{_xlsx_text_runs(value, size=16 if style == 2 else 10, bold=style in (1, 2), italic=style == 3)}</is>"
        "</c>"
    )


def _xlsx_range_ref(sheet_name: str, start_row: int, start_col: int, end_row: int, end_col: int) -> str:
    escaped_name = str(sheet_name).replace("'", "''")
    start = f"${_column_name(start_col)}${start_row}"
    end = f"${_column_name(end_col)}${end_row}"
    if start == end:
        return f"'{escaped_name}'!{start}"
    return f"'{escaped_name}'!{start}:{end}"


def _audit_chart_title_xml(text: str, font_size: int = 1400) -> str:
    return (
        "<c:tx><c:rich><a:bodyPr/><a:lstStyle/><a:p><a:r>"
        f'<a:rPr lang="zh-CN" sz="{font_size}" b="1">'
        '<a:latin typeface="Times New Roman"/><a:ea typeface="楷体"/><a:cs typeface="Times New Roman"/></a:rPr>'
        f"<a:t>{_xml_text(text)}</a:t>"
        "</a:r></a:p></c:rich></c:tx>"
    )


def _audit_scatter_chart_xml(title: str, series: list[dict[str, object]], x_axis_title: str, y_axis_title: str) -> str:
    x_axis_id = 48650112
    y_axis_id = 48672768
    series_xml: list[str] = []
    for index, item in enumerate(series):
        color = str(item.get("color") or "1F4E79").lstrip("#").upper()
        marker = str(item.get("marker") or "circle")
        smooth = "1" if item.get("smooth") else "0"
        name = _xml_text(item.get("name") or f"系列{index + 1}")
        x_ref = _xml_text(item["x_ref"])
        y_ref = _xml_text(item["y_ref"])
        labels_xml = '<c:dLbls><c:dLblPos val="r"/><c:showLegendKey val="0"/><c:showVal val="0"/><c:showCatName val="0"/><c:showSerName val="1"/></c:dLbls>' if item.get('show_label') else ''
        series_xml.append(
            f"""
<c:ser>
  <c:idx val="{index}"/><c:order val="{index}"/>
  <c:tx><c:v>{name}</c:v></c:tx>
  <c:spPr><a:ln w="25400"><a:solidFill><a:srgbClr val="{color}"/></a:solidFill></a:ln></c:spPr>
  <c:marker><c:symbol val="{marker}"/><c:size val="5"/><c:spPr><a:solidFill><a:srgbClr val="{color}"/></a:solidFill><a:ln><a:solidFill><a:srgbClr val="{color}"/></a:solidFill></a:ln></c:spPr></c:marker>
  {labels_xml}
  <c:xVal><c:numRef><c:f>{x_ref}</c:f></c:numRef></c:xVal>
  <c:yVal><c:numRef><c:f>{y_ref}</c:f></c:numRef></c:yVal>
  <c:smooth val="{smooth}"/>
</c:ser>"""
        )
    return f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"
 xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
 xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<c:chart>
<c:title>{_audit_chart_title_xml(title, 1500)}<c:overlay val="0"/></c:title>
<c:plotArea>
<c:layout/>
<c:scatterChart>
<c:scatterStyle val="lineMarker"/>
<c:varyColors val="0"/>
{''.join(series_xml)}
<c:axId val="{x_axis_id}"/><c:axId val="{y_axis_id}"/>
</c:scatterChart>
<c:valAx>
<c:axId val="{x_axis_id}"/><c:scaling><c:orientation val="minMax"/></c:scaling><c:delete val="0"/><c:axPos val="b"/>
<c:majorGridlines/><c:title>{_audit_chart_title_xml(x_axis_title, 1100)}<c:overlay val="0"/></c:title>
<c:numFmt formatCode="0.00" sourceLinked="0"/><c:majorTickMark val="out"/><c:minorTickMark val="none"/><c:tickLblPos val="nextTo"/>
<c:crossAx val="{y_axis_id}"/><c:crosses val="autoZero"/><c:crossBetween val="midCat"/>
</c:valAx>
<c:valAx>
<c:axId val="{y_axis_id}"/><c:scaling><c:orientation val="minMax"/></c:scaling><c:delete val="0"/><c:axPos val="l"/>
<c:majorGridlines/><c:title>{_audit_chart_title_xml(y_axis_title, 1100)}<c:overlay val="0"/></c:title>
<c:numFmt formatCode="0.00" sourceLinked="0"/><c:majorTickMark val="out"/><c:minorTickMark val="none"/><c:tickLblPos val="nextTo"/>
<c:crossAx val="{x_axis_id}"/><c:crosses val="autoZero"/><c:crossBetween val="midCat"/>
</c:valAx>
</c:plotArea>
<c:legend><c:legendPos val="b"/><c:overlay val="0"/></c:legend>
<c:plotVisOnly val="1"/>
</c:chart>
<c:txPr><a:bodyPr/><a:lstStyle/><a:p><a:pPr><a:defRPr>
<a:latin typeface="Times New Roman"/><a:ea typeface="楷体"/><a:cs typeface="Times New Roman"/>
</a:defRPr></a:pPr><a:endParaRPr lang="zh-CN"/></a:p></c:txPr>
<c:printSettings><c:headerFooter/><c:pageMargins b="0.75" l="0.7" r="0.7" t="0.75" header="0.3" footer="0.3"/><c:pageSetup/></c:printSettings>
</c:chartSpace>"""


def _audit_drawing_xml(charts: list[dict[str, object]]) -> str:
    anchors: list[str] = []
    for index, chart in enumerate(charts, 1):
        col1, row1, col2, row2 = chart.get("anchor", (5, 3, 14, 22))
        anchors.append(
            f"""
<xdr:twoCellAnchor>
  <xdr:from><xdr:col>{col1}</xdr:col><xdr:colOff>0</xdr:colOff><xdr:row>{row1}</xdr:row><xdr:rowOff>0</xdr:rowOff></xdr:from>
  <xdr:to><xdr:col>{col2}</xdr:col><xdr:colOff>0</xdr:colOff><xdr:row>{row2}</xdr:row><xdr:rowOff>0</xdr:rowOff></xdr:to>
  <xdr:graphicFrame macro="">
    <xdr:nvGraphicFramePr><xdr:cNvPr id="{index + 1}" name="Chart {index}"/><xdr:cNvGraphicFramePr/></xdr:nvGraphicFramePr>
    <xdr:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/></xdr:xfrm>
    <a:graphic><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart r:id="rId{index}"/></a:graphicData></a:graphic>
  </xdr:graphicFrame>
  <xdr:clientData/>
</xdr:twoCellAnchor>"""
        )
    return (
        '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
        '<xdr:wsDr xmlns:xdr="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" '
        'xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" '
        'xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" '
        'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">'
        + "".join(anchors)
        + "</xdr:wsDr>"
    )


def _audit_sheet_xml(rows: list[tuple[list[object], int]], has_drawing: bool = False) -> str:
    max_cols = max((len(cells) for cells, _style in rows), default=1)
    column_widths = [9] * max_cols
    row_xml: list[str] = []
    for row_idx, (cells, style) in enumerate(rows, 1):
        row_cells: list[str] = []
        for col_idx, value in enumerate(cells, 1):
            if value is None:
                continue
            text_value = f"{float(value):.3f}" if _audit_is_number(value) else str(value)
            length = max((len(part) for part in text_value.splitlines()), default=0)
            column_widths[col_idx - 1] = min(32, max(column_widths[col_idx - 1], length + 4))
            row_cells.append(_xlsx_audit_cell(row_idx, col_idx, value, style))
        height_attr = ' ht="24" customHeight="1"' if style in {1, 2, 3, 4, 5} else ""
        row_xml.append(f'<row r="{row_idx}"{height_attr}>{"".join(row_cells)}</row>')
    cols_xml = "".join(
        f'<col min="{idx}" max="{idx}" width="{width}" customWidth="1"/>'
        for idx, width in enumerate(column_widths, 1)
    )
    max_ref = f"{_column_name(max_cols)}{max(len(rows), 1)}"
    drawing_xml = '<drawing r:id="rId1"/>' if has_drawing else ""
    return f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
 xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<dimension ref="A1:{max_ref}"/>
<sheetViews><sheetView workbookViewId="0"/></sheetViews>
<cols>{cols_xml}</cols>
<sheetData>{''.join(row_xml)}</sheetData>
{drawing_xml}
<pageMargins left="0.35" right="0.35" top="0.6" bottom="0.6" header="0.3" footer="0.3"/>
</worksheet>"""


def _audit_styles_xml() -> str:
    return """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<numFmts count="2"><numFmt numFmtId="164" formatCode="0.000"/><numFmt numFmtId="165" formatCode="0.00%"/></numFmts>
<fonts count="4"><font><sz val="10"/><name val="Times New Roman"/></font><font><b/><sz val="10"/><name val="Times New Roman"/></font><font><b/><sz val="16"/><name val="Times New Roman"/></font><font><i/><sz val="10"/><name val="Times New Roman"/></font></fonts>
<fills count="4"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill><fill><patternFill patternType="solid"><fgColor rgb="FFD9EAD3"/><bgColor indexed="64"/></patternFill></fill><fill><patternFill patternType="solid"><fgColor rgb="FFEAF4E7"/><bgColor indexed="64"/></patternFill></fill></fills>
<borders count="2"><border><left/><right/><top/><bottom/><diagonal/></border><border><left style="thin"><color rgb="FF222222"/></left><right style="thin"><color rgb="FF222222"/></right><top style="thin"><color rgb="FF222222"/></top><bottom style="thin"><color rgb="FF222222"/></bottom><diagonal/></border></borders>
<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>
<cellXfs count="7">
<xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0" applyAlignment="1"><alignment vertical="center" wrapText="1"/></xf>
<xf numFmtId="0" fontId="1" fillId="2" borderId="1" xfId="0" applyAlignment="1" applyBorder="1" applyFill="1"><alignment horizontal="center" vertical="center" wrapText="1"/></xf>
<xf numFmtId="0" fontId="2" fillId="0" borderId="0" xfId="0" applyAlignment="1"><alignment horizontal="center" vertical="center" wrapText="1"/></xf>
<xf numFmtId="0" fontId="3" fillId="0" borderId="0" xfId="0" applyAlignment="1"><alignment vertical="center" wrapText="1"/></xf>
<xf numFmtId="164" fontId="0" fillId="3" borderId="1" xfId="0" applyAlignment="1" applyBorder="1" applyFill="1" applyNumberFormat="1"><alignment horizontal="center" vertical="center" wrapText="1"/></xf>
<xf numFmtId="0" fontId="0" fillId="3" borderId="1" xfId="0" applyAlignment="1" applyBorder="1" applyFill="1"><alignment horizontal="center" vertical="center" wrapText="1"/></xf>
<xf numFmtId="165" fontId="0" fillId="3" borderId="1" xfId="0" applyAlignment="1" applyBorder="1" applyFill="1" applyNumberFormat="1"><alignment horizontal="center" vertical="center" wrapText="1"/></xf>
</cellXfs>
<cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles>
</styleSheet>"""


def _audit_rows(title: str, note: str, table_title: str, headers: list[object], rows: list[list[object]]) -> list[tuple[list[object], int]]:
    result: list[tuple[list[object], int]] = [
        ([title], 2),
        ([note], 3),
        ([], 0),
        ([table_title for _ in headers], 1),
        (headers, 1),
    ]
    for row in rows:
        result.append((row, 4 if any(_audit_is_number(value) for value in row) else 5))
    return result


def _format_freq_label(freq: str) -> str:
    return f"{freq}年" if str(freq).isdigit() else str(freq)


def _target_frequency(freqs: list[str]) -> str:
    return "100" if "100" in freqs else freqs[0]


def _combined_rainfall_sheet(result: object, freqs: list[str], title_prefix: str) -> dict[str, object]:
    target_freq = _target_frequency(freqs)
    headers = ["时段(h)"]
    for freq in freqs:
        label = _format_freq_label(freq)
        headers.extend([f"{label}面雨量(mm)", f"{label}入渗量(mm)", f"{label}净雨(mm)"])
    rows: list[list[object]] = []
    for row in result.net_rain_table:
        values: list[object] = [row.hour]
        for freq in freqs:
            values.extend([
                row.rainfall_mm.get(freq, 0.0),
                row.infiltration_mm.get(freq, 0.0),
                row.net_rain_mm.get(freq, 0.0),
            ])
        rows.append(values)
    total_row: list[object] = ["合计"]
    for freq in freqs:
        total_row.extend([
            sum(row.rainfall_mm[freq] for row in result.net_rain_table),
            sum(row.infiltration_mm[freq] for row in result.net_rain_table),
            sum(row.net_rain_mm[freq] for row in result.net_rain_table),
        ])
    rows.append(total_row)
    sheet_name = "雨量净雨汇总"
    table_rows = _audit_rows(
        f"{title_prefix}：逐时面雨量、入渗与净雨汇总",
        "各频率按同一时段横向合并，便于复制进报告；右侧曲线默认显示100年一遇，其他频率可在表内筛选。",
        "逐时雨量净雨汇总表",
        headers,
        rows,
    )
    target_index = freqs.index(target_freq)
    target_col = 2 + target_index * 3
    target_first_row = 6
    target_last_row = target_first_row + len(result.net_rain_table) - 1
    charts = [
        {
            "title": f"{_format_freq_label(target_freq)}逐时面雨量、入渗与净雨（mm）",
            "x_axis": "时段 t(h)",
            "y_axis": "雨量(mm)",
            "anchor": (6, 3, 15, 22),
            "series": [
                {"name": "面雨量", "x_ref": _xlsx_range_ref(sheet_name, target_first_row, 1, target_last_row, 1), "y_ref": _xlsx_range_ref(sheet_name, target_first_row, target_col, target_last_row, target_col), "color": "1F4E79", "marker": "circle"},
                {"name": "入渗量", "x_ref": _xlsx_range_ref(sheet_name, target_first_row, 1, target_last_row, 1), "y_ref": _xlsx_range_ref(sheet_name, target_first_row, target_col + 1, target_last_row, target_col + 1), "color": "ED7D31", "marker": "square"},
                {"name": "净雨", "x_ref": _xlsx_range_ref(sheet_name, target_first_row, 1, target_last_row, 1), "y_ref": _xlsx_range_ref(sheet_name, target_first_row, target_col + 2, target_last_row, target_col + 2), "color": "548235", "marker": "triangle"},
            ],
        }
    ]
    return {"name": sheet_name, "rows": table_rows, "charts": charts}


def _combined_instant_unit_sheet(result: object, freqs: list[str], title_prefix: str) -> dict[str, object]:
    target_freq = _target_frequency(freqs)
    unit_times = sorted({row.time_h for freq in freqs for row in result.flood_results[freq].unit_line})
    unit_maps = {
        freq: {row.time_h: row.unit_depth_fraction for row in result.flood_results[freq].unit_line}
        for freq in freqs
    }
    rows = [[time_h, *[unit_maps[freq].get(time_h, 0.0) for freq in freqs]] for time_h in unit_times]
    sheet_name = "单位线汇总"
    table_rows = _audit_rows(
        f"{title_prefix}：时段单位线汇总",
        "各频率单位线按同一时段横向合并；右侧曲线默认显示100年一遇。",
        "时段单位线汇总表",
        ["t(h)", *[_format_freq_label(freq) for freq in freqs]],
        rows,
    )
    target_col = 2 + freqs.index(target_freq)
    target_first_row = 6
    target_last_row = target_first_row + len(rows) - 1
    charts = [
        {
            "title": f"{_format_freq_label(target_freq)}时段单位线",
            "x_axis": "时段 t(h)",
            "y_axis": "单位净雨比例",
            "anchor": (7, 3, 15, 20),
            "series": [
                {"name": "单位线", "x_ref": _xlsx_range_ref(sheet_name, target_first_row, 1, target_last_row, 1), "y_ref": _xlsx_range_ref(sheet_name, target_first_row, target_col, target_last_row, target_col), "color": "548235", "marker": "circle"}
            ],
        }
    ]
    return {"name": sheet_name, "rows": table_rows, "charts": charts}


def _combined_instant_hydrograph_sheet(result: object, freqs: list[str], title_prefix: str) -> dict[str, object]:
    target_freq = _target_frequency(freqs)
    hydro_times = sorted({point.time_h for freq in freqs for point in result.flood_results[freq].hydrograph})
    hydro_maps = {
        freq: {point.time_h: point.discharge_m3s for point in result.flood_results[freq].hydrograph}
        for freq in freqs
    }
    rows = [[time_h, *[hydro_maps[freq].get(time_h, 0.0) for freq in freqs]] for time_h in hydro_times]
    sheet_name = "洪水过程汇总"
    table_rows = _audit_rows(
        f"{title_prefix}：洪水过程线汇总",
        "各频率洪水过程按同一时段横向合并；右侧曲线默认显示100年一遇。",
        "洪水过程线汇总表",
        ["t(h)", *[_format_freq_label(freq) for freq in freqs]],
        rows,
    )
    target_col = 2 + freqs.index(target_freq)
    target_first_row = 6
    target_last_row = target_first_row + len(rows) - 1
    charts = [
        {
            "title": f"{_format_freq_label(target_freq)}洪水过程线（m³/s）",
            "x_axis": "时段 t(h)",
            "y_axis": "流量Q(m³/s)",
            "anchor": (5, 3, 14, 22),
            "series": [
                {"name": "瞬时单位线", "x_ref": _xlsx_range_ref(sheet_name, target_first_row, 1, target_last_row, 1), "y_ref": _xlsx_range_ref(sheet_name, target_first_row, target_col, target_last_row, target_col), "color": "1F4E79", "marker": "diamond"}
            ],
        }
    ]
    return {"name": sheet_name, "rows": table_rows, "charts": charts}


def _combined_rational_hydrograph_sheet(result: object, freqs: list[str], title_prefix: str) -> dict[str, object]:
    target_freq = _target_frequency(freqs)
    hydro_times = sorted({point.time_h for freq in freqs for point in result.frequency_results[freq].hydrograph})
    hydro_maps = {
        freq: {point.time_h: point.discharge_m3s for point in result.frequency_results[freq].hydrograph}
        for freq in freqs
    }
    rows = [[time_h, *[hydro_maps[freq].get(time_h, 0.0) for freq in freqs]] for time_h in hydro_times]
    sheet_name = "洪水过程汇总"
    table_rows = _audit_rows(
        f"{title_prefix}：推理公式洪水过程线汇总",
        "本表仅列推理公式法成果，并按同一时段横向合并；两法对比不在单方法计算书中默认输出。",
        "推理公式洪水过程线汇总表",
        ["t(h)", *[_format_freq_label(freq) for freq in freqs]],
        rows,
    )
    target_col = 2 + freqs.index(target_freq)
    target_first_row = 6
    target_last_row = target_first_row + len(rows) - 1
    charts = [
        {
            "title": f"{_format_freq_label(target_freq)}推理公式洪水过程线（m³/s）",
            "x_axis": "时段 t(h)",
            "y_axis": "流量Q(m³/s)",
            "anchor": (5, 3, 14, 22),
            "series": [
                {"name": "推理公式", "x_ref": _xlsx_range_ref(sheet_name, target_first_row, 1, target_last_row, 1), "y_ref": _xlsx_range_ref(sheet_name, target_first_row, target_col, target_last_row, target_col), "color": "ED7D31", "marker": "square"}
            ],
        }
    ]
    return {"name": sheet_name, "rows": table_rows, "charts": charts}


def _combined_rational_trial_sheet(result: object, freqs: list[str], title_prefix: str) -> dict[str, object]:
    rows: list[list[object]] = []
    for freq in freqs:
        for trial in result.frequency_results[freq].confluence_curve:
            rows.append([
                _format_freq_label(freq),
                trial[0],
                trial[1],
                trial[2],
                trial[3],
                trial[4],
                trial[4] - trial[0],
                trial[5] if len(trial) > 5 else "",
            ])
    table_rows = _audit_rows(
        f"{title_prefix}：图解法求Qm与τ试算汇总",
        "试算表保留关键试算点；汇流曲线图采用加密采样点绘制，不再用两行试算点直接连线。",
        "图解法试算汇总表",
        ["重现期", "假设t(h)", "累计hₜ(mm)", "hₜ/t(mm/h)", "Qₜ(m³/s)", "τₜ(h)", "τₜ-t", "起点(h)"],
        rows,
    )
    return {"name": "图解试算汇总", "rows": table_rows, "charts": []}


def _combined_rational_confluence_sheet(result: object, freqs: list[str], title_prefix: str) -> dict[str, object]:
    target_freq = _target_frequency(freqs)
    rows: list[list[object]] = []
    target_first_row = 6
    target_last_row = 6
    for freq in freqs:
        points = result.frequency_results[freq].confluence_curve_points
        if freq == target_freq:
            target_first_row = 6 + len(rows)
        for point in points:
            duration_h, q_rain, tau_h = point[:3]
            q_travel = point[3] if len(point) >= 4 else q_rain
            rows.append([_format_freq_label(freq), duration_h, q_rain, tau_h, q_travel])
        if freq == target_freq:
            target_last_row = target_first_row + len(points) - 1
    sheet_name = "汇流曲线"
    table_rows = _audit_rows(
        f"{title_prefix}：推理公式汇流曲线",
        "曲线采用公式加密采样点：绿色为假设历时t对应的Q(t)，红色为汇流时间τ对应的Q(τ)，两曲线交点即Qm与τ。",
        "推理公式汇流曲线采样表",
        ["重现期", "假设t(h)", "Q(t)(m³/s)", "τ(h)", "Q(τ)(m³/s)"],
        rows,
    )
    charts = [
        {
            "title": f"{_format_freq_label(target_freq)}推理公式汇流曲线（m³/s）",
            "x_axis": "τ或t(h)",
            "y_axis": "流量Q(m³/s)",
            "anchor": (5, 3, 14, 22),
            "series": [
                {"name": "按净雨强计算Q(t)", "x_ref": _xlsx_range_ref(sheet_name, target_first_row, 2, target_last_row, 2), "y_ref": _xlsx_range_ref(sheet_name, target_first_row, 3, target_last_row, 3), "color": "1C7C54", "marker": "circle"},
                {"name": "按汇流时间计算Q(τ)", "x_ref": _xlsx_range_ref(sheet_name, target_first_row, 4, target_last_row, 4), "y_ref": _xlsx_range_ref(sheet_name, target_first_row, 5, target_last_row, 5), "color": "B33F1D", "marker": "square"},
            ],
        }
    ]
    return {"name": sheet_name, "rows": table_rows, "charts": charts}


def _rainfall_sheet(result: object, freq: str, title_prefix: str) -> dict[str, object]:
    rows = [
        [
            row.hour,
            row.rainfall_mm.get(freq, 0.0),
            row.infiltration_mm.get(freq, 0.0),
            row.net_rain_mm.get(freq, 0.0),
        ]
        for row in result.net_rain_table
    ]
    rows.append([
        "合计",
        sum(float(row[1]) for row in rows),
        sum(float(row[2]) for row in rows),
        sum(float(row[3]) for row in rows),
    ])
    sheet_name = f"{freq}年-雨量净雨"
    table_rows = _audit_rows(
        f"{title_prefix}：{_format_freq_label(freq)}逐时面雨量、入渗与净雨",
        "面雨量为点雨量经点面折减和流域形状修正后的逐时分配；入渗量按产流期平均入渗率f折算；净雨为参与产流汇流的有效雨量。",
        "逐时雨量净雨计算表",
        ["时段(h)", "面雨量(mm)", "入渗量(mm)", "净雨(mm)"],
        rows,
    )
    first_data_row = 6
    last_data_row = first_data_row + len(rows) - 2
    charts = [
        {
            "title": f"{_format_freq_label(freq)}逐时面雨量、入渗与净雨（mm）",
            "x_axis": "时段 t(h)",
            "y_axis": "雨量(mm)",
            "anchor": (5, 3, 14, 22),
            "series": [
                {"name": "面雨量", "x_ref": _xlsx_range_ref(sheet_name, first_data_row, 1, last_data_row, 1), "y_ref": _xlsx_range_ref(sheet_name, first_data_row, 2, last_data_row, 2), "color": "1F4E79", "marker": "circle"},
                {"name": "入渗量", "x_ref": _xlsx_range_ref(sheet_name, first_data_row, 1, last_data_row, 1), "y_ref": _xlsx_range_ref(sheet_name, first_data_row, 3, last_data_row, 3), "color": "ED7D31", "marker": "square"},
                {"name": "净雨", "x_ref": _xlsx_range_ref(sheet_name, first_data_row, 1, last_data_row, 1), "y_ref": _xlsx_range_ref(sheet_name, first_data_row, 4, last_data_row, 4), "color": "548235", "marker": "triangle"},
            ],
        }
    ]
    return {"name": sheet_name, "rows": table_rows, "charts": charts}


def _instant_unit_sheet(result: object, freq: str, title_prefix: str) -> dict[str, object]:
    flood = result.flood_results[freq]
    rows = [[row.time_h, row.t_over_k, row.s_t, row.s_prev, row.unit_depth_fraction] for row in flood.unit_line]
    sheet_name = f"{freq}年-单位线"
    table_rows = _audit_rows(
        f"{title_prefix}：{_format_freq_label(freq)}时段单位线",
        "S(t)为Nash瞬时单位线积分；u(Δt,t)=S(t)-S(t-Δt)，各时段u合计应接近1。",
        "单位线计算表",
        ["t(h)", "t/k", "S(t)", "S(t-Δt)", "u(Δt,t)"],
        rows,
    )
    first_data_row = 6
    last_data_row = first_data_row + len(rows) - 1
    charts = [
        {
            "title": f"{_format_freq_label(freq)}时段单位线",
            "x_axis": "时段 t(h)",
            "y_axis": "单位净雨比例",
            "anchor": (6, 3, 14, 20),
            "series": [
                {"name": "单位线", "x_ref": _xlsx_range_ref(sheet_name, first_data_row, 1, last_data_row, 1), "y_ref": _xlsx_range_ref(sheet_name, first_data_row, 5, last_data_row, 5), "color": "548235", "marker": "circle"}
            ],
        }
    ]
    return {"name": sheet_name, "rows": table_rows, "charts": charts}


def _instant_hydrograph_sheet(result: object, freq: str, title_prefix: str) -> dict[str, object]:
    flood = result.flood_results[freq]
    rows = [[point.time_h, point.discharge_m3s] for point in flood.hydrograph]
    sheet_name = f"{freq}年-洪水过程"
    table_rows = _audit_rows(
        f"{title_prefix}：{_format_freq_label(freq)}洪水过程线",
        "洪水过程由逐时净雨与时段单位线卷积得到；本表保留完整过程线，图表采用真实时间坐标。",
        "瞬时单位线洪水过程",
        ["t(h)", "Q(m³/s)"],
        rows,
    )
    first_data_row = 6
    last_data_row = first_data_row + len(rows) - 1
    charts = [
        {
            "title": f"{_format_freq_label(freq)}洪水过程线（m³/s）",
            "x_axis": "时段 t(h)",
            "y_axis": "流量Q(m³/s)",
            "anchor": (4, 3, 14, 22),
            "series": [
                {"name": "瞬时单位线", "x_ref": _xlsx_range_ref(sheet_name, first_data_row, 1, last_data_row, 1), "y_ref": _xlsx_range_ref(sheet_name, first_data_row, 2, last_data_row, 2), "color": "1F4E79", "marker": "diamond"}
            ],
        }
    ]
    return {"name": sheet_name, "rows": table_rows, "charts": charts}


def _rational_hydrograph_sheet(result: object, freq: str, title_prefix: str) -> dict[str, object]:
    rational_flood = result.frequency_results[freq]
    rows = [[point.time_h, point.discharge_m3s] for point in rational_flood.hydrograph]
    sheet_name = f"{freq}年-洪水过程"
    table_rows = _audit_rows(
        f"{title_prefix}：{_format_freq_label(freq)}推理公式洪水过程线",
        "本表只列推理公式法自身成果；洪水过程线按《甘肃省暴雨洪水图集》推理公式法和五点概化过程线组织，图表采用真实时间坐标。",
        "推理公式洪水过程线",
        ["t(h)", "推理公式Q(m³/s)"],
        rows,
    )
    table_rows.extend([([], 0), (["推理公式图解求Qm与τ试算表"] * 6, 1), (["假设t(h)", "累计hₜ(mm)", "hₜ/t(mm/h)", "Qₜ(m³/s)", "τₜ(h)", "起点(h)"], 1)])
    for trial in rational_flood.confluence_curve:
        table_rows.append(([trial[0], trial[1], trial[2], trial[3], trial[4], trial[5] if len(trial) > 5 else ""], 4))
    first_data_row = 6
    last_data_row = first_data_row + len(rows) - 1
    charts = [
        {
            "title": f"{_format_freq_label(freq)}推理公式洪水过程线（m³/s）",
            "x_axis": "时段 t(h)",
            "y_axis": "流量Q(m³/s)",
            "anchor": (4, 3, 14, 22),
            "series": [
                {"name": "推理公式", "x_ref": _xlsx_range_ref(sheet_name, first_data_row, 1, last_data_row, 1), "y_ref": _xlsx_range_ref(sheet_name, first_data_row, 2, last_data_row, 2), "color": "ED7D31", "marker": "square"},
            ],
        }
    ]
    return {"name": sheet_name, "rows": table_rows, "charts": charts}


def _instant_summary_sheets(result: object, title: str, freqs: list[str]) -> list[dict[str, object]]:
    overview_rows: list[tuple[list[object], int]] = [
        ([title], 2),
        (["专项核验式输出：雨量净雨、单位线、洪水过程按各频率拆分为独立工作表，并在对应工作表内嵌曲线图。"], 3),
        ([], 0),
        (["项目", "数值", "说明"], 1),
        (["计算方法", "《甘肃省暴雨洪水图集》瞬时单位线法", "水文图集法"], 5),
        (["流域面积F(km²)", result.area_km2 or "", "输入"], 5),
        (["计算步长Δt(h)", result.delta_t_h or 1, "图集附表10为1小时步长"], 5),
        (["雨型分区", result.rainfall_zone_name or "", "输入选择"], 5),
        (["主雨峰", f"{result.main_peak_hours}h" if result.main_peak_hours else "", "由流域面积自动选择"], 5),
        (["雨型来源", result.distribution_source or "", "自动读取"], 5),
    ]
    summary_rows = [[_format_freq_label(freq), result.peak_summary[freq][0], result.peak_summary[freq][1], result.peak_summary[freq][2]] for freq in freqs]
    flow_rows = _audit_rows(
        "各频率成果与流量校核",
        "频率增大时洪峰应单调增大；洪量由洪水过程线梯形积分复核。",
        "瞬时单位线各频率成果",
        ["重现期", "Q峰(m³/s)", "峰现(h)", "洪量(万m³)"],
        summary_rows,
    )
    return [{"name": "成果总览", "rows": overview_rows, "charts": []}, {"name": "流量校核", "rows": flow_rows, "charts": []}]


def _rational_summary_sheets(result: object, title: str, freqs: list[str]) -> list[dict[str, object]]:
    overview_rows: list[tuple[list[object], int]] = [
        ([title], 2),
        (["专项核验式输出：本工作簿只列推理公式法自身成果；雨量净雨、洪水过程和图解试算按各频率拆分，瞬时单位线对比另做“两法专项校核”。"], 3),
        ([], 0),
        (["项目", "数值", "说明"], 1),
        (["计算方法", "《甘肃省暴雨洪水图集》推理公式法", "图集表10、推理公式及五点概化过程线"], 5),
        (["流域面积F(km²)", result.shared.area_km2 or "", "输入"], 5),
        (["主河道长度L(km)", result.river_length_km, "输入"], 5),
        (["主河槽比降J(‰)", result.river_slope_permille, "输入"], 5),
        (["雨型分区", result.shared.rainfall_zone_name or "", "前段暴雨与产流共用瞬时单位线法"], 5),
        (["推理公式分区", result.rational_region_name, "表10 m关系"], 5),
        (["Ka", result.shape_ka, result.shape_source], 5),
        (["Kw", result.shape_kw, result.shape_source], 5),
        (["KT", result.shape_kt, result.shape_source], 5),
        (["基流Q0(m³/s)", result.baseflow_m3s, result.baseflow_source], 5),
    ]
    summary_rows = []
    for freq in freqs:
        rational = result.frequency_results[freq]
        summary_rows.append([
            _format_freq_label(freq),
            rational.surface_peak_m3s,
            rational.peak_flow_m3s,
            rational.peak_time_h,
            rational.tau_h,
            rational.flood_volume_10k_m3,
            rational.runoff_condition,
        ])
    flow_rows = _audit_rows(
        "各频率成果与流量校核",
        "本表只列推理公式法成果；Qp为Qm叠加基流后的设计洪峰。",
        "推理公式各频率成果",
        ["重现期", "Qm地表(m³/s)", "Qp设计(m³/s)", "峰现(h)", "τ(h)", "洪量(万m³)", "说明"],
        summary_rows,
    )
    return [{"name": "成果总览", "rows": overview_rows, "charts": []}, {"name": "流量校核", "rows": flow_rows, "charts": []}]


def _write_audit_workbook(path: Path, sheet_specs: list[dict[str, object]]) -> None:
    used_names: set[str] = set()
    sheets: list[dict[str, object]] = []
    rename_map: dict[str, str] = {}
    for spec in sheet_specs:
        original_name = str(spec["name"])
        name = _safe_sheet_name(original_name, used_names)
        rename_map[original_name] = name
        updated = dict(spec)
        updated["name"] = name
        sheets.append(updated)
    for sheet in sheets:
        for chart in sheet.get("charts") or []:
            for series in chart.get("series") or []:
                for key in ("x_ref", "y_ref"):
                    value = str(series[key])
                    for original, renamed in rename_map.items():
                        if original != renamed:
                            value = value.replace(f"'{original}'!", f"'{renamed}'!")
                    series[key] = value
    sheet_items = "".join(f'<sheet name="{_xml_text(sheet["name"])}" sheetId="{idx}" r:id="rId{idx}"/>' for idx, sheet in enumerate(sheets, 1))
    workbook_xml = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
 xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<sheets>{sheet_items}</sheets>
</workbook>"""
    worksheet_overrides = "".join(f'<Override PartName="/xl/worksheets/sheet{idx}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>' for idx in range(1, len(sheets) + 1))
    workbook_relationships = [f'<Relationship Id="rId{idx}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet{idx}.xml"/>' for idx in range(1, len(sheets) + 1)]
    workbook_relationships.append(f'<Relationship Id="rId{len(sheets) + 1}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>')
    root_rels = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
</Relationships>"""
    workbook_rels = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' + "".join(workbook_relationships) + "</Relationships>"
    path.parent.mkdir(parents=True, exist_ok=True)
    with ZipFile(path, "w", ZIP_DEFLATED) as zf:
        zf.writestr("_rels/.rels", root_rels)
        zf.writestr("xl/workbook.xml", workbook_xml)
        zf.writestr("xl/_rels/workbook.xml.rels", workbook_rels)
        zf.writestr("xl/styles.xml", _audit_styles_xml())
        drawing_overrides: list[str] = []
        chart_overrides: list[str] = []
        deferred_parts: list[tuple[str, str]] = []
        drawing_index = 0
        chart_index = 0
        for sheet_index, sheet in enumerate(sheets, 1):
            charts = list(sheet.get("charts") or [])
            zf.writestr(f"xl/worksheets/sheet{sheet_index}.xml", _audit_sheet_xml(sheet["rows"], has_drawing=bool(charts)))  # type: ignore[arg-type]
            if not charts:
                continue
            drawing_index += 1
            drawing_name = f"drawing{drawing_index}.xml"
            drawing_overrides.append(f'<Override PartName="/xl/drawings/{drawing_name}" ContentType="application/vnd.openxmlformats-officedocument.drawing+xml"/>')
            zf.writestr(f"xl/worksheets/_rels/sheet{sheet_index}.xml.rels", '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' + f'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing" Target="../drawings/{drawing_name}"/>' + "</Relationships>")
            zf.writestr(f"xl/drawings/{drawing_name}", _audit_drawing_xml(charts))
            drawing_rels: list[str] = []
            for rel_index, chart in enumerate(charts, 1):
                chart_index += 1
                chart_name = f"chart{chart_index}.xml"
                chart_overrides.append(f'<Override PartName="/xl/charts/{chart_name}" ContentType="application/vnd.openxmlformats-officedocument.drawingml.chart+xml"/>')
                drawing_rels.append(f'<Relationship Id="rId{rel_index}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart" Target="../charts/{chart_name}"/>')
                deferred_parts.append((f"xl/charts/{chart_name}", _audit_scatter_chart_xml(str(chart["title"]), chart["series"], str(chart.get("x_axis") or "时段 t(h)"), str(chart.get("y_axis") or "数值"))))  # type: ignore[arg-type]
            zf.writestr(f"xl/drawings/_rels/{drawing_name}.rels", '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' + "".join(drawing_rels) + "</Relationships>")
        for name, xml in deferred_parts:
            zf.writestr(name, xml)
        content_types = (
            '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">'
            '<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>'
            '<Default Extension="xml" ContentType="application/xml"/>'
            '<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>'
            + worksheet_overrides
            + "".join(drawing_overrides)
            + "".join(chart_overrides)
            + '<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>'
            + "</Types>"
        )
        zf.writestr("[Content_Types].xml", content_types)


def export_rainfall_runoff_audit_xlsx(result: object, path: str | Path, title: str = "雨洪计算专项核验") -> None:
    """Export instant-unit-line or rational-formula results in the audit workbook layout."""

    path = Path(path)
    if hasattr(result, "frequency_results") and hasattr(result, "shared"):
        freqs = [freq for freq in result.frequency_results if freq in result.shared.flood_results]
        sheet_specs = _rational_summary_sheets(result, title, freqs)
        for freq in freqs:
            sheet_specs.append(_rainfall_sheet(result.shared, freq, title))
            sheet_specs.append(_rational_hydrograph_sheet(result, freq, title))
    elif hasattr(result, "flood_results"):
        freqs = [freq for freq in result.flood_results]
        sheet_specs = _instant_summary_sheets(result, title, freqs)
        for freq in freqs:
            sheet_specs.append(_rainfall_sheet(result, freq, title))
            sheet_specs.append(_instant_unit_sheet(result, freq, title))
            sheet_specs.append(_instant_hydrograph_sheet(result, freq, title))
    else:
        raise ValueError("不支持的雨洪专项核验导出结果类型")
    _write_audit_workbook(path, sheet_specs)


def _strip_html_document(fragment: str) -> str:
    text = str(fragment or "")
    match = re.search(r"<body[^>]*>(.*)</body>", text, flags=re.IGNORECASE | re.DOTALL)
    if match:
        return match.group(1)
    return text


def export_calculation_report_html(
    content: str,
    path: str | Path,
    title: str = "水文计算专项报告",
    figures: list[tuple[str, str]] | None = None,
) -> None:
    path = Path(path)
    blocks = _parse_blocks(content)
    figures = list(figures or [])
    table_count = sum(1 for block_type, _payload in blocks if block_type == "table")
    body_parts = [
        "<!doctype html><html lang=\"zh-CN\"><head><meta charset=\"utf-8\">",
        f"<title>{html_lib.escape(title)}</title>",
        """
<style>
body{margin:0;background:#eef3f7;color:#172b3a;font-family:"Times New Roman","KaiTi","楷体",serif;}
main{max-width:1180px;margin:0 auto;padding:28px 28px 52px;}
.hero{background:#fff;border:1px solid #d6e1e8;padding:22px 26px;margin-bottom:18px;}
h1{font-size:26px;text-align:center;margin:0 0 12px;}
h2{font-size:18px;margin:22px 0 10px;color:#12324a;}
p{line-height:1.7;margin:6px 0;}
.meta{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;margin-top:14px;}
.meta div{background:#f5fbf7;border:1px solid #d4e5d0;padding:10px 12px;text-align:center;}
.section{background:#fff;border:1px solid #d6e1e8;margin:14px 0;padding:16px 18px;}
table{border-collapse:collapse;table-layout:fixed;margin:10px auto 16px;width:100%;max-width:1080px;background:#eef7e9;}
th,td{border:1px solid #222;padding:6px 8px;text-align:center;vertical-align:middle;word-break:break-word;}
th{background:#d9ead3;font-weight:700;}
.paragraph{white-space:pre-wrap;}
.figure{background:#fff;border:1px solid #d6e1e8;margin:18px 0;padding:14px 16px;}
.figure h2{text-align:center;margin-top:0;}
svg{display:block;max-width:100%;height:auto;margin:0 auto;}
.chart{background:white;border:1px solid #d6e1e8;margin:12px 0;padding:12px;}
.axis{stroke:#263847;stroke-width:1.5}.grid{stroke:#dce4ea;stroke-width:1}.title{font-size:19px;font-weight:700}.tick{font-size:12px;fill:#4e6070}.label{font-size:14px;fill:#263847}.legend{font-size:12px;font-weight:700}
@media print{body{background:#fff}main{padding:0}.hero,.section,.figure{break-inside:avoid}}
</style></head><body><main>
""",
        "<div class=\"hero\">",
        f"<h1>{html_lib.escape(title)}</h1>",
        "<div class=\"meta\">",
        f"<div>生成时间<br>{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</div>",
        f"<div>表格数量<br>{table_count}</div>",
        f"<div>图件数量<br>{len(figures)}</div>",
        "</div></div>",
        "<div class=\"section\"><h2>计算结果</h2>",
    ]
    for block_type, payload in blocks:
        if block_type == "paragraph":
            text = str(payload)
            if not text.strip():
                body_parts.append("<p></p>")
            elif _is_heading_line(text):
                body_parts.append(f"<h2>{html_lib.escape(text)}</h2>")
            else:
                body_parts.append(f"<p class=\"paragraph\">{html_lib.escape(text)}</p>")
        else:
            rows = [[str(cell) for cell in row] for row in payload]  # type: ignore[assignment]
            body_parts.append("<table>")
            for row_index, row in enumerate(rows):
                tag = "th" if row_index == 0 else "td"
                cells = "".join(f"<{tag}>{html_lib.escape(cell)}</{tag}>" for cell in row)
                body_parts.append(f"<tr>{cells}</tr>")
            body_parts.append("</table>")
    body_parts.append("</div>")
    if figures:
        body_parts.append("<div class=\"section\"><h2>曲线图、断面示意图</h2>")
        for caption, figure_html in figures:
            body_parts.append("<div class=\"figure\">")
            body_parts.append(f"<h2>{html_lib.escape(caption)}</h2>")
            body_parts.append(_strip_html_document(figure_html))
            body_parts.append("</div>")
        body_parts.append("</div>")
    body_parts.append("</main></body></html>")
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text("".join(body_parts), encoding="utf-8")


def _docx_paragraph(text: str, center: bool = False, bold: bool = False) -> str:
    jc = '<w:jc w:val="center"/>' if center else ""
    ppr = f"<w:pPr>{jc}</w:pPr>" if jc else ""
    rpr = '<w:rPr><w:rFonts w:ascii="Times New Roman" w:hAnsi="Times New Roman" w:eastAsia="楷体" w:cs="Times New Roman"/>'
    rpr += ("<w:b/>" if bold else "") + "</w:rPr>"
    return f"<w:p>{ppr}<w:r>{rpr}<w:t xml:space=\"preserve\">{_xml_text(text)}</w:t></w:r></w:p>"


def _docx_figure_drawing(rid: str, caption: str, index: int, width_emu: int = 5486400, height_emu: int = 2743200) -> str:
    name = _xml_text(caption or f"figure {index}")
    return f"""
<w:p><w:pPr><w:jc w:val="center"/></w:pPr><w:r><w:drawing>
<wp:inline distT="0" distB="0" distL="0" distR="0">
<wp:extent cx="{width_emu}" cy="{height_emu}"/>
<wp:docPr id="{index}" name="{name}"/>
<wp:cNvGraphicFramePr><a:graphicFrameLocks noChangeAspect="1"/></wp:cNvGraphicFramePr>
<a:graphic><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">
<pic:pic><pic:nvPicPr><pic:cNvPr id="{index}" name="{name}"/><pic:cNvPicPr/></pic:nvPicPr>
<pic:blipFill><a:blip r:embed="{rid}"/><a:stretch><a:fillRect/></a:stretch></pic:blipFill>
<pic:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="{width_emu}" cy="{height_emu}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></pic:spPr>
</pic:pic></a:graphicData></a:graphic>
</wp:inline></w:drawing></w:r></w:p>
"""


def _split_wide_table(rows: list[list[str]], max_cols: int = _WORD_MAX_TABLE_COLS) -> list[list[list[str]]]:
    if not rows:
        return [rows]
    max_row_cols = max((len(row) for row in rows), default=1)
    if max_row_cols <= max_cols:
        return [rows]
    fixed_cols = 1 if max_row_cols > 1 else 0
    data_cols = max(1, max_cols - fixed_cols)
    chunks: list[list[list[str]]] = []
    for start in range(fixed_cols, max_row_cols, data_cols):
        col_indexes = list(range(fixed_cols)) + list(range(start, min(start + data_cols, max_row_cols)))
        chunk_rows: list[list[str]] = []
        for row in rows:
            chunk_rows.append([row[index] if index < len(row) else "" for index in col_indexes])
        chunks.append(chunk_rows)
    return chunks


def _docx_table(rows: list[list[str]]) -> str:
    max_cols = max((len(row) for row in rows), default=1)
    col_width = max(900, _WORD_USABLE_WIDTH_DXA // max(max_cols, 1))
    table_width = col_width * max(max_cols, 1)
    grid = "".join(f'<w:gridCol w:w="{col_width}"/>' for _ in range(max_cols))
    row_xml: list[str] = []
    for row in rows:
        normalized = row + [""] * (max_cols - len(row))
        cells = []
        for cell in normalized:
            cells.append(
                f"<w:tc><w:tcPr><w:tcW w:w=\"{col_width}\" w:type=\"dxa\"/>"
                "<w:vAlign w:val=\"center\"/></w:tcPr>"
                f"{_docx_paragraph(cell, center=True)}</w:tc>"
            )
        row_xml.append(f"<w:tr>{''.join(cells)}</w:tr>")
    return (
        f"<w:tbl><w:tblPr><w:tblW w:w=\"{table_width}\" w:type=\"dxa\"/><w:tblLayout w:type=\"fixed\"/>"
        "<w:tblBorders><w:top w:val=\"single\" w:sz=\"4\" w:space=\"0\" w:color=\"000000\"/>"
        "<w:left w:val=\"single\" w:sz=\"4\" w:space=\"0\" w:color=\"000000\"/>"
        "<w:bottom w:val=\"single\" w:sz=\"4\" w:space=\"0\" w:color=\"000000\"/>"
        "<w:right w:val=\"single\" w:sz=\"4\" w:space=\"0\" w:color=\"000000\"/>"
        "<w:insideH w:val=\"single\" w:sz=\"4\" w:space=\"0\" w:color=\"000000\"/>"
        "<w:insideV w:val=\"single\" w:sz=\"4\" w:space=\"0\" w:color=\"000000\"/>"
        "</w:tblBorders><w:jc w:val=\"center\"/><w:tblCellMar>"
        "<w:top w:w=\"80\" w:type=\"dxa\"/><w:left w:w=\"80\" w:type=\"dxa\"/>"
        "<w:bottom w:w=\"80\" w:type=\"dxa\"/><w:right w:w=\"80\" w:type=\"dxa\"/>"
        f"</w:tblCellMar></w:tblPr><w:tblGrid>{grid}</w:tblGrid>{''.join(row_xml)}</w:tbl>"
    )


def export_calculation_book_docx(
    content: str,
    path: str | Path,
    title: str = "计算书",
    *,
    project_name: str = "",
    version: str = "",
    watermark: str = "",
    figures: list[tuple[str, str | Path]] | None = None,
) -> None:
    path = Path(path)
    body_parts = [_docx_paragraph(title, center=True, bold=True)]
    for block_type, payload in _parse_blocks(content):
        if block_type == "paragraph":
            body_parts.append(_docx_paragraph(str(payload)))
        else:
            table_chunks = _split_wide_table(payload)  # type: ignore[arg-type]
            for index, chunk in enumerate(table_chunks, 1):
                if len(table_chunks) > 1:
                    body_parts.append(_docx_paragraph(f"续表 {index}/{len(table_chunks)}", center=True, bold=True))
                body_parts.append(_docx_table(chunk))
                body_parts.append(_docx_paragraph(""))
    figure_media: list[tuple[str, str, bytes]] = []
    for figure_index, (caption, source) in enumerate(figures or [], 1):
        raw_source = str(source)
        source_path = Path(raw_source) if not raw_source.lstrip().startswith('<svg') else None
        if source_path is not None and source_path.exists():
            figure_data = source_path.read_bytes()
            extension = source_path.suffix.lower().lstrip(".") or "svg"
        else:
            figure_data = raw_source.encode("utf-8")
            extension = "svg"
        media_name = f"media/figure{figure_index}.{extension}"
        rid = f"rId{figure_index + 3}"
        figure_media.append((media_name, rid, figure_data))
        body_parts.append(_docx_paragraph(f"图 {figure_index}  {caption}", center=True, bold=True))
        body_parts.append(_docx_figure_drawing(rid, caption, figure_index))
    header_text = _export_header_text(project_name, version)
    watermark_text = _export_watermark_text(watermark)
    document_xml = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
 xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
 xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
 xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
 xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture">
<w:body>{''.join(body_parts)}
<w:sectPr><w:headerReference w:type="default" r:id="rId2"/><w:footerReference w:type="default" r:id="rId3"/><w:pgSz w:w="12240" w:h="15840"/><w:pgMar w:top="1440" w:right="720" w:bottom="1440" w:left="720" w:header="720" w:footer="720" w:gutter="0"/></w:sectPr>
</w:body></w:document>"""
    header_xml = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:hdr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
{_docx_paragraph(header_text, center=True)}
{_docx_paragraph(watermark_text, center=True, bold=True)}
</w:hdr>"""
    footer_xml = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:ftr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:p><w:pPr><w:jc w:val="center"/></w:pPr><w:r><w:t>第 </w:t></w:r><w:fldSimple w:instr="PAGE"><w:r><w:t>1</w:t></w:r></w:fldSimple><w:r><w:t> 页 / 共 </w:t></w:r><w:fldSimple w:instr="NUMPAGES"><w:r><w:t>1</w:t></w:r></w:fldSimple><w:r><w:t> 页</w:t></w:r></w:p>
</w:ftr>"""
    content_types = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
<Default Extension="xml" ContentType="application/xml"/>
<Default Extension="svg" ContentType="image/svg+xml"/>
<Default Extension="png" ContentType="image/png"/>
<Default Extension="jpg" ContentType="image/jpeg"/>
<Default Extension="jpeg" ContentType="image/jpeg"/>
<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
<Override PartName="/word/header1.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml"/>
<Override PartName="/word/footer1.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml"/>
<Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/>
</Types>"""
    root_rels = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
</Relationships>"""
    image_rels = "".join(
        f'<Relationship Id="{rid}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="{media_name}"/>'
        for media_name, rid, _data in figure_media
    )
    document_rels = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/header" Target="header1.xml"/>
<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer" Target="footer1.xml"/>
<Relationship Id="rIdStyles" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
{image_rels}</Relationships>"""
    path.parent.mkdir(parents=True, exist_ok=True)
    with ZipFile(path, "w", ZIP_DEFLATED) as zf:
        zf.writestr("[Content_Types].xml", content_types)
        zf.writestr("_rels/.rels", root_rels)
        zf.writestr("word/document.xml", document_xml)
        zf.writestr("word/_rels/document.xml.rels", document_rels)
        zf.writestr("word/header1.xml", header_xml)
        zf.writestr("word/footer1.xml", footer_xml)
        zf.writestr("word/styles.xml", '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:docDefaults><w:rPrDefault><w:rPr><w:rFonts w:ascii="Times New Roman" w:hAnsi="Times New Roman" w:eastAsia="楷体" w:cs="Times New Roman"/></w:rPr></w:rPrDefault></w:docDefaults>
<w:style w:type="paragraph" w:default="1" w:styleId="Normal"><w:name w:val="Normal"/></w:style>
</w:styles>''')
        for media_name, _rid, figure_data in figure_media:
            zf.writestr(f"word/{media_name}", figure_data)
