"""pptx_helpers.py — PPT skill 的共享版式工具箱。

逐页生成时**每页一个 run_python**(载入已有 .pptx → append 一页 → save),
这些 helper 以前要在每页里重新默写一遍 —— 既烧 token 又会在长 deck 里漂移
(第 7 页的 apply_brand 坐标和第 2 页写得不一样)。收进本模块后,每页只 import。

用法(在 run_python block 顶部):

    import sys; sys.path.insert(0, "<skill_dir>/scripts")  # <skill_dir> 用 system prompt 注入值
    import pptx_helpers as P

    # —— 第一页(创建)——
    prs = P.new_presentation("16:9")          # 默认 16:9,可传 4:3 / 9:16 / 3:4
    P.set_palette(spec_path="<task_dir>/...spec.md")   # 默认商务红;spec 覆盖了才需要
    slide = P.add_slide(prs)
    P.apply_brand(slide, "cover")
    P.add_textbox(slide, 0.9, 2.6, 11.9, 1.4, "标题", 44, bold=True, color=P.INK)
    prs.save("<task_dir>/<topic>.pptx")

    # —— 后续页(追加)——
    prs = P.load("<task_dir>/<topic>.pptx")   # 从文件实际尺寸回填画布常量
    P.set_palette(spec_path="<task_dir>/...spec.md")   # 每页都重读 spec(同 SKILL.md 规则)
    slide = P.add_slide(prs)
    ...
    prs.save("<task_dir>/<topic>.pptx")

⚠️ 一律用 `P.xxx` 访问颜色常量与函数 —— set_palette 靠改模块属性生效,
   `from pptx_helpers import *` 会把旧绑定拷进页面命名空间,覆盖配色不生效。
"""
from __future__ import annotations

import re
from pathlib import Path

from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR, MSO_AUTO_SIZE
from pptx.enum.shapes import MSO_SHAPE
from pptx.oxml.ns import qn

# ============================================================
# 配色 (商务红 — 硬约束默认)
# ============================================================
# ⛔ 不允许擅自换色:除非用户明确点名其它配色 或 spec 已写其它 hex,否则就是这套红。
#    要换走 set_palette(),禁止以"这场景蓝色更专业"这类自我合理化做替换。
PRIMARY = RGBColor(0xC0, 0x00, 0x00)    # 深红 - 标题/强调/关键数据
SECONDARY = RGBColor(0xE1, 0x55, 0x54)  # 砖红 - 次要图形
ACCENT = RGBColor(0xFF, 0xC1, 0x07)     # 金黄 - 关键数据点/CTA
INK = RGBColor(0x1F, 0x1F, 0x1F)
GREY = RGBColor(0x59, 0x59, 0x59)
GREY_LIGHT = RGBColor(0x88, 0x88, 0x88)
HAIRLINE = RGBColor(0xDD, 0xDD, 0xDD)   # 细分隔线
BG = RGBColor(0xFA, 0xFA, 0xFA)         # 背景近白
WHITE = RGBColor(0xFF, 0xFF, 0xFF)

CN_FONT = "微软雅黑"   # 中文字形走 <a:ea> 槽位
EN_FONT = "Arial"      # 拉丁字形走 <a:latin> 槽位

# ============================================================
# 画布与安全区 (new_presentation / load 会按实际尺寸回填这些)
# ============================================================
SLIDE_W = 13.33
SLIDE_H = 7.5
MARGIN_X = 0.7
MARGIN_Y = 0.5
SAFE_LEFT = MARGIN_X
SAFE_TOP = MARGIN_Y
SAFE_RIGHT = SLIDE_W - MARGIN_X
SAFE_BOTTOM = SLIDE_H - MARGIN_Y
SAFE_W = SAFE_RIGHT - SAFE_LEFT
SAFE_H = SAFE_BOTTOM - SAFE_TOP

_CANVAS = {
    "16:9": (13.33, 7.5),
    "4:3": (10.0, 7.5),
    "9:16": (7.5, 13.33),
    "3:4": (7.5, 10.0),
}


def _recompute_safe() -> None:
    global SAFE_LEFT, SAFE_TOP, SAFE_RIGHT, SAFE_BOTTOM, SAFE_W, SAFE_H
    SAFE_LEFT = MARGIN_X
    SAFE_TOP = MARGIN_Y
    SAFE_RIGHT = SLIDE_W - MARGIN_X
    SAFE_BOTTOM = SLIDE_H - MARGIN_Y
    SAFE_W = SAFE_RIGHT - SAFE_LEFT
    SAFE_H = SAFE_BOTTOM - SAFE_TOP


def new_presentation(canvas: str = "16:9") -> Presentation:
    """建空白 deck 并设画布尺寸,同步回填模块的安全区常量。第一页用。"""
    global SLIDE_W, SLIDE_H
    if canvas not in _CANVAS:
        raise ValueError(f"未知画布 {canvas!r},支持 {list(_CANVAS)}")
    SLIDE_W, SLIDE_H = _CANVAS[canvas]
    _recompute_safe()
    prs = Presentation()
    prs.slide_width = Inches(SLIDE_W)
    prs.slide_height = Inches(SLIDE_H)
    return prs


