"""Normalize OOXML in generated .docx bytes for safer layout in browsers (docx-preview) and print."""

from __future__ import annotations

import io
import re
import zipfile
import xml.etree.ElementTree as ET
from copy import deepcopy

# Self-closing trHeight, e.g. <w:trHeight w:val="123" w:hRule="atLeast"/>
_TR_HEIGHT_RE = re.compile(r"<w:trHeight\b[^>]*/>", re.IGNORECASE)

# Optional paired form (unusual for trHeight but defensive)
_TR_HEIGHT_BLOCK_RE = re.compile(
    r"<w:trHeight\b[^>]*>.*?</w:trHeight>",
    re.IGNORECASE | re.DOTALL,
)

_PARAGRAPH_RE = re.compile(r"<w:p\b[^>]*>.*?</w:p>", re.IGNORECASE | re.DOTALL)
_RUN_RE = re.compile(r"<w:r\b[^>]*>.*?</w:r>", re.IGNORECASE | re.DOTALL)
_RUN_SZ_RE = re.compile(r'(<w:sz\b[^>]*\bw:val=")(\d+)(")', re.IGNORECASE)
_RUN_SZCS_RE = re.compile(r'(<w:szCs\b[^>]*\bw:val=")(\d+)(")', re.IGNORECASE)

_SHRINK_TARGET_PHRASES = (
    "ĐẠI HỌC Y DƯỢC THÀNH PHỐ HỒ CHÍ MINH",
    "ĐẠI HỌC Y DƯỢCTHÀNH PHỐ HỒ CHÍ MINH",
    "CỘNG HÒA XÃ HỘI CHỦ NGHĨA VIỆT NAM",
    "Phòng Khoa học Công nghệ",
)
_LEFT_SHIFT_TARGETS = (
    "ĐẠI HỌC Y DƯỢC",
    "THÀNH PHỐ HỒ CHÍ MINH",
    "PHÒNG KHOA HỌC CÔNG NGHỆ",
    "{{ mau_02.don_vi|upper }}",
)

_W_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
_NS = {"w": _W_NS}


def _paragraph_plain_text(p: ET.Element) -> str:
    return "".join((t.text or "") for t in p.findall(".//w:t", _NS))


def _compact_no_inner_spaces(text: str) -> str:
    """Collapse whitespace incl. NBSP / figure space so « BỘ Y TẾ » matches reliably."""
    t = (
        text.replace("\u00a0", " ")
        .replace("\u2007", " ")
        .replace("\u202f", " ")
        .replace("\u3000", " ")
    )
    return "".join(t.split())


def _ensure_paragraph_centered_no_indent(p: ET.Element) -> None:
    p_pr = p.find("w:pPr", _NS)
    if p_pr is None:
        p_pr = ET.SubElement(p, f"{{{_W_NS}}}pPr")

    ind = p_pr.find("w:ind", _NS)
    if ind is not None:
        p_pr.remove(ind)

    jc = p_pr.find("w:jc", _NS)
    if jc is None:
        jc = ET.SubElement(p_pr, f"{{{_W_NS}}}jc")
    jc.set(f"{{{_W_NS}}}val", "center")


def _ensure_run_times_new_roman(r_pr: ET.Element) -> None:
    fonts = r_pr.find("w:rFonts", _NS)
    if fonts is None:
        fonts = ET.SubElement(r_pr, f"{{{_W_NS}}}rFonts")
    tnr = "Times New Roman"
    fonts.set(f"{{{_W_NS}}}ascii", tnr)
    fonts.set(f"{{{_W_NS}}}hAnsi", tnr)
    fonts.set(f"{{{_W_NS}}}cs", tnr)
    fonts.set(f"{{{_W_NS}}}eastAsia", tnr)


def _ensure_run_bold(r_pr: ET.Element) -> None:
    for tag in ("b", "bCs"):
        el = r_pr.find(f"w:{tag}", _NS)
        if el is None:
            el = ET.SubElement(r_pr, f"{{{_W_NS}}}{tag}")
        el.set(f"{{{_W_NS}}}val", "1")


def _ensure_run_not_bold(r_pr: ET.Element) -> None:
    """Strip bold so the run renders regular weight. Removes the tag entirely (rather than
    setting val="0") because the cover ministry line never inherits bold from the styles we
    emit, and tests pin the post-normalization XML to having no <w:b> at all."""
    for tag in ("b", "bCs"):
        el = r_pr.find(f"w:{tag}", _NS)
        if el is not None:
            r_pr.remove(el)


def _ensure_run_not_italic(r_pr: ET.Element) -> None:
    """Force upright text with val="0" rather than removing the tag, so it overrides any
    italic inherited from paragraph / character styles."""
    for tag in ("i", "iCs"):
        el = r_pr.find(f"w:{tag}", _NS)
        if el is None:
            el = ET.SubElement(r_pr, f"{{{_W_NS}}}{tag}")
        el.set(f"{{{_W_NS}}}val", "0")


def _run_has_text_content(run: ET.Element) -> bool:
    for t in run.findall(".//w:t", _NS):
        if (t.text or "").strip():
            return True
    return False


def _local_tag(elem: ET.Element) -> str:
    tag = elem.tag
    return tag.split("}", 1)[-1] if "}" in tag else tag


# Letterhead: one paragraph with a soft break, or two stacked paragraphs.
_UNI_LINE_DHYD_COMPACT = "".join("ĐẠI HỌC Y DƯỢC".split())
_UNI_LINE_TPHCM_COMPACT = "".join("THÀNH PHỐ HỒ CHÍ MINH".split())


def _canonicalize_dhyd_typo(s: str) -> str:
    """Map the official template's misspelled « HỘC » (Ộ = U+1ED8, Ô + dot below) onto the
    correct « HỌC » (Ọ = U+1ECC, O + dot below) so all letterhead comparisons can use a
    single canonical compact form regardless of which spelling the source DOCX carries."""
    return s.replace("\u1ed8", "\u1ecc").replace("\u1ed9", "\u1ecd")


def _is_university_letterhead_paragraph(para_text: str) -> bool:
    n = (
        para_text.replace("\u00a0", " ")
        .replace("\u2007", " ")
        .replace("\u202f", " ")
        .replace("\u3000", " ")
    )
    canonical = _canonicalize_dhyd_typo(n)
    if "ĐẠI HỌC Y DƯỢC" in canonical and "THÀNH PHỐ HỒ CHÍ MINH" in canonical:
        return True
    compact = "".join(canonical.split())
    return compact in (_UNI_LINE_DHYD_COMPACT, _UNI_LINE_TPHCM_COMPACT)


def _canonical_compact(text: str) -> str:
    """Compact (drop all whitespace) and canonicalize the dhyd typo in one step."""
    return _canonicalize_dhyd_typo(_compact_no_inner_spaces(text))


def _university_paragraph_has_soft_break(p: ET.Element) -> bool:
    """True when a non-page <w:br/> already separates the two letterhead phrases in
    document order inside this paragraph (handles a soft break inside one run, or a
    break between two runs)."""
    type_attr = f"{{{_W_NS}}}type"
    dhyd = _UNI_LINE_DHYD_COMPACT
    tphcm = _UNI_LINE_TPHCM_COMPACT
    seen_dhyd = False
    for elem in p.iter():
        local = _local_tag(elem)
        if local == "t":
            compact = _canonical_compact(elem.text or "")
            if not seen_dhyd and dhyd in compact:
                seen_dhyd = True
                tphcm_at = compact.find(tphcm)
                if tphcm_at != -1 and tphcm_at > compact.find(dhyd):
                    # Both phrases sit in the same <w:t> with nothing between them.
                    return False
            elif seen_dhyd and tphcm in compact:
                return False
        elif local == "br" and seen_dhyd:
            if elem.attrib.get(type_attr) != "page":
                return True
    return False


