#!/usr/bin/env python3
"""羚牛数智中心 — 赛博朋克增强版 PPT（背景图/图示/科幻字体/动画标记）"""

from __future__ import annotations

import shutil
import zipfile
from pathlib import Path

from lxml import etree
from pptx import Presentation
from pptx.dml.color import RGBColor
from pptx.enum.shapes import MSO_SHAPE
from pptx.enum.text import PP_ALIGN
from pptx.util import Inches, Pt

BASE = Path(__file__).resolve().parent.parent
IMG = BASE / "assets" / "ppt-images"
OUT = BASE / "羚牛数智中心建设规划_赛博朋克_增强版.pptx"
OUT_DESKTOP = Path("/Users/sylvawong/Desktop/羚牛数智中心建设规划_赛博朋克_增强版.pptx")

# 科幻字体（Mac 常见；若已安装 Orbitron 会自动使用）
FONT_EN = "Orbitron"
FONT_EN_FB = "Avenir Next Demi Bold"
FONT_CN = "PingFang SC Light"
FONT_CN_FB = "Heiti SC Light"

BG_DARK = RGBColor(6, 10, 28)
BG_PANEL = RGBColor(12, 18, 48)
CYAN = RGBColor(0, 245, 255)
MAGENTA = RGBColor(255, 0, 140)
PURPLE = RGBColor(140, 80, 255)
GOLD = RGBColor(255, 210, 60)
WHITE = RGBColor(235, 245, 255)
SILVER = RGBColor(160, 180, 210)
DIM = RGBColor(90, 110, 150)
OVERLAY = RGBColor(8, 12, 32)

SLIDE_W = Inches(13.333)
SLIDE_H = Inches(7.5)

# 每页登记待动画 shape_id: {slide_index: [(shape_id, anim_type)]}
ANIM_REGISTRY: dict[int, list[tuple[int, str]]] = {}
SLIDE_BG_CYCLE = ["bg-city.png", "bg-hydrogen.png", "bg-dataflow.png", "bg-city.png"]


def font_en():
    return FONT_EN


def font_cn():
    return FONT_CN


def set_font(p, en=False, size=16, bold=False, color=SILVER):
    p.font.size = Pt(size)
    p.font.bold = bold
    p.font.color.rgb = color
    p.font.name = font_en() if en else font_cn()


def register_anim(slide_idx: int, shape_id: int, anim: str):
    ANIM_REGISTRY.setdefault(slide_idx, []).append((shape_id, anim))


def add_bg_image(slide, filename: str, overlay_alpha=0.72):
    path = IMG / filename
    if not path.exists():
        return
    pic = slide.shapes.add_picture(str(path), 0, 0, width=SLIDE_W, height=SLIDE_H)
    # 暗色蒙版提升文字可读性
    mask = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, 0, 0, SLIDE_W, SLIDE_H)
    mask.fill.solid()
    mask.fill.fore_color.rgb = OVERLAY
    mask.fill.transparency = 1.0 - overlay_alpha  # 0=不透明蒙版
    mask.line.fill.background()
    # 置底：先添加的在下，把 pic 和 mask 移到最前再 send backward - python-pptx z-order
    sp_tree = slide.shapes._spTree
    sp_tree.remove(pic._element)
    sp_tree.insert(2, pic._element)
    sp_tree.remove(mask._element)
    sp_tree.insert(3, mask._element)


def add_glow_bar(slide, left, top, width, height, color):
    sh = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, left, top, width, height)
    sh.fill.solid()
    sh.fill.fore_color.rgb = color
    sh.line.fill.background()
    return sh


def add_corner_accents(slide):
    add_glow_bar(slide, Inches(0), Inches(0), Inches(0.12), SLIDE_H, CYAN)
    add_glow_bar(slide, SLIDE_W - Inches(0.08), Inches(0), Inches(0.08), SLIDE_H, MAGENTA)


def add_header(slide, slide_idx, title, subtitle=None, anim_title="fly", anim_sub="fade"):
    add_corner_accents(slide)
    add_glow_bar(slide, Inches(0.8), Inches(1.05), Inches(3.5), Pt(4), CYAN)
    tb = slide.shapes.add_textbox(Inches(0.8), Inches(0.45), Inches(11.5), Inches(0.9))
    p = tb.text_frame.paragraphs[0]
    p.text = title
    set_font(p, en=False, size=32, bold=True, color=WHITE)
    register_anim(slide_idx, tb.shape_id, anim_title)
    if subtitle:
        tb2 = slide.shapes.add_textbox(Inches(0.8), Inches(1.12), Inches(11), Inches(0.5))
        p2 = tb2.text_frame.paragraphs[0]
        p2.text = subtitle
        set_font(p2, en=True, size=13, color=CYAN)
        register_anim(slide_idx, tb2.shape_id, anim_sub)


