"""OOXML 字体解析：处理样式继承链 + 主题字体。

Word 把字体属性分散在四个层级：
1. 直接 run rPr：`<w:r><w:rPr><w:rFonts/></w:rPr>...`
2. 段落 rPr（段落标记字体）：`<w:p><w:pPr><w:rPr><w:rFonts/></w:rPr></w:pPr>`
3. 段落引用样式：`<w:p><w:pPr><w:pStyle w:val="Heading1"/></w:pPr>`
   样式定义在 styles.xml，可经 `<w:basedOn>` 链向上继承
4. 全局默认：styles.xml 的 `<w:docDefaults>`

此外 `<w:rFonts>` 的 `*Theme` 属性指向 theme1.xml 中的字体方案
（majorEastAsia / minorEastAsia 等），需要做二次解析。
"""

from __future__ import annotations
from dataclasses import dataclass
from docx.oxml.ns import qn
from lxml import etree

# theme1.xml 命名空间
_DML_NS = "http://schemas.openxmlformats.org/drawingml/2006/main"


@dataclass
class ResolvedRunStyle:
    font_eastasia: str | None = None
    font_ascii: str | None = None
    size_pt: float | None = None
    bold: bool | None = None
    italic: bool | None = None


def _empty_to_none(s: str | None) -> str | None:
    if s is None:
        return None
    s = s.strip()
    return s or None