def _ensure_university_letterhead_visual_split(p: ET.Element) -> None:
    """Insert a soft <w:br/> immediately before the city-line phrase when both letterhead
    phrases share a paragraph on one visual line.

    Idempotent: skips paragraphs that only carry one of the phrases (two-paragraph layout
    is handled by the caller) and paragraphs already broken with a soft <w:br/>.

    Handles three shapes:
      - both phrases inside a single <w:t> -> split that <w:t>
      - phrases in two adjacent <w:t> in the same <w:r> -> insert <w:br/> between them
      - phrases in <w:t> elements that live in different <w:r> -> insert <w:br/> inside
        the run carrying the city line, right before its <w:t>
    """
    full_compact = _canonical_compact(_paragraph_plain_text(p))
    if _UNI_LINE_DHYD_COMPACT not in full_compact:
        return
    if _UNI_LINE_TPHCM_COMPACT not in full_compact:
        return
    if _university_paragraph_has_soft_break(p):
        return

    target_compact = _UNI_LINE_TPHCM_COMPACT
    head_char = target_compact[0]

    for run in p.findall("w:r", _NS):
        for child_idx, child in enumerate(list(run)):
            if _local_tag(child) != "t":
                continue
            text = child.text or ""
            if target_compact not in _canonical_compact(text):
                continue

            split_at = -1
            for i, ch in enumerate(text):
                if ch == head_char and _canonical_compact(text[i:]).startswith(target_compact):
                    split_at = i
                    break
            if split_at < 0:
                continue

            br = ET.Element(f"{{{_W_NS}}}br")
            if split_at == 0:
                run.insert(child_idx, br)
            else:
                before = text[:split_at].rstrip()
                after = text[split_at:]
                child.text = before
                new_t = ET.Element(f"{{{_W_NS}}}t")
                new_t.set("{http://www.w3.org/XML/1998/namespace}space", "preserve")
                new_t.text = after
                run.insert(child_idx + 1, br)
                run.insert(child_idx + 2, new_t)
            return


def _paragraphs_document_order(root: ET.Element) -> list[ET.Element]:
    """All ``w:p`` in serialization (depth-first) order."""
    return root.findall(".//w:p", _NS)


def _first_hard_page_break_paragraph_index(paragraphs: list[ET.Element]) -> int | None:
    """Index of the first paragraph that contains ``w:br`` with ``type`` = ``page``."""
    type_attr = f"{{{_W_NS}}}type"
    for i, p in enumerate(paragraphs):
        for br in p.findall(".//w:br", _NS):
            if br.attrib.get(type_attr) == "page":
                return i
    return None


def _paragraph_ids_first_physical_page(paragraphs: list[ET.Element]) -> set[int]:
    """
    Paragraph object ids on the first printed page: everything up to and including the first
    paragraph with a hard page break. If there is no hard break (rare), only the first chunks
    of the body are considered so duplicate letterheads in later tables keep template styling.
    """
    brk = _first_hard_page_break_paragraph_index(paragraphs)
    if brk is not None:
        return {id(paragraphs[i]) for i in range(brk + 1)}
    cap = min(len(paragraphs), 24)
    return {id(paragraphs[i]) for i in range(cap)}


def strip_table_row_height_rules_from_docx(data: bytes) -> bytes:
    """
    Remove `<w:trHeight/>` from `word/document.xml` so rows use automatic height in Word OOXML.

    Fixed / atLeast heights are often mapped incorrectly by docx-preview to CSS `height`, which
    makes wrapped Vietnamese text overlap inside table cells when exporting to PDF in the
    browser. LibreOffice also lays out more predictably without contradictory height hints.
    """
    inp = io.BytesIO(data)
    out = io.BytesIO()
    with zipfile.ZipFile(inp, "r") as zin:
        with zipfile.ZipFile(out, "w", compression=zipfile.ZIP_DEFLATED) as zout:
            for info in zin.infolist():
                raw = zin.read(info.filename)
                if info.filename == "word/document.xml":
                    text = raw.decode("utf-8")
                    text, n1 = _TR_HEIGHT_RE.subn("", text)
                    text, n2 = _TR_HEIGHT_BLOCK_RE.subn("", text)
                    if n1 or n2:
                        raw = text.encode("utf-8")
                zout.writestr(info, raw)
    return out.getvalue()


def _should_rewrite_distribute_jc_in_aux_word_part(filename: str) -> bool:
    """Parts besides ``document.xml`` that may carry ``<w:jc w:val=\"distribute\"/>`` (styles, notes, headers)."""
    if filename == "word/document.xml":
        return False
    if not filename.startswith("word/") or not filename.endswith(".xml"):
        return False
    if filename in (
        "word/styles.xml",
        "word/footnotes.xml",
        "word/endnotes.xml",
        "word/comments.xml",
    ):
        return True
    base = filename.split("/")[-1]
    if base.startswith("header") and base.endswith(".xml"):
        return True
    if base.startswith("footer") and base.endswith(".xml"):
        return True
    return False


def _patch_settings_add_do_not_expand_shift_return(raw: bytes) -> bytes:
    """Ensure ``w:doNotExpandShiftReturn`` so lines ending in soft breaks are not stretched when justified."""
    val_attr = f"{{{_W_NS}}}val"
    root = ET.fromstring(raw)
    compat = root.find("w:compat", _NS)
    if compat is None:
        compat = ET.SubElement(root, f"{{{_W_NS}}}compat")
    dnsr = None
    for child in compat:
        if _local_tag(child) == "doNotExpandShiftReturn":
            dnsr = child
            break
    if dnsr is None:
        dnsr = ET.SubElement(compat, f"{{{_W_NS}}}doNotExpandShiftReturn")
    dnsr.set(val_attr, "1")
    return ET.tostring(root, encoding="utf-8", xml_declaration=True)


def relax_justified_softbreak_paragraphs_in_docx(data: bytes) -> bytes:
    """
    Cap word-spacing growth in justified paragraphs so word gaps stay closer to normal
    word space (target « not more than a few spaces » between words on short lines).

    Transforms:

    1. ``<w:jc w:val=\"distribute\"/>`` -> ``<w:jc w:val=\"both\"/>`` everywhere it appears
       (``document.xml``, ``styles.xml``, headers/footers, footnotes…). ``distribute`` also
       adds inter-*character* pitch on every line and is the worst case for gappy Vietnamese.
    2. In ``word/document.xml`` only: justified (``both``) paragraphs that contain a non-page
       soft ``<w:br/>`` are split into one paragraph per soft-break segment so each visual
       line before a soft break becomes a true paragraph last line and is not stretched.
    3. ``word/settings.xml``: set compatibility ``w:doNotExpandShiftReturn`` so consumers that
       honor it also skip stretching lines that end in a soft line break.

    The XML structure is preserved otherwise (runs, run properties, text content, page
    breaks all survive unchanged); only soft ``<w:br/>`` elements are consumed during the
    paragraph split.
    """
    inp = io.BytesIO(data)
    out = io.BytesIO()
    with zipfile.ZipFile(inp, "r") as zin:
        with zipfile.ZipFile(out, "w", compression=zipfile.ZIP_DEFLATED) as zout:
            for info in zin.infolist():
                raw = zin.read(info.filename)
                if info.filename == "word/document.xml":
                    root = ET.fromstring(raw)
                    _convert_distribute_to_both(root)
                    _split_justified_paragraphs_at_softbreaks(root)
                    raw = ET.tostring(root, encoding="utf-8", xml_declaration=True)
                elif _should_rewrite_distribute_jc_in_aux_word_part(info.filename):
                    root = ET.fromstring(raw)
                    _convert_distribute_to_both(root)
                    raw = ET.tostring(root, encoding="utf-8", xml_declaration=True)
                elif info.filename == "word/settings.xml":
                    raw = _patch_settings_add_do_not_expand_shift_return(raw)
                zout.writestr(info, raw)
    return out.getvalue()