def add_bullets(slide, slide_idx, items, top=Inches(1.6)):
    tb = slide.shapes.add_textbox(Inches(0.9), top, Inches(11.2), Inches(5.2))
    tf = tb.text_frame
    tf.word_wrap = True
    for i, (text, accent) in enumerate(items):
        p = tf.paragraphs[0] if i == 0 else tf.add_paragraph()
        p.text = "▸  " + text
        set_font(p, size=16 if len(text) < 80 else 14, color=accent or SILVER)
        p.space_after = Pt(10)
    register_anim(slide_idx, tb.shape_id, "fade")


def add_cards(slide, slide_idx, cards, top=Inches(1.7)):
    n = len(cards)
    gap = Inches(0.25)
    w = (Inches(11.5) - gap * (n - 1)) / n
    for i, (title, body, color) in enumerate(cards):
        left = Inches(0.8) + (w + gap) * i
        panel = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, left, top, w, Inches(4.8))
        panel.fill.solid()
        panel.fill.fore_color.rgb = BG_PANEL
        panel.line.color.rgb = color
        panel.line.width = Pt(1.5)
        register_anim(slide_idx, panel.shape_id, "fade")
        tb = slide.shapes.add_textbox(left + Inches(0.2), top + Inches(0.25), w - Inches(0.4), Inches(4.3))
        tf = tb.text_frame
        tf.word_wrap = True
        p0 = tf.paragraphs[0]
        p0.text = title
        set_font(p0, en=True, size=17, bold=True, color=color)
        p1 = tf.add_paragraph()
        p1.text = body
        set_font(p1, size=12, color=SILVER)
        p1.space_before = Pt(12)


def add_flow_diagram(slide, slide_idx, labels, colors, top=Inches(2.2)):
    """三步演进图示：箭头连接圆角框"""
    n = len(labels)
    box_w = Inches(3.2)
    gap = Inches(0.55)
    start_x = Inches(0.9)
    for i, (lab, col) in enumerate(zip(labels, colors)):
        left = start_x + (box_w + gap) * i
        sh = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, left, top, box_w, Inches(1.4))
        sh.fill.solid()
        sh.fill.fore_color.rgb = BG_PANEL
        sh.line.color.rgb = col
        sh.line.width = Pt(2)
        register_anim(slide_idx, sh.shape_id, "fly")
        tb = slide.shapes.add_textbox(left, top + Inches(0.35), box_w, Inches(0.8))
        p = tb.text_frame.paragraphs[0]
        p.text = lab
        p.alignment = PP_ALIGN.CENTER
        set_font(p, size=14, bold=True, color=col)
        if i < n - 1:
            ax = left + box_w + Inches(0.08)
            arr = slide.shapes.add_shape(MSO_SHAPE.RIGHT_ARROW, ax, top + Inches(0.45), gap - Inches(0.16), Inches(0.5))
            arr.fill.solid()
            arr.fill.fore_color.rgb = col
            arr.line.fill.background()


def add_hex_diagram(slide, slide_idx, items, top=Inches(2.0)):
    """1+N+X 六边形图示"""
    n = len(items)
    w = Inches(3.5)
    gap = Inches(0.35)
    for i, (t, b, c) in enumerate(items):
        left = Inches(0.85) + (w + gap) * i
        hex_shape = slide.shapes.add_shape(MSO_SHAPE.HEXAGON, left, top, w, Inches(2.8))
        hex_shape.fill.solid()
        hex_shape.fill.fore_color.rgb = BG_PANEL
        hex_shape.line.color.rgb = c
        hex_shape.line.width = Pt(2)
        register_anim(slide_idx, hex_shape.shape_id, "fade")
        tb = slide.shapes.add_textbox(left + Inches(0.15), top + Inches(0.5), w - Inches(0.3), Inches(2))
        tf = tb.text_frame
        tf.word_wrap = True
        p0 = tf.paragraphs[0]
        p0.text = t
        p0.alignment = PP_ALIGN.CENTER
        set_font(p0, en=True, size=22, bold=True, color=c)
        p1 = tf.add_paragraph()
        p1.text = b
        p1.alignment = PP_ALIGN.CENTER
        set_font(p1, size=11, color=SILVER)
        p1.space_before = Pt(8)