def load(path) -> Presentation:
    """载入已有 deck,并按文件实际尺寸回填模块画布常量(逐页进程间自动同步)。"""
    global SLIDE_W, SLIDE_H
    prs = Presentation(str(path))
    SLIDE_W = prs.slide_width / 914400
    SLIDE_H = prs.slide_height / 914400
    _recompute_safe()
    return prs


def add_slide(prs: Presentation):
    """追加一张空白版式(layout 6)的 slide。"""
    return prs.slides.add_slide(prs.slide_layouts[6])


# ============================================================
# 配色覆盖 (默认商务红;spec 写了别的色才调)
# ============================================================
def _to_rgb(h: str) -> RGBColor:
    return RGBColor.from_string(h.lstrip("#").upper())


def set_palette(primary: str | None = None, secondary: str | None = None,
                accent: str | None = None, cn_font: str | None = None,
                en_font: str | None = None, spec_path=None) -> None:
    """覆盖主题色 / 字体。逐页生成时每页都调一次(对齐 SKILL.md「每页重读 spec」)。

    - 显式传 primary/secondary/accent(hex,带不带 # 都行)即覆盖对应色。
    - 传 spec_path:从 spec.md 按文档顺序取前 3 个 #hex 作 主/辅/强调
      (spec 模板里配色行是 hex 唯一出现处)。找不到则保持商务红默认。
    - 都不传 = 维持商务红,无副作用。
    """
    global PRIMARY, SECONDARY, ACCENT, CN_FONT, EN_FONT
    if spec_path:
        p = Path(spec_path)
        if p.exists():
            hexes = re.findall(r"#([0-9A-Fa-f]{6})", p.read_text(encoding="utf-8"))
            if len(hexes) >= 1 and primary is None:
                primary = hexes[0]
            if len(hexes) >= 2 and secondary is None:
                secondary = hexes[1]
            if len(hexes) >= 3 and accent is None:
                accent = hexes[2]
    if primary:
        PRIMARY = _to_rgb(primary)
    if secondary:
        SECONDARY = _to_rgb(secondary)
    if accent:
        ACCENT = _to_rgb(accent)
    if cn_font:
        CN_FONT = cn_font
    if en_font:
        EN_FONT = en_font


# ============================================================
# 安全区校验
# ============================================================
def assert_inside(left, top, width, height, name="") -> None:
    """放置前调一次。越界直接报错而不是悄悄超出。"""
    if left < 0 or top < 0:
        raise ValueError(f"[{name}] 左/上为负: ({left}, {top})")
    if left + width > SLIDE_W + 1e-3:
        raise ValueError(f"[{name}] 右越界: {left}+{width} > {SLIDE_W}")
    if top + height > SLIDE_H + 1e-3:
        raise ValueError(f"[{name}] 下越界: {top}+{height} > {SLIDE_H}")


# ============================================================
# 文本辅助
# ============================================================
def _apply_run_font(run, size, bold, color, latin_font, ea_font) -> None:
    """设字号/粗细/颜色 + 同时设 latin(拉丁)与 ea/cs(东亚)字体。

    关键:python-pptx 的 `run.font.name = x` 只写 <a:latin>。中文字形走 <a:ea>
    槽位,不设的话会落到主题默认字体 —— 这就是「指定了微软雅黑却没真生效」的根因。
    这里 latin=英文体、ea/cs=中文体,中英混排各自命中正确字体。
    """
    run.font.size = Pt(size)
    run.font.bold = bold
    run.font.color.rgb = color
    run.font.name = latin_font  # <a:latin>
    rPr = run._r.get_or_add_rPr()
    for tag in ("a:ea", "a:cs"):
        el = rPr.find(qn(tag))
        if el is None:
            el = rPr.makeelement(qn(tag), {})
            rPr.append(el)
        el.set("typeface", ea_font)


def set_text(tf, text, size, bold=False, color=INK, align=PP_ALIGN.LEFT,
             font=None) -> None:
    """写单段文本并设样式。font=None → 拉丁 EN_FONT + 东亚 CN_FONT(推荐);
    传 font 则 latin 与 ea 都用它(纯英文大字 / 纯数字时用)。"""
    latin = font or EN_FONT
    ea = font or CN_FONT
    tf.text = text
    p = tf.paragraphs[0]
    p.alignment = align
    _apply_run_font(p.runs[0], size, bold, color, latin, ea)


def add_textbox(slide, left, top, width, height, text, size,
                bold=False, color=INK, align=PP_ALIGN.LEFT,
                anchor=MSO_ANCHOR.TOP, font=None, shrink=True,
                name="textbox"):
    """加文本框。默认 word_wrap + shrink-to-fit 兜底(不替代字数预算)。"""
    assert_inside(left, top, width, height, name)
    tb = slide.shapes.add_textbox(Inches(left), Inches(top),
                                  Inches(width), Inches(height))
    tf = tb.text_frame
    tf.vertical_anchor = anchor
    tf.word_wrap = True
    if shrink:
        tf.auto_size = MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE
    set_text(tf, text, size, bold, color, align, font)
    return tb


