"""平台渲染层 · 共享叶子原语(docx 三 profile + 部分 pdf 复用)。

放**真正同源、与 profile 无关**的底层件:字体 OOXML 助手、化学式下标白名单、
内联/块级 markdown 正则、表格行切分、图片路径解析。三套 docx profile
(manuscript=paper/proposal、brief)都 import 这里,**单一事实源**——
改化学式白名单 / 字体规范只动这一处,不再三处各拷一份。

历史:原先 skills/{brief,paper,proposal}/scripts/render_docx.py 各自带一份
拷贝(_CHEM_RE 三份逐字相同、易漏改)。2026-06 抽到平台层 rendering/。
"""
from __future__ import annotations

import re
from pathlib import Path

from docx.oxml import OxmlElement
from docx.oxml.ns import qn
from docx.shared import Cm, Pt


# ───────────────────────── 字体 OOXML 助手 ─────────────────────────

def set_run_fonts(run, *, cn_font: str = "宋体", en_font: str = "Times New Roman") -> None:
    """同时设置 run 的中文 (eastAsia) 和西文 (ascii/hAnsi) 字体。"""
    rPr = run._element.get_or_add_rPr()
    rFonts = rPr.find(qn("w:rFonts"))
    if rFonts is None:
        rFonts = OxmlElement("w:rFonts")
        rPr.append(rFonts)
    rFonts.set(qn("w:eastAsia"), cn_font)
    rFonts.set(qn("w:ascii"), en_font)
    rFonts.set(qn("w:hAnsi"), en_font)


def set_style_fonts(style, *, cn_font: str = "宋体", en_font: str = "Times New Roman") -> None:
    """直接给 style 写 rFonts, 基于该 style 的所有段落都继承字体。"""
    el = style.element
    rPr = el.find(qn("w:rPr"))
    if rPr is None:
        rPr = OxmlElement("w:rPr")
        el.insert(0, rPr)
    rFonts = rPr.find(qn("w:rFonts"))
    if rFonts is None:
        rFonts = OxmlElement("w:rFonts")
        rPr.append(rFonts)
    rFonts.set(qn("w:eastAsia"), cn_font)
    rFonts.set(qn("w:ascii"), en_font)
    rFonts.set(qn("w:hAnsi"), en_font)


def set_subscript(run) -> None:
    rPr = run._element.get_or_add_rPr()
    va = OxmlElement("w:vertAlign")
    va.set(qn("w:val"), "subscript")
    rPr.append(va)


# ───────────────────────── emoji 字体回退 ─────────────────────────

# 正文/标题字体是宋体/黑体/Times New Roman —— 这些字体**没有 emoji 字形**,直接把
# emoji 写进这类 run,Word 里会渲染成豆腐块/缺字(用户视角"图标缺失",踩过 task
# d9c74a5a 的今日新闻 docx)。做法:识别 emoji 码点段,单独开 run 走 Segoe UI Emoji
# (Windows Word 自带彩色 emoji 字体),其余文字仍走原中西文字体。范围只圈真正的
# emoji 常用块,不含文本箭头(2190-21FF)等易误伤的符号。
EMOJI_RE = re.compile(
    "([\U0001F300-\U0001FAFF"      # 主 emoji 区(符号图形/补充/扩展-A,含 📰🔥📊🔹)
    "\U0001F1E6-\U0001F1FF"        # 区域指示符(国旗,如 🇨🇳)
    "\U00002600-\U000027BF"        # 杂项符号 + Dingbats(☀✅✨✂ 等)
    "\U00002300-\U000023FF"        # ⌚⏰⏱ 等
    "\U00002B00-\U00002BFF"        # ⬆⭐ 等
    "\U0000FE00-\U0000FE0F"        # 变体选择符(VS16 强制 emoji 呈现,如 🏙️)
    "\U0000200D]+)"                # ZWJ(emoji 连字)
)

_EMOJI_FONT = "Segoe UI Emoji"


def add_fonted_runs(paragraph, text: str, *, size=None, cn_font: str = "宋体",
                    en_font: str = "Times New Roman", bold: bool = False,
                    italic: bool = False) -> None:
    """把 text 写成若干 run:emoji 段走 Segoe UI Emoji(否则宋体/黑体缺字形→豆腐块),
    其余段走给定中西文字体。size/bold/italic 施加到每个 run。"""
    if not text:
        return
    for part in EMOJI_RE.split(text):
        if not part:
            continue
        r = paragraph.add_run(part)
        if size is not None:
            r.font.size = size
        if bold:
            r.bold = True
        if italic:
            r.italic = True
        if EMOJI_RE.fullmatch(part):
            set_run_fonts(r, cn_font=_EMOJI_FONT, en_font=_EMOJI_FONT)
        else:
            set_run_fonts(r, cn_font=cn_font, en_font=en_font)