def new_slide(prs, bg_file: str):
    slide = prs.slides.add_slide(prs.slide_layouts[6])
    add_bg_image(slide, bg_file, overlay_alpha=0.68)
    return slide


def slide_title(prs, idx):
    slide = new_slide(prs, "bg-city.png")
    add_corner_accents(slide)
    add_glow_bar(slide, Inches(1.2), Inches(3.85), Inches(10.8), Pt(2), MAGENTA)

    tb = slide.shapes.add_textbox(Inches(0.9), Inches(1.7), Inches(11.5), Inches(1.2))
    p = tb.text_frame.paragraphs[0]
    p.text = "羚牛数智中心"
    set_font(p, size=48, bold=True, color=CYAN)
    p.alignment = PP_ALIGN.CENTER
    register_anim(idx, tb.shape_id, "fly")

    tb2 = slide.shapes.add_textbox(Inches(0.9), Inches(2.85), Inches(11.5), Inches(0.8))
    p2 = tb2.text_frame.paragraphs[0]
    p2.text = "建设规划报告"
    set_font(p2, size=34, color=WHITE)
    p2.alignment = PP_ALIGN.CENTER
    register_anim(idx, tb2.shape_id, "fade")

    tb3 = slide.shapes.add_textbox(Inches(0.9), Inches(4.1), Inches(11.5), Inches(0.6))
    p3 = tb3.text_frame.paragraphs[0]
    p3.text = "LINGNIU DIGITAL INTELLIGENCE CENTER  ·  V1.0"
    set_font(p3, en=True, size=13, color=MAGENTA)
    p3.alignment = PP_ALIGN.CENTER
    register_anim(idx, tb3.shape_id, "fade")

    tb4 = slide.shapes.add_textbox(Inches(0.9), Inches(5.0), Inches(11.5), Inches(0.8))
    p4 = tb4.text_frame.paragraphs[0]
    p4.text = "羚牛氢能 One OS  ·  氢能交通商业操作系统"
    set_font(p4, size=16, color=GOLD)
    p4.alignment = PP_ALIGN.CENTER
    register_anim(idx, tb4.shape_id, "fade")


def slide_architecture(prs, idx):
    slide = new_slide(prs, "bg-dataflow.png")
    add_header(slide, idx, "羚牛氢能 One OS  ·  架构示意图", "SYSTEM ARCHITECTURE", "fly", "fade")
    arch = IMG / "architecture.png"
    if arch.exists():
        pic = slide.shapes.add_picture(
            str(arch), Inches(0.55), Inches(1.45), width=Inches(12.2), height=Inches(5.7)
        )
        register_anim(idx, pic.shape_id, "fade")
    # 叠加矢量层：核心模块框
    layers = [
        ("应用层", "小羚羚 · 运营终端 · 客户服务", CYAN, Inches(1.0), Inches(1.55)),
        ("平台层", "任务调度 · 能源管理 · 资产视图", MAGENTA, Inches(4.6), Inches(1.55)),
        ("数据层", "主数据 · 指标仓 · AI 特征", PURPLE, Inches(8.2), Inches(1.55)),
        ("生态层", "API · SaaS · 产业开放", GOLD, Inches(4.6), Inches(5.85)),
    ]
    for title, sub, col, left, top in layers:
        box = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, left, top, Inches(3.8), Inches(0.95))
        box.fill.solid()
        box.fill.fore_color.rgb = BG_PANEL
        box.line.color.rgb = col
        box.line.width = Pt(1.5)
        register_anim(idx, box.shape_id, "fly")
        tb = slide.shapes.add_textbox(left + Inches(0.1), top + Inches(0.08), Inches(3.6), Inches(0.8))
        tf = tb.text_frame
        p0 = tf.paragraphs[0]
        p0.text = title
        set_font(p0, en=True, size=12, bold=True, color=col)
        p1 = tf.add_paragraph()
        p1.text = sub
        set_font(p1, size=9, color=SILVER)