# ============================================================
# 形状辅助 (无边线实心填充)
# ============================================================
def add_rect(slide, left, top, width, height, fill, name="rect"):
    assert_inside(left, top, width, height, name)
    s = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, Inches(left), Inches(top),
                               Inches(width), Inches(height))
    s.fill.solid()
    s.fill.fore_color.rgb = fill
    s.line.fill.background()
    return s


def add_shape(slide, kind, left, top, width, height, fill, name="shape"):
    assert_inside(left, top, width, height, name)
    s = slide.shapes.add_shape(kind, Inches(left), Inches(top),
                               Inches(width), Inches(height))
    s.fill.solid()
    s.fill.fore_color.rgb = fill
    s.line.fill.background()
    return s


def add_dot(slide, x, y, size=0.18, color=None):
    return add_shape(slide, MSO_SHAPE.OVAL, x, y, size, size,
                     ACCENT if color is None else color, "dot")


def add_accent_line(slide, x, y, length=1.0, thickness=0.05, color=None):
    """标题下面那条强调线,替代大色块。"""
    return add_rect(slide, x, y, length, thickness,
                    ACCENT if color is None else color, "accent_line")


def add_badge(slide, x, y, num, diameter=0.7, fill=None, fg=None):
    """编号徽章 (圆 + 数字)。"""
    c = add_shape(slide, MSO_SHAPE.OVAL, x, y, diameter, diameter,
                  PRIMARY if fill is None else fill, "badge")
    tf = c.text_frame
    tf.text = str(num)
    p = tf.paragraphs[0]
    p.alignment = PP_ALIGN.CENTER
    _apply_run_font(p.runs[0], int(diameter * 28), True,
                    WHITE if fg is None else fg, EN_FONT, EN_FONT)
    return c


# ============================================================
# 标题套件 (内页通用)
# ============================================================
def page_title(slide, text, page_num=None, total=None, footer="项目汇报"):
    add_textbox(slide, SAFE_LEFT, SAFE_TOP, SAFE_W, 0.7, text,
                32, bold=True, color=PRIMARY, name="title")
    add_accent_line(slide, SAFE_LEFT, SAFE_TOP + 0.85, length=0.8)
    if page_num is not None and total is not None:
        add_textbox(slide, SAFE_LEFT, SLIDE_H - 0.5, 6, 0.4, footer,
                    11, color=GREY_LIGHT, shrink=False, name="footer")
        add_textbox(slide, SLIDE_W - 1.33, SLIDE_H - 0.5, 1.2, 0.4,
                    f"{page_num} / {total}", 11, color=GREY_LIGHT,
                    align=PP_ALIGN.RIGHT, shrink=False, name="page_num")


# ============================================================
# 品牌条 (每页起手必调,确保不是裸白纸)
# ============================================================
def apply_brand(slide, kind="inner"):
    """统一品牌锚点。每个版式第一行调用。
    cover —— 左侧主色长竖条 + 顶部短横 + 底部细灰线
    inner —— (默认) 左侧主色窄条 + 底部细灰线
    section —— 整页浅灰 + 左侧主色竖条 + 强调色粗竖条
    end —— 整页浅灰 + 顶/底强调色短线
    """
    btm = SLIDE_H - 0.32
    if kind == "cover":
        add_rect(slide, 0, 0, 0.18, SLIDE_H, PRIMARY, "brand_left_bar")
        add_rect(slide, 0.7, 0.6, 0.8, 0.06, PRIMARY, "brand_top_line")
        add_rect(slide, SAFE_LEFT, btm, SAFE_W, 0.02, HAIRLINE,
                 "brand_btm_hairline")
    elif kind == "section":
        add_rect(slide, 0, 0, SLIDE_W, SLIDE_H, BG, "brand_bg")
        add_rect(slide, 0, 0, 0.18, SLIDE_H, PRIMARY, "brand_left_bar")
        add_rect(slide, 0.7, SLIDE_H / 3, 0.08, SLIDE_H / 3, ACCENT,
                 "brand_section_bar")
    elif kind == "end":
        add_rect(slide, 0, 0, SLIDE_W, SLIDE_H, BG, "brand_bg")
        add_rect(slide, SAFE_LEFT, 0.6, 0.8, 0.06, ACCENT, "brand_top_line")
        add_rect(slide, SAFE_RIGHT - 0.8, SLIDE_H - 0.65, 0.8, 0.06, ACCENT,
                 "brand_btm_line")
    else:  # inner
        add_rect(slide, 0, 0, 0.10, SLIDE_H, PRIMARY, "brand_left_bar")
        add_rect(slide, SAFE_LEFT, btm, SAFE_W, 0.02, HAIRLINE,
                 "brand_btm_hairline")