def _convert_distribute_to_both(root: ET.Element) -> None:
    """Rewrite every ``<w:jc w:val="distribute"/>`` to ``both`` so the last line of each
    paragraph stops being stretched to fill the column width."""
    val_attr = f"{{{_W_NS}}}val"
    for jc in root.findall(".//w:jc", _NS):
        if jc.attrib.get(val_attr) == "distribute":
            jc.attrib[val_attr] = "both"


def _paragraph_is_justified_both(p: ET.Element) -> bool:
    val_attr = f"{{{_W_NS}}}val"
    p_pr = p.find("w:pPr", _NS)
    if p_pr is None:
        return False
    jc = p_pr.find("w:jc", _NS)
    if jc is None:
        return False
    return jc.attrib.get(val_attr) == "both"


def _paragraph_has_soft_break(p: ET.Element) -> bool:
    type_attr = f"{{{_W_NS}}}type"
    for br in p.findall(".//w:br", _NS):
        if br.attrib.get(type_attr) != "page":
            return True
    return False


def _clone_paragraph_shell(p: ET.Element) -> ET.Element:
    """Create an empty ``<w:p>`` element that copies ``p``'s attributes and a deep copy of
    its ``<w:pPr>`` (so paragraph alignment, indent, spacing and run-default properties
    travel into the split fragments). Body content is not copied."""
    new_p = ET.Element(p.tag, dict(p.attrib))
    p_pr = p.find("w:pPr", _NS)
    if p_pr is not None:
        new_p.append(deepcopy(p_pr))
    return new_p


def _split_run_at_softbreak(run: ET.Element) -> list[ET.Element]:
    """Split a single ``<w:r>`` containing one or more non-page ``<w:br/>`` into a list of
    runs. The returned list is interleaved so callers can detect break points: each
    consecutive pair represents content separated by a soft break. Runs without a soft
    break return a single-element list containing the original run."""
    type_attr = f"{{{_W_NS}}}type"
    rPr = run.find("w:rPr", _NS)
    body_children = [child for child in run if _local_tag(child) != "rPr"]
    has_soft_break = any(
        _local_tag(c) == "br" and c.attrib.get(type_attr) != "page" for c in body_children
    )
    if not has_soft_break:
        return [run]

    fragments: list[ET.Element] = []
    current = ET.Element(run.tag, dict(run.attrib))
    if rPr is not None:
        current.append(deepcopy(rPr))

    def _flush() -> None:
        nonlocal current
        fragments.append(current)
        current = ET.Element(run.tag, dict(run.attrib))
        if rPr is not None:
            current.append(deepcopy(rPr))

    for child in body_children:
        if _local_tag(child) == "br" and child.attrib.get(type_attr) != "page":
            _flush()
            continue
        current.append(deepcopy(child))
    fragments.append(current)
    return fragments


def _split_justified_paragraphs_at_softbreaks(root: ET.Element) -> None:
    """For each paragraph with ``<w:jc w:val="both"/>`` that contains a non-page soft break,
    replace the paragraph with N paragraphs, splitting at each soft break.

    Walks each ``<w:p>``'s direct children in document order. Soft breaks inside runs are
    handled by :func:`_split_run_at_softbreak`; soft breaks that appear as direct paragraph
    children (rare but valid) trigger a paragraph boundary directly. Paragraph property
    elements (``<w:pPr>``) are deep-copied into every fragment so alignment / indent /
    spacing survive.
    """
    type_attr = f"{{{_W_NS}}}type"
    parent_map = {child: parent for parent in root.iter() for child in parent}

    for p in list(root.findall(".//w:p", _NS)):
        if not _paragraph_is_justified_both(p):
            continue
        if not _paragraph_has_soft_break(p):
            continue

        fragments: list[ET.Element] = [_clone_paragraph_shell(p)]

        def _start_new_fragment() -> None:
            fragments.append(_clone_paragraph_shell(p))

        for child in list(p):
            local = _local_tag(child)
            if local == "pPr":
                continue
            if local == "br" and child.attrib.get(type_attr) != "page":
                _start_new_fragment()
                continue
            if local == "r":
                run_parts = _split_run_at_softbreak(child)
                if len(run_parts) == 1:
                    fragments[-1].append(run_parts[0])
                    continue
                for idx, part in enumerate(run_parts):
                    if idx > 0:
                        _start_new_fragment()
                    fragments[-1].append(part)
                continue
            fragments[-1].append(deepcopy(child))

        if len(fragments) < 2:
            continue

        parent = parent_map.get(p)
        if parent is None:
            continue
        siblings = list(parent)
        try:
            idx = siblings.index(p)
        except ValueError:
            continue
        parent.remove(p)
        for offset, frag in enumerate(fragments):
            parent.insert(idx + offset, frag)


def shrink_overflow_sensitive_text_half_point(data: bytes) -> bytes:
    """
    Reduce font size by 0.5pt (1 half-point in OOXML) for specific long phrases
    that frequently wrap inside narrow header/table cells.
    """
    inp = io.BytesIO(data)
    out = io.BytesIO()
    with zipfile.ZipFile(inp, "r") as zin:
        with zipfile.ZipFile(out, "w", compression=zipfile.ZIP_DEFLATED) as zout:
            for info in zin.infolist():
                raw = zin.read(info.filename)
                if info.filename == "word/document.xml":
                    text = raw.decode("utf-8")

                    def _patch_paragraph(match: re.Match[str]) -> str:
                        para = match.group(0)
                        para_text = "".join(re.findall(r"<w:t[^>]*>(.*?)</w:t>", para, re.IGNORECASE | re.DOTALL))
                        if not any(t in para_text for t in _SHRINK_TARGET_PHRASES):
                            return para

                        def _patch_run(run_match: re.Match[str]) -> str:
                            run = run_match.group(0)

                            def _dec_sz(m: re.Match[str]) -> str:
                                next_val = max(2, int(m.group(2)) - 1)
                                return f"{m.group(1)}{next_val}{m.group(3)}"

                            run = _RUN_SZ_RE.sub(_dec_sz, run)
                            run = _RUN_SZCS_RE.sub(_dec_sz, run)
                            return run

                        return _RUN_RE.sub(_patch_run, para)

                    text = _PARAGRAPH_RE.sub(_patch_paragraph, text)
                    raw = text.encode("utf-8")
                zout.writestr(info, raw)
    return out.getvalue()