def build_slides(prs):
    si = 0
    slide_title(prs, si)
    si += 1

    s = new_slide(prs, SLIDE_BG_CYCLE[si % 4])
    add_header(s, si, "一、报告背景与目标", "BACKGROUND & OBJECTIVES")
    add_bullets(
        s,
        si,
        [
            ("业务规模扩大，人工台账与分散系统难以为继", None),
            ("数据分散 · 流程依赖人工 · 单点建设难复用", MAGENTA),
            ("建设统一「数智中心」是战略发展的必然选择", CYAN),
        ],
    )
    si += 1

    s = new_slide(prs, SLIDE_BG_CYCLE[si % 4])
    add_header(s, si, "现状痛点", "CURRENT CHALLENGES")
    add_cards(
        s,
        si,
        [
            ("数据孤岛", "难以统一视图", CYAN),
            ("流程非标", "可追溯性不足", MAGENTA),
            ("重复建设", "难以复用扩展", PURPLE),
            ("复杂场景", "多能源难支撑", GOLD),
        ],
    )
    si += 1

    s = new_slide(prs, SLIDE_BG_CYCLE[si % 4])
    add_header(s, si, "要解决的问题", "PROBLEMS TO SOLVE")
    add_cards(
        s,
        si,
        [
            ("当前必须解决", "手工台账 · 系统割裂\n任务依赖人工 · 缺乏数据支撑", CYAN),
            ("未来必须应对", "人效下降 · 复杂度上升\n平台不可持续 · AI 难落地", MAGENTA),
        ],
    )
    si += 1

    s = new_slide(prs, SLIDE_BG_CYCLE[si % 4])
    add_header(s, si, "四大建设目标", "CORE OBJECTIVES")
    add_cards(
        s,
        si,
        [
            ("稳定运行", "支撑当前业务", CYAN),
            ("持续演进", "支持5年发展", MAGENTA),
            ("数据资产", "可沉淀可变现", PURPLE),
            ("对内对外", "降本+SaaS输出", GOLD),
        ],
    )
    si += 1

    s = new_slide(prs, SLIDE_BG_CYCLE[si % 4])
    add_header(s, si, "二、战略定位", "STRATEGIC POSITIONING")
    add_bullets(
        s,
        si,
        [
            ("数智中心：未来3–5年核心数字基础设施", CYAN),
            ("长期战略资产，非单一IT项目", WHITE),
            ("羚牛氢能 One OS — 氢能交通商业操作系统", GOLD),
            ("核心产品：小羚羚统一运营与服务终端", MAGENTA),
        ],
    )
    si += 1

    slide_architecture(prs, si)
    si += 1

    s = new_slide(prs, SLIDE_BG_CYCLE[si % 4])
    add_header(s, si, "战略阶段：三步走", "THREE-PHASE ROADMAP")
    add_flow_diagram(
        s,
        si,
        ["Phase 01\n统一·标准·稳定", "Phase 02\n平台·复用·协同", "Phase 03\n智能·开放·商业"],
        [CYAN, MAGENTA, GOLD],
    )
    si += 1

    for title, sub, bullets in [
        (
            "Phase 01  ·  统一 · 标准 · 稳定",
            "FOUNDATION",
            [
                ("业务在线、数据留痕、流程可控", CYAN),
                ("统一运营平台与小羚羚终端", None),
                ("标准化数据、流程、接口", None),
                ("高可用安全合规", None),
            ],
        ),
        (
            "Phase 02  ·  平台化 · 复用 · 协同",
            "PLATFORM",
            [
                ("从有数据到用数据", MAGENTA),
                ("平台化抽象通用能力", None),
                ("复用降低边际成本", None),
                ("跨部门协同打通全链路", GOLD),
            ],
        ),
        (
            "Phase 03  ·  智能化 · 开放 · 商业化",
            "INTELLIGENCE",
            [
                ("AI驱动绿色智能生态", GOLD),
                ("智能化自主优化", None),
                ("开放API与SaaS", None),
                ("成本中心→利润中心", CYAN),
            ],
        ),
    ]:
        s = new_slide(prs, SLIDE_BG_CYCLE[si % 4])
        add_header(s, si, title, sub)
        add_bullets(s, si, bullets)
        si += 1

    s = new_slide(prs, SLIDE_BG_CYCLE[si % 4])
    add_header(s, si, "演进逻辑", "EVOLUTION LOGIC")
    add_flow_diagram(s, si, ["信息化\n0 → 1", "数字化\n1 → N", "智能化\nN → ∞"], [CYAN, MAGENTA, GOLD], top=Inches(2.4))
    tb = s.shapes.add_textbox(Inches(0.8), Inches(5.4), Inches(11.5), Inches(0.8))
    p = tb.text_frame.paragraphs[0]
    p.text = "成本中心  ──▶  利润中心  ·  驱动绿色交通变革"
    set_font(p, size=18, color=WHITE)
    p.alignment = PP_ALIGN.CENTER
    register_anim(si, tb.shape_id, "fade")
    si += 1

    s = new_slide(prs, SLIDE_BG_CYCLE[si % 4])
    add_header(s, si, "三、整体建设方案", "IMPLEMENTATION")
    add_bullets(
        s,
        si,
        [
            ("分阶段实施，按业务节奏推进", CYAN),
            ("基础优先：系统能力先于智能", None),
            ("核心内控：需求架构数据内控", None),
            ("敏捷迭代：MVP快速验证", None),
        ],
    )
    si += 1

    s = new_slide(prs, SLIDE_BG_CYCLE[si % 4])
    add_header(s, si, "「1 + N + X」核心理念", "1 + N + X")
    add_hex_diagram(
        s,
        si,
        [
            ("1", "车辆/设备/能源\n唯一主键", CYAN),
            ("N", "电·氢·充换氢\n多能源", MAGENTA),
            ("X", "运营·物流·金融\nESG·调度", GOLD),
        ],
    )
    si += 1

    s = new_slide(prs, "bg-hydrogen.png")
    add_corner_accents(s)
    add_glow_bar(s, Inches(2), Inches(3.2), Inches(9.3), Pt(3), CYAN)
    tb = s.shapes.add_textbox(Inches(0.9), Inches(2.5), Inches(11.5), Inches(1.5))
    p = tb.text_frame.paragraphs[0]
    p.text = "羚牛氢能 One OS"
    set_font(p, size=44, bold=True, color=CYAN)
    p.alignment = PP_ALIGN.CENTER
    register_anim(si, tb.shape_id, "fly")
    tb2 = s.shapes.add_textbox(Inches(0.9), Inches(3.8), Inches(11.5), Inches(1))
    p2 = tb2.text_frame.paragraphs[0]
    p2.text = "连接产业各方  ·  驱动绿色交通变革"
    set_font(p2, size=20, color=WHITE)
    p2.alignment = PP_ALIGN.CENTER
    register_anim(si, tb2.shape_id, "fade")