class StyleResolver:
    """构造时一次性解析样式表 + 主题；之后 resolve_run() 是 O(链长)。"""

    def __init__(self, docx):
        self._theme = self._load_theme(docx)
        self._styles, self._doc_defaults = self._load_styles(docx)

    # ---- 主题 ---------------------------------------------------------
    def _load_theme(self, docx) -> dict[tuple[str, str], str | None]:
        """返回 {(axis, scheme_attr): font_name}。

        scheme_attr 形如 'majorEastAsia' / 'minorAscii'，axis 是 rFonts 的轴。
        """
        out: dict[tuple[str, str], str | None] = {}
        try:
            theme_part = next(
                p for p in docx.part.package.parts
                if p.partname.endswith("/theme/theme1.xml")
            )
        except StopIteration:
            return out
        try:
            root = etree.fromstring(theme_part.blob)
        except etree.XMLSyntaxError:
            return out

        ns = {"a": _DML_NS}
        for kind, font_tag in (("major", "majorFont"), ("minor", "minorFont")):
            font_elem = root.find(f".//a:fontScheme/a:{font_tag}", ns)
            if font_elem is None:
                continue
            latin = font_elem.find("a:latin", ns)
            ea = font_elem.find("a:ea", ns)
            cs = font_elem.find("a:cs", ns)
            # ea 为空时用简中 Hans 兜底
            ea_val = _empty_to_none(ea.get("typeface")) if ea is not None else None
            if ea_val is None:
                hans = font_elem.find('a:font[@script="Hans"]', ns)
                if hans is not None:
                    ea_val = _empty_to_none(hans.get("typeface"))
            latin_val = _empty_to_none(latin.get("typeface")) if latin is not None else None
            cs_val = _empty_to_none(cs.get("typeface")) if cs is not None else None

            out[("ascii", f"{kind}Ascii")] = latin_val
            out[("ascii", f"{kind}HAnsi")] = latin_val  # asciiTheme=majorHAnsi 也可能出现
            out[("hAnsi", f"{kind}HAnsi")] = latin_val
            out[("hAnsi", f"{kind}Ascii")] = latin_val
            out[("eastAsia", f"{kind}EastAsia")] = ea_val
            out[("cs", f"{kind}Bidi")] = cs_val
        return out

    # ---- 样式表 -------------------------------------------------------
    def _load_styles(
        self, docx
    ) -> tuple[dict[str, dict], ResolvedRunStyle | None]:
        out: dict[str, dict] = {}
        defaults: ResolvedRunStyle | None = None
        try:
            styles_root = docx.part._styles_part.element
        except (AttributeError, KeyError):
            return out, defaults
        if styles_root is None:
            return out, defaults

        # docDefaults
        ddef = styles_root.find(qn("w:docDefaults"))
        if ddef is not None:
            rdef = ddef.find(qn("w:rPrDefault"))
            if rdef is not None:
                defaults = self._read_rpr(rdef.find(qn("w:rPr")))

        # 各 style
        for style in styles_root.findall(qn("w:style")):
            sid = style.get(qn("w:styleId"))
            if not sid:
                continue
            rpr = style.find(qn("w:rPr"))
            ppr = style.find(qn("w:pPr"))
            ppr_rpr = ppr.find(qn("w:rPr")) if ppr is not None else None
            based_on = None
            bo = style.find(qn("w:basedOn"))
            if bo is not None:
                based_on = bo.get(qn("w:val"))
            link = style.find(qn("w:link"))
            link_id = link.get(qn("w:val")) if link is not None else None
            out[sid] = {
                "rpr": rpr,
                "ppr_rpr": ppr_rpr,
                "based_on": based_on,
                "link": link_id,
            }
        return out, defaults

    # ---- 读 rPr -------------------------------------------------------
    def _read_rpr(self, rpr) -> ResolvedRunStyle | None:
        if rpr is None:
            return None
        rs = ResolvedRunStyle()
        rfonts = rpr.find(qn("w:rFonts"))
        if rfonts is not None:
            rs.font_eastasia = self._resolve_font_axis(rfonts, "eastAsia")
            rs.font_ascii = self._resolve_font_axis(rfonts, "ascii")
        sz = rpr.find(qn("w:sz"))
        if sz is not None and sz.get(qn("w:val")):
            try:
                rs.size_pt = float(sz.get(qn("w:val"))) / 2.0
            except ValueError:
                pass
        if rpr.find(qn("w:b")) is not None:
            rs.bold = True
        if rpr.find(qn("w:i")) is not None:
            rs.italic = True
        return rs

    def _resolve_font_axis(self, rfonts, axis: str) -> str | None:
        """同一根 rFonts 上 explicit > theme。"""
        explicit = _empty_to_none(rfonts.get(qn(f"w:{axis}")))
        if explicit:
            return explicit
        theme_attr = "cstheme" if axis == "cs" else f"{axis}Theme"
        theme = _empty_to_none(rfonts.get(qn(f"w:{theme_attr}")))
        if theme:
            return self._theme.get((axis, theme))
        return None

    # ---- 合并 ---------------------------------------------------------
    @staticmethod
    def _fill(target: ResolvedRunStyle, source: ResolvedRunStyle | None) -> None:
        """target 已有的字段保留；缺的从 source 取。"""
        if source is None:
            return
        if target.font_eastasia is None:
            target.font_eastasia = source.font_eastasia
        if target.font_ascii is None:
            target.font_ascii = source.font_ascii
        if target.size_pt is None:
            target.size_pt = source.size_pt
        if target.bold is None:
            target.bold = source.bold
        if target.italic is None:
            target.italic = source.italic

    def _resolve_style_chain(
        self, sid: str | None, _seen: set[str] | None = None
    ) -> ResolvedRunStyle | None:
        """段落样式 → 链向 basedOn → 沿途累积 rPr 与 pPr 的 rPr。"""
        if sid is None:
            return None
        seen = _seen or set()
        if sid in seen:
            return None
        seen = seen | {sid}
        info = self._styles.get(sid)
        if info is None:
            return None
        # 当前 style 的两个 rPr
        rs = ResolvedRunStyle()
        self._fill(rs, self._read_rpr(info.get("rpr")))
        self._fill(rs, self._read_rpr(info.get("ppr_rpr")))
        # 链接的 character style（如果有）
        if info.get("link"):
            self._fill(rs, self._resolve_style_chain(info["link"], seen))
        # 父样式
        if info.get("based_on"):
            self._fill(rs, self._resolve_style_chain(info["based_on"], seen))
        return rs

    # ---- 主入口 -------------------------------------------------------
    def resolve_run(self, p_elem, run_elem) -> ResolvedRunStyle:
        """解析单个 run 的最终样式。p_elem 可为 None。"""
        rs = ResolvedRunStyle()
        # 1. 直接 run rPr
        if run_elem is not None:
            self._fill(rs, self._read_rpr(run_elem.find(qn("w:rPr"))))
        # 2. 段落 rPr（段落标记字体）+ pStyle 链
        if p_elem is not None:
            ppr = p_elem.find(qn("w:pPr"))
            if ppr is not None:
                self._fill(rs, self._read_rpr(ppr.find(qn("w:rPr"))))
                pstyle = ppr.find(qn("w:pStyle"))
                if pstyle is not None and pstyle.get(qn("w:val")):
                    self._fill(rs, self._resolve_style_chain(pstyle.get(qn("w:val"))))
        # 3. 默认 style "Normal"（中文文档常见）
        if "Normal" in self._styles:
            self._fill(rs, self._resolve_style_chain("Normal"))
        # 4. docDefaults
        self._fill(rs, self._doc_defaults)
        return rs

    def resolve_paragraph(self, p_elem) -> ResolvedRunStyle:
        """段落整体样式（不读 run，仅 pPr/style/默认）。"""
        rs = ResolvedRunStyle()
        if p_elem is not None:
            ppr = p_elem.find(qn("w:pPr"))
            if ppr is not None:
                self._fill(rs, self._read_rpr(ppr.find(qn("w:rPr"))))
                pstyle = ppr.find(qn("w:pStyle"))
                if pstyle is not None and pstyle.get(qn("w:val")):
                    self._fill(rs, self._resolve_style_chain(pstyle.get(qn("w:val"))))
        if "Normal" in self._styles:
            self._fill(rs, self._resolve_style_chain("Normal"))
        self._fill(rs, self._doc_defaults)
        return rs
