"""
DOCX template rendering utilities for printing.

This repo's print templates (e.g. `print-template.docx`) use placeholder tokens like:
- `#{打印-销售单（8*9）.客户名称}`
- `{备注}`

Important: Microsoft Word / WPS may split a token across multiple "runs" (w:r / w:t).
So we must replace placeholders on the concatenated text of a paragraph/cell, then
write back to runs to avoid leaving raw tokens in the output.
"""

from __future__ import annotations

import copy
import re
from collections.abc import Iterable, Mapping
from dataclasses import dataclass
from typing import Any

from docx.document import Document as _DocumentType
from docx.table import Table, _Cell
from docx.text.paragraph import Paragraph


_RE_HASH_PLACEHOLDER = re.compile(r"#\{(.{1,300}?)\}", re.S)
# Plain `{xxx}` but not `#{xxx}` (negative lookbehind on '#')
_RE_BRACE_PLACEHOLDER = re.compile(r"(?<!#)\{([^{}\r\n]{1,200})\}")


def _normalize_placeholder_inner(s: str) -> str:
    # In real templates, tokens may be split with newlines/spaces across runs.
    # Normalizing by removing all whitespace makes keys stable for mapping.
    return re.sub(r"\s+", "", (s or "")).strip()


def _normalize_context(context: Mapping[str, Any]) -> dict[str, str]:
    """
    Normalize context keys by stripping whitespace, and stringify values.
    """
    m: dict[str, str] = {}
    for k, v in (context or {}).items():
        kk = _normalize_placeholder_inner(str(k))
        m[kk] = "" if v is None else str(v)
    return m


@dataclass(frozen=True)
class DocxRenderStats:
    paragraphs_touched: int
    placeholders_replaced_or_cleared: int


def _iter_paragraphs_in_table(table: Table) -> Iterable[Paragraph]:
    for row in table.rows:
        for cell in row.cells:
            yield from _iter_paragraphs_in_cell(cell)


def _iter_paragraphs_in_cell(cell: _Cell) -> Iterable[Paragraph]:
    for p in cell.paragraphs:
        yield p
    for t in cell.tables:
        yield from _iter_paragraphs_in_table(t)


def _iter_all_paragraphs_in_doc(d: _DocumentType) -> Iterable[Paragraph]:
    for p in d.paragraphs:
        yield p
    for t in d.tables:
        yield from _iter_paragraphs_in_table(t)

    # headers / footers may also contain placeholders
    for sec in d.sections:
        for p in sec.header.paragraphs:
            yield p
        for t in sec.header.tables:
            yield from _iter_paragraphs_in_table(t)
        for p in sec.footer.paragraphs:
            yield p
        for t in sec.footer.tables:
            yield from _iter_paragraphs_in_table(t)


def _replace_placeholders_in_paragraphs(
    *,
    paragraphs: Iterable[Paragraph],
    context: Mapping[str, Any],
    clear_unresolved: bool,
) -> DocxRenderStats:
    ctx = _normalize_context(context)
    paragraphs_touched = 0
    placeholders_done = 0

    def replace_in_text(text: str) -> tuple[str, int]:
        changed = 0

        def repl_hash(m: re.Match[str]) -> str:
            nonlocal changed
            raw_inner = m.group(1)
            key = _normalize_placeholder_inner(raw_inner)
            if key in ctx:
                changed += 1
                return ctx[key]
            if clear_unresolved:
                changed += 1
                return ""
            return m.group(0)

        def repl_brace(m: re.Match[str]) -> str:
            nonlocal changed
            raw_inner = m.group(1)
            key = _normalize_placeholder_inner(raw_inner)
            if key in ctx:
                changed += 1
                return ctx[key]
            if clear_unresolved:
                changed += 1
                return ""
            return m.group(0)

        new_text = _RE_HASH_PLACEHOLDER.sub(repl_hash, text)
        new_text = _RE_BRACE_PLACEHOLDER.sub(repl_brace, new_text)
        return new_text, changed

    def write_back_runs_keep_styles(runs, new_text: str) -> None:
        """
        Best-effort to keep run styles:
        - Keep the same number of runs
        - Distribute new text by original run lengths (last run gets the remainder)
        """
        orig_lens = [len(r.text or "") for r in runs]
        total_len = sum(orig_lens)
        if total_len <= 0:
            runs[0].text = new_text
            for r in runs[1:]:
                r.text = ""
            return

        # If new text is shorter, later runs become empty; if longer, last run grows.
        pos = 0
        for idx, r in enumerate(runs):
            if idx == len(runs) - 1:
                r.text = new_text[pos:]
                pos = len(new_text)
            else:
                n = orig_lens[idx]
                r.text = new_text[pos : pos + n]
                pos += n
        # Any remaining runs after pos are already set (some may become "").

    def replace_in_paragraph(p: Paragraph) -> int:
        nonlocal paragraphs_touched, placeholders_done
        runs = list(p.runs)
        if not runs:
            return 0
        text = "".join(r.text or "" for r in runs)
        # quick filter to skip most paragraphs
        if "#{" not in text and "}" not in text and "{" not in text:
            return 0

        new_text, changed = replace_in_text(text)
        if changed <= 0 or new_text == text:
            return 0

        write_back_runs_keep_styles(runs, new_text)
        paragraphs_touched += 1
        placeholders_done += changed
        return changed

    for p in paragraphs:
        replace_in_paragraph(p)

    return DocxRenderStats(
        paragraphs_touched=int(paragraphs_touched),
        placeholders_replaced_or_cleared=int(placeholders_done),
    )