# ---------- 动画注入（OOXML）----------

NSMAP = {
    "p": "http://schemas.openxmlformats.org/presentationml/2006/main",
    "a": "http://schemas.openxmlformats.org/drawingml/2006/main",
}


def _timing_xml(shape_id: int, preset_id: int, preset_subtype: int, dur_ms: int) -> etree._Element:
    """生成单 shape 入场动画 timing 片段（fade preset 10, fly preset 2）"""
    # 简化：使用 preset 动画
    return etree.fromstring(
        f"""
    <p:par xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
           xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
      <p:cTn id="{shape_id + 1000}" presetID="{preset_id}" presetClass="entr"
             presetSubtype="{preset_subtype}" fill="hold" nodeType="clickEffect">
        <p:stCondLst><p:cond delay="0"/></p:stCondLst>
        <p:childTnLst>
          <p:set>
            <p:cBhvr>
              <p:cTn id="{shape_id + 2000}" dur="1" fill="hold">
                <p:stCondLst><p:cond delay="0"/></p:stCondLst>
              </p:cTn>
              <p:tgtEl><p:spTgt spid="{shape_id}"/></p:tgtEl>
              <p:attributeNameLst><p:attributeName>style.visibility</p:attributeName></p:attributeNameLst>
            </p:cBhvr>
            <p:to><p:strVal val="visible"/></p:to>
          </p:set>
          <p:animEffect transition="in" filter="fade({dur_ms})">
            <p:cBhvr>
              <p:cTn id="{shape_id + 3000}" dur="{dur_ms}"/>
              <p:tgtEl><p:spTgt spid="{shape_id}"/></p:tgtEl>
            </p:cBhvr>
          </p:animEffect>
        </p:childTnLst>
      </p:cTn>
    </p:par>
    """.encode()
    )