def style_national_header_line(data: bytes) -> bytes:
    """
    Force `CỘNG HÒA XÃ HỘI CHỦ NGHĨA VIỆT NAM` runs to:
    - 13pt
    - bold
    - slightly condensed character spacing
    """
    target = "CỘNG HÒA XÃ HỘI CHỦ NGHĨA VIỆT NAM"

    inp = io.BytesIO(data)
    out = io.BytesIO()
    with zipfile.ZipFile(inp, "r") as zin:
        with zipfile.ZipFile(out, "w", compression=zipfile.ZIP_DEFLATED) as zout:
            for info in zin.infolist():
                raw = zin.read(info.filename)
                if info.filename == "word/document.xml":
                    root = ET.fromstring(raw)
                    for p in root.findall(".//w:p", _NS):
                        para_text = "".join((t.text or "") for t in p.findall(".//w:t", _NS))
                        if target not in para_text:
                            continue
                        p_pr = p.find("w:pPr", _NS)
                        if p_pr is None:
                            p_pr = ET.SubElement(p, f"{{{_W_NS}}}pPr")

                        jc = p_pr.find("w:jc", _NS)
                        if jc is None:
                            jc = ET.SubElement(p_pr, f"{{{_W_NS}}}jc")
                        jc.set(f"{{{_W_NS}}}val", "left")

                        ind = p_pr.find("w:ind", _NS)
                        if ind is None:
                            ind = ET.SubElement(p_pr, f"{{{_W_NS}}}ind")
                        # Shift further left by ~10px (150 twips) from previous layout.
                        ind.set(f"{{{_W_NS}}}left", "-270")
                        ind.set(f"{{{_W_NS}}}right", "0")

                        for r in p.findall("w:r", _NS):
                            rPr = r.find("w:rPr", _NS)
                            if rPr is None:
                                rPr = ET.SubElement(r, f"{{{_W_NS}}}rPr")

                            sz = rPr.find("w:sz", _NS)
                            if sz is None:
                                sz = ET.SubElement(rPr, f"{{{_W_NS}}}sz")
                            sz.set(f"{{{_W_NS}}}val", "21")  # 10.5pt (closest OOXML half-point to 10.7)

                            sz_cs = rPr.find("w:szCs", _NS)
                            if sz_cs is None:
                                sz_cs = ET.SubElement(rPr, f"{{{_W_NS}}}szCs")
                            sz_cs.set(f"{{{_W_NS}}}val", "21")

                            b = rPr.find("w:b", _NS)
                            if b is None:
                                b = ET.SubElement(rPr, f"{{{_W_NS}}}b")
                            b.set(f"{{{_W_NS}}}val", "1")

                            b_cs = rPr.find("w:bCs", _NS)
                            if b_cs is None:
                                b_cs = ET.SubElement(rPr, f"{{{_W_NS}}}bCs")
                            b_cs.set(f"{{{_W_NS}}}val", "1")

                            spacing = rPr.find("w:spacing", _NS)
                            if spacing is None:
                                spacing = ET.SubElement(rPr, f"{{{_W_NS}}}spacing")
                            spacing.set(f"{{{_W_NS}}}val", "-6")

                    raw = ET.tostring(root, encoding="utf-8", xml_declaration=True)
                zout.writestr(info, raw)
    return out.getvalue()


def shift_selected_header_lines_left(data: bytes) -> bytes:
    """Shift selected header/unit lines left by ~10px (150 twips)."""
    inp = io.BytesIO(data)
    out = io.BytesIO()
    with zipfile.ZipFile(inp, "r") as zin:
        with zipfile.ZipFile(out, "w", compression=zipfile.ZIP_DEFLATED) as zout:
            for info in zin.infolist():
                raw = zin.read(info.filename)
                if info.filename == "word/document.xml":
                    root = ET.fromstring(raw)
                    for p in root.findall(".//w:p", _NS):
                        para_text = "".join((t.text or "") for t in p.findall(".//w:t", _NS))
                        if not any(tok in para_text for tok in _LEFT_SHIFT_TARGETS):
                            continue
                        p_pr = p.find("w:pPr", _NS)
                        if p_pr is None:
                            p_pr = ET.SubElement(p, f"{{{_W_NS}}}pPr")
                        ind = p_pr.find("w:ind", _NS)
                        if ind is None:
                            ind = ET.SubElement(p_pr, f"{{{_W_NS}}}ind")
                        ind.set(f"{{{_W_NS}}}left", "-150")
                        ind.set(f"{{{_W_NS}}}right", "0")
                    raw = ET.tostring(root, encoding="utf-8", xml_declaration=True)
                zout.writestr(info, raw)
    return out.getvalue()


def normalize_bo_y_te_header_lines(data: bytes) -> bytes:
    """
    First-page letterhead only:

    - Ministry line « BỘ Y TẾ »: centered, regular weight (bold tag stripped), upright.
    - University block « ĐẠI HỘC Y DƯỢC » + « THÀNH PHỐ HỒ CHÍ MINH »: centered, bold, upright.
      When the two phrases live in one paragraph on a single visual line, a soft <w:br/> is
      inserted before the city line so the cover renders the phrases on two stacked lines.

    Later pages / tables that repeat the ministry block are left unchanged.

    Run after `shift_selected_header_lines_left` so negative indents do not offset these blocks.
    """
    inp = io.BytesIO(data)
    out = io.BytesIO()
    with zipfile.ZipFile(inp, "r") as zin:
        with zipfile.ZipFile(out, "w", compression=zipfile.ZIP_DEFLATED) as zout:
            for info in zin.infolist():
                raw = zin.read(info.filename)
                if info.filename == "word/document.xml":
                    root = ET.fromstring(raw)
                    paragraphs = _paragraphs_document_order(root)
                    first_page_ids = _paragraph_ids_first_physical_page(paragraphs)
                    for p in paragraphs:
                        if id(p) not in first_page_ids:
                            continue
                        para_text = _paragraph_plain_text(p)
                        compact = _compact_no_inner_spaces(para_text)

                        if compact == "BỘYTẾ":
                            _ensure_paragraph_centered_no_indent(p)
                            for r in p.findall("w:r", _NS):
                                r_pr = r.find("w:rPr", _NS)
                                if r_pr is None:
                                    r_pr = ET.SubElement(r, f"{{{_W_NS}}}rPr")
                                _ensure_run_not_bold(r_pr)
                                _ensure_run_not_italic(r_pr)
                                _ensure_run_times_new_roman(r_pr)
                            continue

                        if _is_university_letterhead_paragraph(para_text):
                            _ensure_paragraph_centered_no_indent(p)
                            _ensure_university_letterhead_visual_split(p)
                            for r in p.findall("w:r", _NS):
                                if not _run_has_text_content(r):
                                    continue
                                r_pr = r.find("w:rPr", _NS)
                                if r_pr is None:
                                    r_pr = ET.SubElement(r, f"{{{_W_NS}}}rPr")
                                _ensure_run_bold(r_pr)
                                _ensure_run_not_italic(r_pr)
                                _ensure_run_times_new_roman(r_pr)

                    raw = ET.tostring(root, encoding="utf-8", xml_declaration=True)
                zout.writestr(info, raw)
    return out.getvalue()


_DON_VI_PREFIX = "ĐƠN VỊ:"
_DON_VI_SIGNATURE_PREFIX = "Đơn vị "
_SIGNATURE_LEADER_PHRASE = "Xác nhận của lãnh đạo"
_NATIONAL_HEADER_PHRASE = "CỘNG HÒA XÃ HỘI CHỦ NGHĨA VIỆT NAM"


def _table_plain_text(tbl: ET.Element) -> str:
    return "".join((t.text or "") for t in tbl.findall(".//w:t", _NS))


def _strip_text_prefix_from_paragraph(p: ET.Element, prefix: str) -> bool:
    """Remove the first occurrence of ``prefix`` from a <w:t> in this paragraph. Returns
    True when the prefix was found and stripped. The trailing remainder is left-stripped
    so a trailing whitespace token does not turn the line into a leading-space artifact."""
    for t in p.findall(".//w:t", _NS):
        text = t.text or ""
        idx = text.find(prefix)
        if idx < 0:
            continue
        remainder = text[idx + len(prefix):].lstrip()
        t.text = (text[:idx] + remainder).lstrip()
        t.set("{http://www.w3.org/XML/1998/namespace}space", "preserve")
        return True
    return False


def _strip_don_vi_prefix_from_paragraph(p: ET.Element) -> None:
    """Remove the literal « ĐƠN VỊ: » prefix from the first <w:t> that contains it; keep
    the rest of the paragraph untouched (Jinja placeholder, trailing whitespace, etc.)."""
    _strip_text_prefix_from_paragraph(p, _DON_VI_PREFIX)