# ───────────────────────── 内联 markdown 切分 ─────────────────────────

# 顺序敏感:**bold** 必须先于 *italic* 匹配, 否则会被 italic 抢
INLINE_RE = re.compile(
    r"(?P<bold>\*\*(?P<bold_t>[^*\n]+?)\*\*)"
    r"|(?P<italic>(?<![\*\w])\*(?P<italic_t>[^*\n]+?)\*(?!\*))"
    r"|(?P<code>`(?P<code_t>[^`\n]+?)`)"
)


def parse_inline(text: str) -> list[tuple[str, str]]:
    """切成 (style, segment) 列表; style ∈ plain/bold/italic/code。"""
    out: list[tuple[str, str]] = []
    pos = 0
    for m in INLINE_RE.finditer(text):
        if m.start() > pos:
            out.append(("plain", text[pos:m.start()]))
        if m.group("bold"):
            out.append(("bold", m.group("bold_t")))
        elif m.group("italic"):
            out.append(("italic", m.group("italic_t")))
        elif m.group("code"):
            out.append(("code", m.group("code_t")))
        pos = m.end()
    if pos < len(text):
        out.append(("plain", text[pos:]))
    return out or [("plain", text)]


# ── 化学式下标白名单(三 profile 共用同一份;单一事实源)──
# 长的在前,\b 防误伤 LC3 / C595 / 2026;不收 Ca2+ 这类带电荷的(那是上标,白名单不收即天然避开)
CHEM_RE = re.compile(
    r"Ca\(OH\)2|Mg\(OH\)2"
    r"|\b(?:Al2O3|Fe2O3|Fe3O4|Mn2O3|Cr2O3|P2O5|Na2SO4|K2SO4|CaSO4|CaCO3|MgCO3|"
    r"CaCl2|MgCl2|Na2O|K2O|SiO2|TiO2|ZrO2|SO4|SO3|SO2|CO3|CO2|NO3|NO2|PO4|"
    r"H2O|NH3|CH4|C4AF|C3S2|C2AS|C3S|C2S|C3A|O2|N2|H2)\b"
)


# ───────────────────────── 块级行类型正则 ─────────────────────────

HEADING_RE = re.compile(r"^(#{1,6})\s+(.+)$")
TABLE_LINE_RE = re.compile(r"^\s*\|.*\|\s*$")
BLOCKQUOTE_RE = re.compile(r"^\s*>\s?")
HR_RE = re.compile(r"^\s*-{3,}\s*$|^\s*={3,}\s*$|^\s*_{3,}\s*$")
FENCE_RE = re.compile(r"^\s*(`{3,}|~{3,})\s*(\S*)\s*$")
IMAGE_LINE_RE = re.compile(r"^\s*!\[(?P<cap>[^\]]*)\]\((?P<src>[^)\s]+)\)\s*$")


def is_table_line(line: str) -> bool:
    return bool(TABLE_LINE_RE.match(line))


def is_heading(line: str) -> bool:
    return bool(HEADING_RE.match(line))


def is_blockquote(line: str) -> bool:
    return bool(BLOCKQUOTE_RE.match(line))


def is_hr(line: str) -> bool:
    return bool(HR_RE.match(line))


# ───────────────────────── 表格行切分 ─────────────────────────

def split_md_row(line: str) -> list[str]:
    return [c.strip() for c in line.strip().strip("|").split("|")]


def is_separator_row(cells: list[str]) -> bool:
    return all(re.match(r"^[-:\s]+$", c) for c in cells if c != "")


# ───────────────────────── 图片 ─────────────────────────

MAX_IMG_WIDTH = Cm(15)


def resolve_image_path(src: str, base_dir: Path) -> Path | None:
    """图片相对路径以 base_dir (单个 .md 所在目录) 为锚。"""
    p = Path(src)
    if not p.is_absolute():
        p = (base_dir / p).resolve()
    return p if p.is_file() else None


# ───────────────────────── sections 输入收集 ─────────────────────────

def collect_md_files(src: Path) -> list[Path]:
    """把 render 入口的 src 归一为 md 文件列表:单个 .md → [src],目录 → 内部
    *.md 排序。空则 [](调用方报错)。render.py 三 docx profile 与 pdf 共用同一
    "目录或单文件"契约 —— 此前 docx 侧只认目录,单文件报 `sections dir not found`,
    逼模型 mkdir sections/ 再 cp 进去兜底(踩过 task d9c74a5a),现统一放开。"""
    if src.is_file():
        return [src]
    if src.is_dir():
        return sorted(src.glob("*.md"))
    return []