def inject_animations(pptx_path: Path, registry: dict[int, list[tuple[int, str]]]):
    """向 pptx 注入基础淡入/飞入动画（PowerPoint 2016+）"""
    tmp = pptx_path.parent / "_pptx_anim_tmp"
    if tmp.exists():
        shutil.rmtree(tmp)
    with zipfile.ZipFile(pptx_path, "r") as z:
        z.extractall(tmp)

    slides_dir = tmp / "ppt" / "slides"
    slide_files = sorted(slides_dir.glob("slide*.xml"), key=lambda p: int(p.stem.replace("slide", "")))

    for slide_idx, anims in registry.items():
        if slide_idx >= len(slide_files):
            continue
        slide_path = slide_files[slide_idx]
        tree = etree.parse(str(slide_path))
        root = tree.getroot()

        # 移除已有 timing
        for old in root.findall("p:timing", NSMAP):
            root.remove(old)

        child_tn_lst = etree.Element("{http://schemas.openxmlformats.org/presentationml/2006/main}childTnLst")
        for spid, anim_type in anims:
            if anim_type == "fly":
                # fly from bottom: presetID 2 subtype 4
                node = _timing_xml(spid, 2, 4, 500)
            else:
                node = _timing_xml(spid, 10, 0, 400)
            child_tn_lst.append(node)

        timing = etree.Element("{http://schemas.openxmlformats.org/presentationml/2006/main}timing")
        tn_lst = etree.SubElement(timing, "{http://schemas.openxmlformats.org/presentationml/2006/main}tnLst")
        par = etree.SubElement(tn_lst, "{http://schemas.openxmlformats.org/presentationml/2006/main}par")
        ctn = etree.SubElement(
            par,
            "{http://schemas.openxmlformats.org/presentationml/2006/main}cTn",
            id="1",
            dur="indefinite",
            restart="never",
            nodeType="tmRoot",
        )
        ctn_child = etree.SubElement(
            ctn, "{http://schemas.openxmlformats.org/presentationml/2006/main}childTnLst"
        )
        seq = etree.SubElement(
            ctn_child,
            "{http://schemas.openxmlformats.org/presentationml/2006/main}seq",
            concurrent="1",
            nextAc="seek",
        )
        seq_ctn = etree.SubElement(
            seq,
            "{http://schemas.openxmlformats.org/presentationml/2006/main}cTn",
            id="2",
            dur="indefinite",
            nodeType="mainSeq",
        )
        seq_child = etree.SubElement(
            seq_ctn, "{http://schemas.openxmlformats.org/presentationml/2006/main}childTnLst"
        )
        for child in child_tn_lst:
            wrap = etree.SubElement(
                seq_child, "{http://schemas.openxmlformats.org/presentationml/2006/main}par"
            )
            wrap_ctn = etree.SubElement(
                wrap,
                "{http://schemas.openxmlformats.org/presentationml/2006/main}cTn",
                fill="hold",
            )
            wrap_child = etree.SubElement(
                wrap_ctn,
                "{http://schemas.openxmlformats.org/presentationml/2006/main}childTnLst",
            )
            wrap_child.append(child)

        root.append(timing)
        tree.write(str(slide_path), xml_declaration=True, encoding="UTF-8", standalone=True)

    out_zip = pptx_path.parent / "_pptx_anim_out.pptx"
    if out_zip.exists():
        out_zip.unlink()
    with zipfile.ZipFile(out_zip, "w", zipfile.ZIP_DEFLATED) as zout:
        for fp in tmp.rglob("*"):
            if fp.is_file():
                zout.write(fp, fp.relative_to(tmp))
    shutil.move(str(out_zip), str(pptx_path))
    shutil.rmtree(tmp, ignore_errors=True)


def main():
    if not IMG.exists():
        raise SystemExit(f"缺少图片资源目录: {IMG}")

    prs = Presentation()
    prs.slide_width = SLIDE_W
    prs.slide_height = SLIDE_H
    build_slides(prs)
    prs.save(OUT)

    try:
        inject_animations(OUT, ANIM_REGISTRY)
        anim_note = "已注入标题飞入/内容淡入动画"
    except Exception as e:
        anim_note = f"动画注入部分失败({e})，可在PPT中全选标题批量添加「飞入/淡入」"

    shutil.copy(OUT, OUT_DESKTOP)
    print(f"已生成: {OUT}")
    print(f"已复制: {OUT_DESKTOP}")
    print(f"共 {len(prs.slides)} 页 | {anim_note}")
    print(f"字体: 英文 {font_en()} / 中文 {font_cn()}（若 Orbitron 未安装将回退系统字体）")


if __name__ == "__main__":
    main()