def normalize_mau_02_letterhead_table(data: bytes) -> bytes:
    """Tighten the Mẫu số 02 letterhead table so the four header lines align cleanly:

    - Right cell « CỘNG HÒA XÃ HỘI CHỦ NGHĨA VIỆT NAM »: shrink font so the phrase fits
      on a single line within the narrow right column, override « left » alignment set
      by :func:`style_national_header_line` back to centered, and clear its negative
      indent (which only made sense for the full-width header on other pages).
    - Left cell « ĐẠI HỌC Y DƯỢC THÀNH PHỐ HỒ CHÍ MINH »: insert a soft <w:br/> before
      « THÀNH PHỐ HỒ CHÍ MINH » so the phrase wraps cleanly into two stacked lines, and
      strip bold from every run.
    - Left cell « ĐƠN VỊ: {{ mau_02.don_vi }} »: drop the « ĐƠN VỊ: » literal prefix so
      only the resolved unit name remains; force bold and strip italic on every run.

    Scoped to the Mẫu số 02 table by requiring both « ĐƠN VỊ: » and the national-header
    phrase in the same <w:tbl>, so the duplicate letterhead blocks on Mẫu số 03 / Bản
    cam kết (which have « BỘ Y TẾ » instead of « ĐƠN VỊ ») are left untouched.
    """
    inp = io.BytesIO(data)
    out = io.BytesIO()
    with zipfile.ZipFile(inp, "r") as zin:
        with zipfile.ZipFile(out, "w", compression=zipfile.ZIP_DEFLATED) as zout:
            for info in zin.infolist():
                raw = zin.read(info.filename)
                if info.filename == "word/document.xml":
                    root = ET.fromstring(raw)
                    for tbl in root.findall(".//w:tbl", _NS):
                        all_text = _table_plain_text(tbl)
                        if _DON_VI_PREFIX not in all_text:
                            continue
                        if _NATIONAL_HEADER_PHRASE not in all_text:
                            continue
                        for p in tbl.findall(".//w:p", _NS):
                            para_text = _paragraph_plain_text(p)

                            if _NATIONAL_HEADER_PHRASE in para_text:
                                p_pr = p.find("w:pPr", _NS)
                                if p_pr is None:
                                    p_pr = ET.SubElement(p, f"{{{_W_NS}}}pPr")
                                ind = p_pr.find("w:ind", _NS)
                                if ind is not None:
                                    p_pr.remove(ind)
                                jc = p_pr.find("w:jc", _NS)
                                if jc is None:
                                    jc = ET.SubElement(p_pr, f"{{{_W_NS}}}jc")
                                jc.set(f"{{{_W_NS}}}val", "center")
                                for r in p.findall("w:r", _NS):
                                    r_pr = r.find("w:rPr", _NS)
                                    if r_pr is None:
                                        r_pr = ET.SubElement(r, f"{{{_W_NS}}}rPr")
                                    sz = r_pr.find("w:sz", _NS)
                                    if sz is None:
                                        sz = ET.SubElement(r_pr, f"{{{_W_NS}}}sz")
                                    sz.set(f"{{{_W_NS}}}val", "19")  # 9.5pt
                                    sz_cs = r_pr.find("w:szCs", _NS)
                                    if sz_cs is None:
                                        sz_cs = ET.SubElement(r_pr, f"{{{_W_NS}}}szCs")
                                    sz_cs.set(f"{{{_W_NS}}}val", "19")
                                    spacing = r_pr.find("w:spacing", _NS)
                                    if spacing is None:
                                        spacing = ET.SubElement(r_pr, f"{{{_W_NS}}}spacing")
                                    spacing.set(f"{{{_W_NS}}}val", "-8")
                                continue

                            if _is_university_letterhead_paragraph(para_text):
                                _ensure_paragraph_centered_no_indent(p)
                                _ensure_university_letterhead_visual_split(p)
                                for r in p.findall("w:r", _NS):
                                    if not _run_has_text_content(r):
                                        continue
                                    r_pr = r.find("w:rPr", _NS)
                                    if r_pr is None:
                                        r_pr = ET.SubElement(r, f"{{{_W_NS}}}rPr")
                                    _ensure_run_not_bold(r_pr)
                                    _ensure_run_not_italic(r_pr)
                                    _ensure_run_times_new_roman(r_pr)
                                continue

                            if _DON_VI_PREFIX in para_text:
                                _strip_don_vi_prefix_from_paragraph(p)
                                _ensure_paragraph_centered_no_indent(p)
                                for r in p.findall("w:r", _NS):
                                    r_pr = r.find("w:rPr", _NS)
                                    if r_pr is None:
                                        r_pr = ET.SubElement(r, f"{{{_W_NS}}}rPr")
                                    _ensure_run_bold(r_pr)
                                    _ensure_run_not_italic(r_pr)
                                    _ensure_run_times_new_roman(r_pr)

                    raw = ET.tostring(root, encoding="utf-8", xml_declaration=True)
                zout.writestr(info, raw)
    return out.getvalue()


_BO_Y_TE_COMPACT = "BỘYTẾ"


_MAU_04_HEADER_COMPACT = "Mẫusố04"


def _paragraph_has_page_break(p: ET.Element) -> bool:
    """True when ``p`` contains at least one ``<w:br w:type="page"/>``."""
    type_attr = f"{{{_W_NS}}}type"
    for br in p.findall(".//w:br", _NS):
        if br.attrib.get(type_attr) == "page":
            return True
    return False


def _paragraph_is_empty_page_break(p: ET.Element) -> bool:
    """True when the paragraph contains a page break and no visible text — i.e. its
    only purpose is to force a page break. Such paragraphs render as a blank page in
    docx-preview when followed by a table, even though LibreOffice/Word collapse them.
    """
    if not _paragraph_has_page_break(p):
        return False
    for t in p.findall(".//w:t", _NS):
        if (t.text or "").strip():
            return False
    return True


def _ensure_pPr(p: ET.Element) -> ET.Element:
    p_pr = p.find("w:pPr", _NS)
    if p_pr is None:
        p_pr = ET.Element(f"{{{_W_NS}}}pPr")
        p.insert(0, p_pr)
    return p_pr


def _ensure_page_break_before(p: ET.Element) -> None:
    """Add ``<w:pageBreakBefore/>`` to the paragraph's ``<w:pPr>`` (after ``pStyle`` if
    present, to satisfy the OOXML element order). Idempotent."""
    p_pr = _ensure_pPr(p)
    if p_pr.find("w:pageBreakBefore", _NS) is not None:
        return
    pbb = ET.Element(f"{{{_W_NS}}}pageBreakBefore")
    p_style = p_pr.find("w:pStyle", _NS)
    if p_style is None:
        p_pr.insert(0, pbb)
    else:
        children = list(p_pr)
        idx = children.index(p_style)
        p_pr.insert(idx + 1, pbb)


def _first_paragraph_inside_table(tbl: ET.Element) -> ET.Element | None:
    """Return the first ``<w:p>`` inside the first cell of the first row of ``tbl``, or
    ``None`` if the table has no paragraph (rare; tables always have a cell paragraph)."""
    first_row = tbl.find("w:tr", _NS)
    if first_row is None:
        return None
    first_cell = first_row.find("w:tc", _NS)
    if first_cell is None:
        return None
    return first_cell.find("w:p", _NS)


