"""Word(.docx) 模板渲染：递归替换正文/表格/文本框/页眉页脚中的占位符。"""
from __future__ import annotations

from pathlib import Path

from docx import Document
from docx.oxml.ns import qn
from docx.text.paragraph import Paragraph

from app.core.config import settings
from app.core.logger import get_logger
from app.core.response import AppError
from app.services.placeholder import has_placeholder, render_text

logger = get_logger(__name__)


def _parse_placeholder_fonts(raw: str) -> dict[str, str]:
    """解析「占位符=字体」映射；多个用英文逗号分隔，如 "法人签名=Animal Chariot,日期=华文行楷"。"""
    mapping: dict[str, str] = {}
    for part in (raw or "").split(","):
        part = part.strip()
        if "=" not in part:
            continue
        key, font = part.split("=", 1)
        key, font = key.strip(), font.strip()
        if key and font:
            mapping[key] = font
    return mapping


# 占位符→字体 映射（可配置，见 .env 的 PLACEHOLDER_FONTS；需在渲染机器上安装对应字体）
_PLACEHOLDER_FONTS: dict[str, str] = _parse_placeholder_fonts(settings.placeholder_fonts)


def _font_for_text(text: str) -> str | None:
    """若文本含某配置的占位符，返回其字体；否则 None。"""
    for key, font in _PLACEHOLDER_FONTS.items():
        if key in text:
            return font
    return None


def _set_run_text(run, text: str) -> None:
    """设置 run 文本并清除黄色高亮（模板里占位符带 yellow 高亮，替换后需去掉）。"""
    run.text = text
    run.font.highlight_color = None


def _set_run_font(run, font: str) -> None:
    """设置 run 的西文字体为指定字体、中文字体为配置的兜底字体。"""
    run.font.name = font
    rPr = run._element.get_or_add_rPr()
    rFonts = rPr.get_or_add_rFonts()
    rFonts.set(qn("w:eastAsia"), settings.east_asia_font)


def _replace_paragraph(
    paragraph: Paragraph,
    values: dict,
    unresolved: list[str],
    replacements: list[int],
) -> None:
    if not has_placeholder(paragraph.text):
        return

    # 第一步：run 级替换，尽量保留原有格式
    for run in paragraph.runs:
        if has_placeholder(run.text):
            font = _font_for_text(run.text)
            res = render_text(run.text, values)
            _set_run_text(run, res.rendered)
            if font:
                _set_run_font(run, font)
            replacements[0] += res.replacements
            for key in res.unresolved:
                if key not in unresolved:
                    unresolved.append(key)

    # 第二步：占位符被 Word 拆到多个 run 时，做段落级兜底
    if has_placeholder(paragraph.text):
        font = _font_for_text(paragraph.text)
        res = render_text(paragraph.text, values)
        runs = paragraph.runs
        if runs:
            # 继承占位符 run 的字体格式：写入含占位符的 run，而非固定 runs[0]
            # 用 '{' 而非 '{{'：占位符可能被 Word 拆成两个单独的 '{' run
            target = next((r for r in runs if has_placeholder(r.text) or '{' in r.text), runs[0])
            _set_run_text(target, res.rendered)
            if font:
                _set_run_font(target, font)
            for run in runs:
                if run is not target:
                    _set_run_text(run, "")
        replacements[0] += res.replacements
        for key in res.unresolved:
            if key not in unresolved:
                unresolved.append(key)


def render_docx(template_path: str | Path, values: dict, output_path: str | Path) -> dict:
    template_path = Path(template_path)
    output_path = Path(output_path)
    if not template_path.exists():
        raise AppError(f"模板文件不存在: {template_path}")

    doc = Document(str(template_path))
    unresolved: list[str] = []
    replacements = [0]

    def walk(paragraphs):
        # 先物化成 list：_replace_paragraph 会在遍历中改 XML（删/增 w:t），
        # 直接遍历惰性生成器会导致 lxml 迭代器在改树后跳过后续节点
        for p in list(paragraphs):
            _replace_paragraph(p, values, unresolved, replacements)

    # 递归遍历正文所有段落（含文本框、嵌套表格、内容控件中的占位符）
    walk(Paragraph(p_el, None) for p_el in doc.element.body.iter(qn("w:p")))
    # 页眉/页脚同样可能存在文本框/嵌套表格，一并递归遍历
    for section in doc.sections:
        walk(Paragraph(p_el, None) for p_el in section.header._element.iter(qn("w:p")))
        walk(Paragraph(p_el, None) for p_el in section.footer._element.iter(qn("w:p")))

    output_path.parent.mkdir(parents=True, exist_ok=True)
    doc.save(str(output_path))

    if unresolved:
        logger.warning("docx 有 %d 个未替换占位符: %s", len(unresolved), unresolved)
    logger.debug("docx 替换 %d 处 -> %s", replacements[0], output_path)

    return {
        "type": "docx",
        "output": str(output_path),
        "replacements": replacements[0],
        "unresolved": unresolved,
    }