def extract_docx_placeholders(*, template_path: str) -> set[str]:
    """
    Extract placeholder keys from a .docx file (document + header/footer parts if present).

    Returned keys are normalized (all whitespace removed) so they can be used as mapping keys.
    """
    import zipfile
    import xml.etree.ElementTree as ET

    ns = {"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"}
    keys: set[str] = set()

    def collect_from_xml(xml_text: str) -> None:
        try:
            root = ET.fromstring(xml_text)
        except Exception:
            return
        texts = [(el.text or "") for el in root.findall(".//w:t", ns)]
        full = "".join(texts)
        for inner in _RE_HASH_PLACEHOLDER.findall(full):
            k = _normalize_placeholder_inner(inner)
            if k:
                keys.add(k)
        for inner in _RE_BRACE_PLACEHOLDER.findall(full):
            k = _normalize_placeholder_inner(inner)
            if k:
                keys.add(k)

    with zipfile.ZipFile(template_path, "r") as zf:
        for name in zf.namelist():
            if not name.startswith("word/"):
                continue
            if not name.endswith(".xml"):
                continue
            # Focus on common content parts; still safe if more exist.
            if not (
                name == "word/document.xml"
                or name.startswith("word/header")
                or name.startswith("word/footer")
            ):
                continue
            try:
                xml = zf.read(name).decode("utf-8", errors="replace")
            except Exception:
                continue
            collect_from_xml(xml)

    return keys


def render_docx_template(
    *,
    template_path: str,
    output_path: str,
    context: Mapping[str, Any],
    clear_unresolved: bool = True,
) -> DocxRenderStats:
    """
    Render a .docx template with placeholder replacement.

    - Replace `#{...}` and `{...}` tokens using `context` (key = inner token).
    - If a token is not found in context:
      - cleared to empty string when `clear_unresolved=True`
      - kept as-is when `clear_unresolved=False`

    Notes:
    - This function intentionally handles tokens split across runs by replacing on
      concatenated text and writing back to runs (first run gets full text; others cleared).
    """
    from docx import Document

    doc: _DocumentType = Document(template_path)
    stats = _replace_placeholders_in_paragraphs(
        paragraphs=_iter_all_paragraphs_in_doc(doc),
        context=context,
        clear_unresolved=bool(clear_unresolved),
    )

    doc.save(output_path)
    return stats


def render_docx_template_pages(
    *,
    template_path: str,
    output_path: str,
    pages_contexts: list[Mapping[str, Any]],
    clear_unresolved: bool = True,
) -> DocxRenderStats:
    """
    Render multiple pages from a single-page docx template by duplicating the template
    table(s) for each page and filling placeholders per-page.

    Assumption (true for this repo's `print-template.docx`):
    - The whole page layout is in the document body tables.
    - Each page uses the same placeholder names; we fill them page-by-page.
    """
    if not pages_contexts:
        raise ValueError("pages_contexts 不能为空")

    from docx import Document

    doc: _DocumentType = Document(template_path)
    template_tables = list(doc.tables)
    if not template_tables:
        raise ValueError("模板 docx 不包含任何 table，无法按页复制")

    pages_tables: list[list[Table]] = []
    # page 1 uses existing tables
    pages_tables.append(list(template_tables))

    # subsequent pages: page break + deep copy template tables
    for _i in range(2, len(pages_contexts) + 1):
        doc.add_page_break()
        new_tables: list[Table] = []
        for t in template_tables:
            new_tbl_el = copy.deepcopy(t._tbl)  # pylint: disable=protected-access
            doc._body._body.append(new_tbl_el)  # type: ignore[attr-defined]  # pylint: disable=protected-access
            new_tables.append(Table(new_tbl_el, doc._body))  # type: ignore[arg-type]
        pages_tables.append(new_tables)

    total_stats = DocxRenderStats(paragraphs_touched=0, placeholders_replaced_or_cleared=0)

    for page_idx, (ctx, tbls) in enumerate(zip(pages_contexts, pages_tables), start=1):
        page_paragraphs: list[Paragraph] = []
        for t in tbls:
            page_paragraphs.extend(list(_iter_paragraphs_in_table(t)))
        st = _replace_placeholders_in_paragraphs(
            paragraphs=page_paragraphs,
            context=ctx,
            clear_unresolved=bool(clear_unresolved),
        )
        total_stats = DocxRenderStats(
            paragraphs_touched=total_stats.paragraphs_touched + st.paragraphs_touched,
            placeholders_replaced_or_cleared=total_stats.placeholders_replaced_or_cleared + st.placeholders_replaced_or_cleared,
        )

    doc.save(output_path)
    return total_stats