def collapse_empty_page_break_paragraphs_in_docx(data: bytes) -> bytes:
    """Eliminate blank pages introduced by empty paragraphs whose only purpose is to
    host a ``<w:br w:type="page"/>``. The browser-side docx-preview renderer treats such
    paragraphs as occupying their own page when followed by a table (see body indices
    where the rendered docx shows e.g. « page 4 / 10 » as a blank sheet between Mẫu số
    01 sig table and Mẫu số 02 letterhead table), even though LibreOffice/Word collapse
    them onto the surrounding pages.

    Transform pattern (per empty page-break paragraph ``P_break``):

    1. Look at the *next* body sibling of ``P_break``:
       - ``<w:p>``: add ``<w:pageBreakBefore/>`` to its ``<w:pPr>`` (idempotent), then
         remove ``P_break`` from the body. The next paragraph now starts on a new page.
       - ``<w:tbl>``: add ``<w:pageBreakBefore/>`` to the ``<w:pPr>`` of the first
         paragraph inside the first cell of the first row of the table, then remove
         ``P_break``. The table is then anchored to a new page via the cell paragraph.
       - Anything else / no next sibling: leave ``P_break`` untouched so the original
         page break is preserved as a fallback.

    Idempotent: once a paragraph already carries ``<w:pageBreakBefore/>``, a second pass
    does not double-register it; once the empty break paragraph is removed, there is
    nothing left to collapse.
    """
    inp = io.BytesIO(data)
    out = io.BytesIO()
    with zipfile.ZipFile(inp, "r") as zin:
        with zipfile.ZipFile(out, "w", compression=zipfile.ZIP_DEFLATED) as zout:
            for info in zin.infolist():
                raw = zin.read(info.filename)
                if info.filename == "word/document.xml":
                    root = ET.fromstring(raw)
                    body = root.find("w:body", _NS)
                    if body is not None:
                        _collapse_empty_page_break_paragraphs(body)
                    raw = ET.tostring(root, encoding="utf-8", xml_declaration=True)
                zout.writestr(info, raw)
    return out.getvalue()


def _collapse_empty_page_break_paragraphs(body: ET.Element) -> None:
    """Body-direct-children scan: do not recurse into tables (page breaks inside table
    cells are out of scope here)."""
    children = list(body)
    for i, child in enumerate(children):
        if _local_tag(child) != "p":
            continue
        if not _paragraph_is_empty_page_break(child):
            continue
        nxt = children[i + 1] if i + 1 < len(children) else None
        if nxt is None:
            continue

        nxt_local = _local_tag(nxt)
        if nxt_local == "p":
            _ensure_page_break_before(nxt)
            body.remove(child)
        elif nxt_local == "tbl":
            anchor = _first_paragraph_inside_table(nxt)
            if anchor is None:
                continue
            _ensure_page_break_before(anchor)
            body.remove(child)


def strip_mau_04_evaluation_section_in_docx(data: bytes) -> bytes:
    """Remove the « Mẫu số 04 — PHIẾU ĐÁNH GIÁ SÁNG KIẾN » section from the applicant
    template. The council evaluation form is filled in the dedicated council UI
    (``fe0/src/components/council/evaluation/InitiativeEvaluationForm.tsx``); the
    applicant's submission package must not embed an empty council scorecard.

    Section boundaries inside ``word/document.xml``:

    - Locate the ``<w:p>`` whose plain text is « Mẫu số 04 » (compact match handles any
      stray NBSP / figure-space variants).
    - Walk backwards through the body children until we hit the empty ``<w:p>`` that
      contains a ``<w:br w:type="page"/>`` — that page break ends the previous section's
      page and starts the Mẫu số 04 page. Everything from that page-break paragraph up
      to (and including) the Mẫu số 04 header is removed.
    - Walk forwards from the Mẫu số 04 header through the body children until we hit
      either the next ``<w:p>`` with a page break or a ``<w:sectPr>``. We remove
      everything strictly before that boundary, so the page break leading into the next
      section (« Bản cam kết ») is preserved and the section keeps its own page.

    Idempotent: a second pass finds no « Mẫu số 04 » paragraph and is a no-op.
    """
    inp = io.BytesIO(data)
    out = io.BytesIO()
    with zipfile.ZipFile(inp, "r") as zin:
        with zipfile.ZipFile(out, "w", compression=zipfile.ZIP_DEFLATED) as zout:
            for info in zin.infolist():
                raw = zin.read(info.filename)
                if info.filename == "word/document.xml":
                    root = ET.fromstring(raw)
                    body = root.find("w:body", _NS)
                    if body is not None:
                        _strip_mau_04_section_from_body(body)
                    raw = ET.tostring(root, encoding="utf-8", xml_declaration=True)
                zout.writestr(info, raw)
    return out.getvalue()


def _strip_mau_04_section_from_body(body: ET.Element) -> None:
    """Locate the Mẫu số 04 header paragraph and remove the surrounding section bounded
    by the page break before it and the page break (or ``<w:sectPr>``) after it."""
    children = list(body)

    target_idx: int | None = None
    for i, child in enumerate(children):
        if _local_tag(child) != "p":
            continue
        compact = _compact_no_inner_spaces(_paragraph_plain_text(child)).strip()
        if compact == _MAU_04_HEADER_COMPACT:
            target_idx = i
            break

    if target_idx is None:
        return

    # Walk backwards to find the page-break paragraph that opens the Mẫu số 04 page.
    # Bail out (do nothing) if no page break is found, so we never strip into the
    # previous section by mistake.
    start_idx: int | None = None
    for j in range(target_idx - 1, -1, -1):
        child = children[j]
        if _local_tag(child) == "p" and _paragraph_has_page_break(child):
            start_idx = j
            break
    if start_idx is None:
        return

    # Walk forwards from the Mẫu số 04 header until we hit either the next page break
    # paragraph (separator into the next section) or a <w:sectPr>. Stop one element
    # before that boundary so the boundary marker itself survives.
    end_idx: int = len(children) - 1
    for j in range(target_idx + 1, len(children)):
        child = children[j]
        if _local_tag(child) == "sectPr":
            end_idx = j - 1
            break
        if _local_tag(child) == "p" and _paragraph_has_page_break(child):
            end_idx = j - 1
            break

    for child in children[start_idx : end_idx + 1]:
        body.remove(child)


def normalize_subsequent_letterhead_tables(data: bytes) -> bytes:
    """Two letterhead tables on later pages (Mẫu số 03 + Bản cam kết) carry the same
    three-line block « BỘ Y TẾ », « ĐẠI HỌC Y DƯỢC THÀNH PHỐ HỒ CHÍ MINH » and
    « CỘNG HÒA XÃ HỘI CHỦ NGHĨA VIỆT NAM ». First-page-only
    :func:`normalize_bo_y_te_header_lines` skips them, so this pass mirrors that
    treatment inside the table cells:

    - « BỘ Y TẾ »: strip bold, center.
    - University block: insert a soft <w:br/> between the two phrases so the cell
      wraps cleanly into two stacked lines; keep bold; force upright and Times New
      Roman; center.
    - « CỘNG HÒA XÃ HỘI CHỦ NGHĨA VIỆT NAM »: shrink to 9.5pt with condensed
      character spacing and center so the phrase fits on a single line within the
      narrow right cell (the same trick used for the Mẫu số 02 letterhead table).

    Scoped to tables containing both « BỘ Y TẾ » and the national-header phrase, so
    the Mẫu số 02 letterhead table (which uses « ĐƠN VỊ: » instead of « BỘ Y TẾ »)
    and unrelated tables are left untouched.
    """
    inp = io.BytesIO(data)
    out = io.BytesIO()
    with zipfile.ZipFile(inp, "r") as zin:
        with zipfile.ZipFile(out, "w", compression=zipfile.ZIP_DEFLATED) as zout:
            for info in zin.infolist():
                raw = zin.read(info.filename)
                if info.filename == "word/document.xml":
                    root = ET.fromstring(raw)
                    for tbl in root.findall(".//w:tbl", _NS):
                        all_text = _table_plain_text(tbl)
                        if _NATIONAL_HEADER_PHRASE not in all_text:
                            continue
                        if _compact_no_inner_spaces(all_text).find(_BO_Y_TE_COMPACT) < 0:
                            continue
                        for p in tbl.findall(".//w:p", _NS):
                            para_text = _paragraph_plain_text(p)
                            compact = _compact_no_inner_spaces(para_text)

                            if compact == _BO_Y_TE_COMPACT:
                                _ensure_paragraph_centered_no_indent(p)
                                for r in p.findall("w:r", _NS):
                                    r_pr = r.find("w:rPr", _NS)
                                    if r_pr is None:
                                        r_pr = ET.SubElement(r, f"{{{_W_NS}}}rPr")
                                    _ensure_run_not_bold(r_pr)
                                    _ensure_run_not_italic(r_pr)
                                    _ensure_run_times_new_roman(r_pr)
                                continue

                            if _is_university_letterhead_paragraph(para_text):
                                _ensure_paragraph_centered_no_indent(p)
                                _ensure_university_letterhead_visual_split(p)
                                for r in p.findall("w:r", _NS):
                                    if not _run_has_text_content(r):
                                        continue
                                    r_pr = r.find("w:rPr", _NS)
                                    if r_pr is None:
                                        r_pr = ET.SubElement(r, f"{{{_W_NS}}}rPr")
                                    _ensure_run_bold(r_pr)
                                    _ensure_run_not_italic(r_pr)
                                    _ensure_run_times_new_roman(r_pr)
                                continue

                            if _NATIONAL_HEADER_PHRASE in para_text:
                                p_pr = p.find("w:pPr", _NS)
                                if p_pr is None:
                                    p_pr = ET.SubElement(p, f"{{{_W_NS}}}pPr")
                                ind = p_pr.find("w:ind", _NS)
                                if ind is not None:
                                    p_pr.remove(ind)
                                jc = p_pr.find("w:jc", _NS)
                                if jc is None:
                                    jc = ET.SubElement(p_pr, f"{{{_W_NS}}}jc")
                                jc.set(f"{{{_W_NS}}}val", "center")
                                for r in p.findall("w:r", _NS):
                                    r_pr = r.find("w:rPr", _NS)
                                    if r_pr is None:
                                        r_pr = ET.SubElement(r, f"{{{_W_NS}}}rPr")
                                    sz = r_pr.find("w:sz", _NS)
                                    if sz is None:
                                        sz = ET.SubElement(r_pr, f"{{{_W_NS}}}sz")
                                    sz.set(f"{{{_W_NS}}}val", "19")  # 9.5pt
                                    sz_cs = r_pr.find("w:szCs", _NS)
                                    if sz_cs is None:
                                        sz_cs = ET.SubElement(r_pr, f"{{{_W_NS}}}szCs")
                                    sz_cs.set(f"{{{_W_NS}}}val", "19")
                                    spacing = r_pr.find("w:spacing", _NS)
                                    if spacing is None:
                                        spacing = ET.SubElement(r_pr, f"{{{_W_NS}}}spacing")
                                    spacing.set(f"{{{_W_NS}}}val", "-8")
                    raw = ET.tostring(root, encoding="utf-8", xml_declaration=True)
                zout.writestr(info, raw)
    return out.getvalue()


def normalize_mau_02_signature_unit_prefix(data: bytes) -> bytes:
    """In the Mẫu số 02 sign-off table (« Xác nhận của lãnh đạo / Tác giả sáng kiến »),
    drop the literal « Đơn vị » word that the template prints before
    ``{{ mau_02.don_vi }}`` so the rendered cell shows only the resolved unit name.
    Forces the unit-name run bold and non-italic.

    Scoped to tables containing « Xác nhận của lãnh đạo », so the Mẫu số 03 column
    header « Đơn vị công tác » and the trang bìa label « Đơn vị công tác: » remain
    untouched.
    """
    inp = io.BytesIO(data)
    out = io.BytesIO()
    with zipfile.ZipFile(inp, "r") as zin:
        with zipfile.ZipFile(out, "w", compression=zipfile.ZIP_DEFLATED) as zout:
            for info in zin.infolist():
                raw = zin.read(info.filename)
                if info.filename == "word/document.xml":
                    root = ET.fromstring(raw)
                    for tbl in root.findall(".//w:tbl", _NS):
                        all_text = _table_plain_text(tbl)
                        if _SIGNATURE_LEADER_PHRASE not in all_text:
                            continue
                        if _DON_VI_SIGNATURE_PREFIX not in all_text:
                            continue
                        for p in tbl.findall(".//w:p", _NS):
                            para_text = _paragraph_plain_text(p)
                            if not para_text.lstrip().startswith(_DON_VI_SIGNATURE_PREFIX):
                                continue
                            # Skip the header-style label « Đơn vị công tác » used elsewhere.
                            if "công tác" in para_text:
                                continue
                            if not _strip_text_prefix_from_paragraph(p, _DON_VI_SIGNATURE_PREFIX):
                                continue
                            for r in p.findall("w:r", _NS):
                                if not _run_has_text_content(r):
                                    continue
                                r_pr = r.find("w:rPr", _NS)
                                if r_pr is None:
                                    r_pr = ET.SubElement(r, f"{{{_W_NS}}}rPr")
                                _ensure_run_bold(r_pr)
                                _ensure_run_not_italic(r_pr)
                                _ensure_run_times_new_roman(r_pr)
                    raw = ET.tostring(root, encoding="utf-8", xml_declaration=True)
                zout.writestr(info, raw)
    return out.getvalue()


def normalize_mau_02_body_alignment_spacing(data: bytes) -> bytes:
    """Normalize top-level paragraph alignment/spacing across the whole Mẫu số 02 section.

    The reference style is the compact, single-spaced body layout used in the sample
    screenshot: headings keep their visual alignment, while regular body lines share
    the same no-gap spacing so the page density remains consistent.

    Scope:
    - Only `word/document.xml`
    - Only top-level body paragraphs between `Mẫu số 02` and `Mẫu số 03`
    - Table cell paragraphs are intentionally untouched
    """
    inp = io.BytesIO(data)
    out = io.BytesIO()
    with zipfile.ZipFile(inp, "r") as zin:
        with zipfile.ZipFile(out, "w", compression=zipfile.ZIP_DEFLATED) as zout:
            for info in zin.infolist():
                raw = zin.read(info.filename)
                if info.filename == "word/document.xml":
                    root = ET.fromstring(raw)
                    body = root.find("w:body", _NS)
                    if body is not None:
                        children = list(body)
                        start_idx = None
                        end_idx = None
                        for idx, child in enumerate(children):
                            if _local_tag(child) != "p":
                                continue
                            text = _paragraph_plain_text(child).strip()
                            if start_idx is None and "Mẫu số 02" in text:
                                start_idx = idx
                                continue
                            if start_idx is not None and "Mẫu số 03" in text:
                                end_idx = idx
                                break
                        if start_idx is not None:
                            if end_idx is None:
                                end_idx = len(children)
                            for child in children[start_idx:end_idx]:
                                if _local_tag(child) != "p":
                                    continue
                                p = child
                                text = _paragraph_plain_text(p).strip()
                                if not text:
                                    continue

                                p_pr = p.find("w:pPr", _NS)
                                if p_pr is None:
                                    p_pr = ET.SubElement(p, f"{{{_W_NS}}}pPr")

                                # Remove manual paragraph indentation so each line follows
                                # one consistent body column.
                                ind = p_pr.find("w:ind", _NS)
                                if ind is not None:
                                    p_pr.remove(ind)

                                spacing = p_pr.find("w:spacing", _NS)
                                if spacing is None:
                                    spacing = ET.SubElement(p_pr, f"{{{_W_NS}}}spacing")
                                spacing.set(f"{{{_W_NS}}}before", "0")
                                spacing.set(f"{{{_W_NS}}}after", "0")
                                spacing.set(f"{{{_W_NS}}}line", "240")
                                spacing.set(f"{{{_W_NS}}}lineRule", "auto")

                                jc = p_pr.find("w:jc", _NS)
                                if jc is None:
                                    jc = ET.SubElement(p_pr, f"{{{_W_NS}}}jc")

                                if "Mẫu số 02" in text:
                                    jc.set(f"{{{_W_NS}}}val", "right")
                                    spacing.set(f"{{{_W_NS}}}before", "120")
                                elif "ĐƠN ĐỀ NGHỊ CÔNG NHẬN SÁNG KIẾN" in text:
                                    jc.set(f"{{{_W_NS}}}val", "center")
                                    spacing.set(f"{{{_W_NS}}}before", "80")
                                    spacing.set(f"{{{_W_NS}}}after", "80")
                                elif text.startswith("Kính gửi:"):
                                    jc.set(f"{{{_W_NS}}}val", "center")
                                elif text.startswith("TP. Hồ Chí Minh, ngày"):
                                    jc.set(f"{{{_W_NS}}}val", "right")
                                    spacing.set(f"{{{_W_NS}}}before", "80")
                                else:
                                    jc.set(f"{{{_W_NS}}}val", "left")

                    raw = ET.tostring(root, encoding="utf-8", xml_declaration=True)
                zout.writestr(info, raw)
    return out.getvalue()


_SIGNATURE_DATE_PREFIX = "Tp. Hồ Chí Minh, ngày"


def _signature_table_date_already_lifted(tbl: ET.Element, col_count: int) -> bool:
    """True when the table already starts with a single-cell gridSpan row hosting the date."""
    type_val_attr = f"{{{_W_NS}}}val"
    for row in tbl.findall("w:tr", _NS):
        cells = row.findall("w:tc", _NS)
        if len(cells) != 1:
            return False
        tc_pr = cells[0].find("w:tcPr", _NS)
        if tc_pr is None:
            return False
        grid_span = tc_pr.find("w:gridSpan", _NS)
        if grid_span is None or grid_span.attrib.get(type_val_attr) != str(col_count):
            return False
        for p in cells[0].findall("w:p", _NS):
            if _SIGNATURE_DATE_PREFIX in _paragraph_plain_text(p):
                return True
        return False
    return False


def move_signature_date_to_top_row(data: bytes) -> bytes:
    """Reflow each signature table so its «Tp. Hồ Chí Minh, ngày … tháng … năm …» paragraph
    sits in its own row at the top of the table, hosted in a single cell that spans every
    column.

    Two effects observable in the rendered PDF:

    - The date paragraph occupies a cell as wide as the table, so it fits on one visual line
      (it was wrapping at half-cell width).
    - The original row's other content rises one line, putting the signing-role labels
      « LÃNH ĐẠO ĐƠN VỊ » and « Tác giả sáng kiến » on the same visual line.

    Idempotent: a second pass detects the new top row and is a no-op. Tables that do not
    contain the date prefix are left untouched.
    """
    inp = io.BytesIO(data)
    out = io.BytesIO()
    with zipfile.ZipFile(inp, "r") as zin:
        with zipfile.ZipFile(out, "w", compression=zipfile.ZIP_DEFLATED) as zout:
            for info in zin.infolist():
                raw = zin.read(info.filename)
                if info.filename == "word/document.xml":
                    root = ET.fromstring(raw)
                    for tbl in root.findall(".//w:tbl", _NS):
                        grid = tbl.find("w:tblGrid", _NS)
                        if grid is None:
                            continue
                        col_count = len(grid.findall("w:gridCol", _NS))
                        if col_count < 2:
                            continue
                        if _signature_table_date_already_lifted(tbl, col_count):
                            continue

                        hit = None
                        for cell in tbl.findall(".//w:tc", _NS):
                            for p in cell.findall("w:p", _NS):
                                if _SIGNATURE_DATE_PREFIX in _paragraph_plain_text(p):
                                    hit = (cell, p)
                                    break
                            if hit:
                                break
                        if not hit:
                            continue

                        cell, p = hit
                        cell.remove(p)
                        if cell.find("w:p", _NS) is None:
                            ET.SubElement(cell, f"{{{_W_NS}}}p")

                        # Right-align the lifted paragraph and drop any negative indent so
                        # the full row width is available.
                        p_pr = p.find("w:pPr", _NS)
                        if p_pr is None:
                            p_pr = ET.Element(f"{{{_W_NS}}}pPr")
                            p.insert(0, p_pr)
                        ind = p_pr.find("w:ind", _NS)
                        if ind is not None:
                            p_pr.remove(ind)
                        jc = p_pr.find("w:jc", _NS)
                        if jc is None:
                            jc = ET.SubElement(p_pr, f"{{{_W_NS}}}jc")
                        jc.set(f"{{{_W_NS}}}val", "right")

                        new_row = ET.Element(f"{{{_W_NS}}}tr")
                        new_cell = ET.SubElement(new_row, f"{{{_W_NS}}}tc")
                        new_tc_pr = ET.SubElement(new_cell, f"{{{_W_NS}}}tcPr")
                        grid_span = ET.SubElement(new_tc_pr, f"{{{_W_NS}}}gridSpan")
                        grid_span.set(f"{{{_W_NS}}}val", str(col_count))
                        # Preserve the borderless look of the surrounding signature cells.
                        tc_borders = ET.SubElement(new_tc_pr, f"{{{_W_NS}}}tcBorders")
                        for side in ("top", "left", "bottom", "right"):
                            b = ET.SubElement(tc_borders, f"{{{_W_NS}}}{side}")
                            b.set(f"{{{_W_NS}}}val", "nil")
                        new_cell.append(p)

                        first_row_index = None
                        for idx, child in enumerate(list(tbl)):
                            if _local_tag(child) == "tr":
                                first_row_index = idx
                                break
                        if first_row_index is None:
                            tbl.append(new_row)
                        else:
                            tbl.insert(first_row_index, new_row)

                    raw = ET.tostring(root, encoding="utf-8", xml_declaration=True)
                zout.writestr(info, raw)
    return out.getvalue()


def force_times_new_roman_in_styles_docx(data: bytes) -> bytes:
    """
    Built-in paragraph styles still reference Calibri in `word/styles.xml`; use Times New Roman
    and set docDefaults `rFonts` so inherited runs match when exporting to PDF.
    """
    inp = io.BytesIO(data)
    out = io.BytesIO()
    with zipfile.ZipFile(inp, "r") as zin:
        with zipfile.ZipFile(out, "w", compression=zipfile.ZIP_DEFLATED) as zout:
            for info in zin.infolist():
                raw = zin.read(info.filename)
                if info.filename == "word/styles.xml":
                    root = ET.fromstring(raw)
                    for fonts in root.findall(".//w:rFonts", _NS):
                        for key in list(fonts.attrib.keys()):
                            if key.endswith("}ascii") or key.endswith(
                                "}hAnsi"
                            ) or key.endswith("}cs") or key.endswith("}eastAsia"):
                                fonts.attrib[key] = "Times New Roman"
                    rpr_default = root.find("./w:docDefaults/w:rPrDefault/w:rPr", _NS)
                    if rpr_default is not None:
                        fonts = rpr_default.find("w:rFonts", _NS)
                        if fonts is None:
                            fonts = ET.SubElement(rpr_default, f"{{{_W_NS}}}rFonts")
                        tnr = "Times New Roman"
                        fonts.set(f"{{{_W_NS}}}ascii", tnr)
                        fonts.set(f"{{{_W_NS}}}hAnsi", tnr)
                        fonts.set(f"{{{_W_NS}}}cs", tnr)
                        fonts.set(f"{{{_W_NS}}}eastAsia", tnr)
                    raw = ET.tostring(root, encoding="utf-8", xml_declaration=True)
                zout.writestr(info, raw)
    return out.getvalue()
