from __future__ import annotations

import csv
import html
import json
import textwrap
from dataclasses import dataclass
from pathlib import Path

from openpyxl import load_workbook
from PIL import Image, ImageDraw, ImageFont
from pypdf import PdfReader

from docx import Document
from docx.enum.section import WD_ORIENTATION
from docx.enum.table import WD_ALIGN_VERTICAL, WD_TABLE_ALIGNMENT
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
from docx.shared import Inches, Pt, RGBColor


ROOT = Path(__file__).resolve().parents[1]
XLSX = ROOT / "source-docs" / "周报0605.xlsx"
PDF = ROOT / "source-docs" / "纪要_产品标准化与AI赋能周会纪要.pdf"
IMAGE_MANIFEST = ROOT / "extracted" / "image-manifest.json"
EXTRACTED = ROOT / "extracted"
CHARTS = ROOT / "charts"
DOCX_DIR = ROOT / "docx"
HTML_DIR = ROOT / "html"
DOCX_READY = ROOT / "images" / "docx-ready"
REPORT_MD = ROOT / "product-standardization-ai-weekly-report.md"
SUMMARY_MD = ROOT / "summary.md"
SOURCE_INDEX = ROOT / "source-index.csv"
HTML_REPORT = HTML_DIR / "product-standardization-ai-weekly-report.html"
DOCX_REPORT = DOCX_DIR / "产品标准化与AI赋能项目周报_图文版_20260605.docx"


BLUE = "1f4e79"
NAVY = "0f2437"
TEAL = "0f766e"
GREEN = "2e7d32"
ORANGE = "f97316"
RED = "b91c1c"
GRAY = "64748b"
LIGHT_BLUE = "eaf3ff"
LIGHT_GREEN = "ecfdf5"
LIGHT_ORANGE = "fff7ed"
LIGHT_RED = "fef2f2"
LIGHT_GRAY = "f8fafc"


@dataclass
class Figure:
    key: str
    path: Path
    caption: str
    group: str
    width_in: float = 6.3


def ensure_dirs() -> None:
    for path in [EXTRACTED, CHARTS, DOCX_DIR, HTML_DIR, DOCX_READY]:
        path.mkdir(parents=True, exist_ok=True)


def font(size: int, bold: bool = False) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
    candidates = [
        "/System/Library/Fonts/PingFang.ttc",
        "/System/Library/Fonts/Supplemental/Arial Unicode.ttf",
        "/System/Library/Fonts/Helvetica.ttc",
    ]
    for candidate in candidates:
        if Path(candidate).exists():
            return ImageFont.truetype(candidate, size=size, index=0)
    return ImageFont.load_default()


def wrap_cn(text: str, max_chars: int) -> list[str]:
    lines: list[str] = []
    for para in text.split("\n"):
        para = para.strip()
        if not para:
            lines.append("")
            continue
        current = ""
        for ch in para:
            current += ch
            if len(current) >= max_chars:
                lines.append(current)
                current = ""
        if current:
            lines.append(current)
    return lines


def draw_multiline(draw: ImageDraw.ImageDraw, xy: tuple[int, int], text: str, fnt, fill: str, max_chars: int, line_h: int) -> int:
    if fill and not fill.startswith("#") and len(fill) in {6, 8}:
        fill = f"#{fill}"
    x, y = xy
    for line in wrap_cn(text, max_chars):
        draw.text((x, y), line, font=fnt, fill=fill)
        y += line_h
    return y


def rounded_card(draw: ImageDraw.ImageDraw, box: tuple[int, int, int, int], fill: str, outline: str = "#dbe4ef", radius: int = 28) -> None:
    if not fill.startswith("#"):
        fill = f"#{fill}"
    if not outline.startswith("#"):
        outline = f"#{outline}"
    draw.rounded_rectangle(box, radius=radius, fill=fill, outline=outline, width=2)


def make_exec_summary_chart(path: Path) -> None:
    img = Image.new("RGB", (1800, 1000), "#ffffff")
    d = ImageDraw.Draw(img)
    d.rounded_rectangle((0, 0, 1800, 1000), radius=0, fill="#ffffff")
    d.rectangle((0, 0, 1800, 128), fill=f"#{NAVY}")
    d.text((70, 38), "周报新口径：从“做了很多事”到“做成一套体系”", font=font(42), fill="white")
    d.text((70, 102), "2026.06.01-06.07 | 产品规划 + 项目 SOP + AI 赋能 + 标准化沉淀 + Before/After", font=font(22), fill="#cbd5e1")

    cards = [
        ("产品规划", "把国信、教委端、硬件矩阵、自动化测试等事项放进产品/模块规划表。", LIGHT_BLUE, BLUE),
        ("项目 SOP", "把项目写清楚处于售前、交付、售后或客户确认、开发、上线、验收阶段。", LIGHT_GREEN, TEAL),
        ("AI 赋能", "AI 不只写材料，还要生成需求、测试、报告、视频、手册并进入协作流程。", LIGHT_ORANGE, ORANGE),
        ("资产复用", "一次业务流程沉淀成 SOP、测试用例、自动化脚本、操作视频、用户手册和 FAQ。", LIGHT_RED, RED),
    ]
    x0, y0, w, h = 70, 190, 390, 255
    for i, (title, body, fill, color) in enumerate(cards):
        x = x0 + i * 430
        rounded_card(d, (x, y0, x + w, y0 + h), fill)
        d.text((x + 28, y0 + 28), title, font=font(32), fill=f"#{color}")
        draw_multiline(d, (x + 28, y0 + 92), body, font(25), "#263238", 14, 42)

    # Bottom flow
    steps = [
        ("产品层", "属于哪个产品/模块"),
        ("项目层", "处于哪个 SOP 阶段"),
        ("AI 层", "AI 说话/干事"),
        ("沉淀层", "形成什么资产"),
        ("价值层", "价值变化"),
    ]
    y = 600
    d.text((70, 545), "五层分解：先分层，再讲事；先讲阶段，再讲动作；先讲结果，再讲价值", font=font(31), fill=f"#{NAVY}")
    for i, (title, sub) in enumerate(steps):
        x = 90 + i * 335
        d.rounded_rectangle((x, y, x + 250, y + 155), radius=26, fill="#f8fafc", outline="#b6c6d9", width=2)
        d.ellipse((x + 20, y + 28, x + 72, y + 80), fill=f"#{BLUE if i % 2 == 0 else TEAL}")
        d.text((x + 37, y + 39), str(i + 1), font=font(24), fill="white")
        d.text((x + 90, y + 32), title, font=font(28), fill=f"#{NAVY}")
        draw_multiline(d, (x + 90, y + 82), sub, font(21), f"#{GRAY}", 9, 28)
        if i < len(steps) - 1:
            d.line((x + 255, y + 78, x + 310, y + 78), fill=f"#{GRAY}", width=5)
            d.polygon([(x + 310, y + 78), (x + 288, y + 66), (x + 288, y + 90)], fill=f"#{GRAY}")

    d.text((70, 885), "证据边界：本报告使用内部周报、会议纪要与用户明确教学；不新增未验证收入、毛利、回款或客户反馈数据。", font=font(24), fill=f"#{GRAY}")
    img.save(path, quality=95)


def make_project_board_chart(path: Path) -> None:
    img = Image.new("RGB", (1800, 1100), "#ffffff")
    d = ImageDraw.Draw(img)
    d.text((60, 50), "SOP + 产品规划表：把项目状态讲清楚", font=font(46), fill=f"#{NAVY}")
    d.text((60, 112), "周报主骨架不是“推进中”，而是“当前阶段 -> 本周产物 -> 下周交付 -> 月底目标/复用价值”。", font=font(23), fill=f"#{GRAY}")
    headers = ["产品 / 模块", "SOP 阶段", "本周产物", "下周交付", "月底目标 / 沉淀价值"]
    widths = [290, 270, 380, 360, 390]
    x0, y0 = 55, 185
    row_h = 150
    d.rounded_rectangle((x0, y0, x0 + sum(widths), y0 + 72), radius=20, fill=f"#{NAVY}")
    xx = x0
    for h, w in zip(headers, widths):
        d.text((xx + 18, y0 + 20), h, font=font(23), fill="white")
        xx += w
    rows = [
        ("江苏国信", "交付阶段：客户确认 / 开发准备", "H5/PC 原型；报餐、订餐、招待权限、核销扣款链路", "确认子部门、代报限制，进入可启动任务开发", "形成机关/国企招待餐与预约就餐标准样板"),
        ("教委 / 学校端", "产品阶段：PRD 拆解 / 排期", "PC 端八板块方向明确，准备补齐云效任务", "与开发对齐估时、样式和上线计划", "沉淀学校/教委监管标准产品包"),
        ("硬件矩阵", "适配阶段：样机 / 协议 / 联调", "德沃晨检、出入库称、称重台、AI 摄像头进入评估", "验证门禁联动、稳定性和证据链", "形成硬件白名单与交付前置检查项"),
        ("自动化测试", "能力阶段：从 0 到 1 验证", "自然语言用例、自动执行、HTML 报告方向明确", "中文化报告、失败分析、截图/视频、定时跑测", "形成发布前自动化质量门禁能力"),
        ("AI 手册/视频", "资产阶段：流程转资产", "把业务流程转成脚本、字幕、手册草稿的方向明确", "选取人员卡务等流程打通样例", "一次业务生成测试用例、视频、手册和 FAQ"),
    ]
    yy = y0 + 72
    for ri, row in enumerate(rows):
        fill = "#ffffff" if ri % 2 == 0 else "#f8fafc"
        d.rectangle((x0, yy, x0 + sum(widths), yy + row_h), fill=fill, outline="#dbe4ef")
        xx = x0
        for ci, (text, w) in enumerate(zip(row, widths)):
            color = BLUE if ci == 0 else "#334155"
            draw_multiline(d, (xx + 18, yy + 22), text, font(21), color, max(8, w // 34), 31)
            xx += w
        yy += row_h
    d.rounded_rectangle((60, 1000, 1740, 1060), radius=18, fill="#eaf3ff", outline="#b6c6d9")
    d.text((90, 1016), "写法规则：凡是“跟进中/推进中”，都要补阶段、产物、下周交付和复用价值，否则领导听不到主线。", font=font(24), fill=f"#{BLUE}")
    img.save(path, quality=95)


def make_ai_loop_chart(path: Path) -> None:
    img = Image.new("RGB", (1800, 980), "#ffffff")
    d = ImageDraw.Draw(img)
    d.rectangle((0, 0, 1800, 120), fill="#0b3b5a")
    d.text((70, 36), "AI 赋能：来说话、干事、沉淀资产", font=font(44), fill="white")
    lanes = [
        ("AI 来说话", "会议纪要\nPRD / 字段说明\n演示脚本 / 字幕\n周报 / FAQ / 培训话术", LIGHT_BLUE, BLUE),
        ("AI 来干事", "需求拆模块\n云效任务草稿\n测试用例生成\nPlaywright 执行\nHTML 报告 / 截图视频", LIGHT_GREEN, TEAL),
        ("AI 沉淀资产", "SOP\n自动化脚本\n操作视频\n用户手册\n售后知识库", LIGHT_ORANGE, ORANGE),
    ]
    for i, (title, body, fill, color) in enumerate(lanes):
        x = 95 + i * 570
        y = 195
        rounded_card(d, (x, y, x + 500, y + 390), fill, outline=color)
        d.text((x + 40, y + 35), title, font=font(36), fill=f"#{color}")
        draw_multiline(d, (x + 45, y + 110), body, font(27), "#263238", 18, 45)
        if i < len(lanes) - 1:
            d.line((x + 510, y + 195, x + 555, y + 195), fill="#94a3b8", width=6)
            d.polygon([(x + 555, y + 195), (x + 530, y + 178), (x + 530, y + 212)], fill="#94a3b8")
    d.rounded_rectangle((90, 700, 1710, 885), radius=32, fill="#f8fafc", outline="#dbe4ef", width=2)
    d.text((130, 735), "本周可落地的表达", font=font(32), fill=f"#{NAVY}")
    bullets = [
        "Playwright 自动化测试不只是技术点，而是“自然语言 -> 测试用例 -> 自动执行 -> HTML 报告 -> 发布前门禁”。",
        "产品资料、场景图、讲解视频、会议纪要要进入 AI 协作项目，成为后续需求理解、云效提需和构建发布的训练材料。",
        "业务流程做完不能结束，要转成手册、视频、FAQ 和自动化脚本，减少下一次交付的重复劳动。",
    ]
    yy = 785
    for b in bullets:
        d.text((135, yy), "-", font=font(26), fill=f"#{BLUE}")
        yy = draw_multiline(d, (165, yy), b, font(23), "#334155", 61, 34) + 5
    img.save(path, quality=95)


def make_risk_chart(path: Path) -> None:
    img = Image.new("RGB", (1800, 1020), "#ffffff")
    d = ImageDraw.Draw(img)
    d.text((70, 50), "Before / After：用变化讲出价值", font=font(44), fill=f"#{NAVY}")
    d.text((70, 110), "以后凡是 AI、自动化、标准化、产品能力，都优先用 Before / After 呈现。", font=font(23), fill=f"#{GRAY}")
    headers = ["Before", "After", "价值表达", "证据边界"]
    widths = [430, 560, 500, 200]
    x0, y0 = 70, 150
    row_h = 145
    d.rounded_rectangle((x0, y0, x0 + sum(widths), y0 + 70), radius=18, fill=f"#{NAVY}")
    xx = x0
    for h, w in zip(headers, widths):
        d.text((xx + 20, y0 + 20), h, font=font(24), fill="white")
        xx += w
    rows = [
        ("工作事项碎，领导听不出主线", "按产品规划、项目 SOP、AI 赋能、沉淀价值分层表达", "表达从“事项罗列”变成“体系建设”，管理意图更清楚", "B/U"),
        ("项目只写跟进中、推进中", "写清售前/交付/售后阶段、本周产物、下周交付", "项目状态可追踪，风险和依赖更早暴露", "B/U"),
        ("测试依赖人手写用例、手动执行", "自然语言生成测试用例，Playwright 自动执行并输出报告", "质量门禁可以复用，降低重复验证成本", "B/D"),
        ("业务流程做完就结束", "一次流程沉淀成测试用例、脚本、视频、手册、FAQ", "交付资产可复用，减少下一次售前/交付/培训成本", "U/D"),
        ("技术成果讲不清楚价值", "用 Before / After 讲效率、质量、表达、交付和复用变化", "领导更容易判断资源投入价值和下一步支持", "U/D"),
    ]
    yy = y0 + 70
    for ri, row in enumerate(rows):
        fill = "#ffffff" if ri % 2 == 0 else "#f8fafc"
        d.rectangle((x0, yy, x0 + sum(widths), yy + row_h), fill=fill, outline="#dbe4ef")
        xx = x0
        for ci, (text, w) in enumerate(zip(row, widths)):
            color = RED if ci == 0 else "#334155"
            draw_multiline(d, (xx + 20, yy + 24), text, font(22), color, max(8, w // 34), 31)
            xx += w
        yy += row_h
    d.text((70, 925), "说明：B=来自内部周报/会议材料；U=用户明确教学；D=经营判断或待验证假设。", font=font(22), fill=f"#{GRAY}")
    img.save(path, quality=95)


def make_roadmap_chart(path: Path) -> None:
    img = Image.new("RGB", (1800, 1050), "#ffffff")
    d = ImageDraw.Draw(img)
    d.text((70, 55), "一次性业务 -> 可复用资产流水线", font=font(44), fill=f"#{NAVY}")
    d.text((70, 115), "示例：新增人员 -> 发卡 -> 挂失 -> 冻结 -> 退卡 -> 补卡", font=font(25), fill=f"#{GRAY}")
    flow = ["新增人员", "发卡", "挂失", "冻结", "退卡", "补卡"]
    y = 225
    for i, label in enumerate(flow):
        x = 90 + i * 285
        d.rounded_rectangle((x, y, x + 205, y + 92), radius=22, fill="#eaf3ff", outline=f"#{BLUE}", width=3)
        d.text((x + 42, y + 28), label, font=font(27), fill=f"#{BLUE}")
        if i < len(flow) - 1:
            d.line((x + 210, y + 46, x + 270, y + 46), fill="#94a3b8", width=5)
            d.polygon([(x + 270, y + 46), (x + 248, y + 33), (x + 248, y + 59)], fill="#94a3b8")

    d.text((70, 405), "同一条业务流程，至少沉淀为这些资产", font=font(32), fill=f"#{NAVY}")
    assets = [
        ("SOP", "标准操作流程"),
        ("测试用例", "正常 / 异常路径"),
        ("自动化脚本", "Playwright 点击与断言"),
        ("HTML 报告", "执行结果和截图"),
        ("操作视频", "演示 / 培训素材"),
        ("用户手册", "关键步骤说明"),
        ("FAQ", "问题与售后知识库"),
    ]
    colors = [BLUE, TEAL, ORANGE, GREEN, RED, "7c3aed", "475569"]
    for i, (title, body) in enumerate(assets):
        col = i % 4
        row = i // 4
        x = 85 + col * 425
        yy = 465 + row * 210
        color = colors[i]
        d.rounded_rectangle((x, yy, x + 365, yy + 155), radius=24, fill="#f8fafc", outline=f"#{color}", width=3)
        d.text((x + 30, yy + 30), title, font=font(30), fill=f"#{color}")
        draw_multiline(d, (x + 30, yy + 82), body, font(23), "#334155", 14, 34)

    d.rounded_rectangle((90, 900, 1710, 980), radius=20, fill="#fff7ed", outline="#fdba74")
    d.text((120, 925), "下周建议：选一个高频流程打样，证明“一次业务 -> 测试 -> 自动化 -> 视频 -> 手册”的闭环可复用。", font=font(25), fill=f"#{ORANGE}")
    img.save(path, quality=95)


def make_charts() -> list[Figure]:
    charts = [
        ("exec", CHARTS / "01-executive-summary.png", "图 1｜周报新口径：从做事到做体系", "generated", 6.4, make_exec_summary_chart),
        ("board", CHARTS / "02-project-board.png", "图 2｜SOP + 产品规划表：把项目状态讲清楚", "generated", 6.4, make_project_board_chart),
        ("ai-loop", CHARTS / "03-ai-enablement-loop.png", "图 3｜AI 赋能：来说话、干事、沉淀资产", "generated", 6.4, make_ai_loop_chart),
        ("risk", CHARTS / "04-risk-control.png", "图 4｜Before / After：用变化讲出价值", "generated", 6.4, make_risk_chart),
        ("roadmap", CHARTS / "05-next-week-roadmap.png", "图 5｜一次性业务流程转可复用资产", "generated", 6.4, make_roadmap_chart),
    ]
    figures = []
    for key, path, caption, group, width, maker in charts:
        maker(path)
        figures.append(Figure(key, path, caption, group, width))
    return figures


def image_path(rel: str) -> Path:
    return ROOT / rel


def prepare_image(src: Path, key: str, max_w: int = 2200, max_h: int = 2600) -> Path:
    out = DOCX_READY / f"{key}.jpg"
    with Image.open(src) as im:
        im = im.convert("RGB")
        im.thumbnail((max_w, max_h), Image.Resampling.LANCZOS)
        bg = Image.new("RGB", im.size, "white")
        bg.paste(im)
        bg.save(out, format="JPEG", quality=88, optimize=True)
    return out


def selected_source_figures() -> list[Figure]:
    raw = [
        ("meeting-p1", "images/pdf-pages/meeting-page-01.png", "图 6｜会议纪要首页：会议围绕产品标准化、AI 提效与项目交付展开", "meeting", 6.3),
        ("meeting-p3", "images/pdf-pages/meeting-page-03.png", "图 7｜会议纪要分析：产品标准化与 AI 赋能双轮驱动、SOP 化表达", "meeting", 6.3),
        ("weekly-matrix", "images/xlsx-media/01-image1.png", "图 8｜原始周报证据：标准产品能力版本矩阵与功能沉淀方向", "evidence", 5.0),
        ("guoxin-architecture", "images/xlsx-media/29-image35.png", "图 9｜国信/第三方小程序与报餐预约 H5 闭环部署图", "product", 6.2),
        ("food-safety-dashboard", "images/xlsx-media/22-image29.png", "图 10｜学校/教委场景：校园智慧食安驾驶舱示例", "product", 6.3),
        ("food-safety-platform", "images/xlsx-media/24-image30.png", "图 11｜食安监管平台总览：标准化场景展示示例", "product", 6.3),
        ("ai-dish-manage", "images/xlsx-media/14-image21.png", "图 12｜AI 菜品管理页面：前厅营养结算标准能力示例", "product", 6.3),
        ("pickup-cabinet", "images/xlsx-media/38-image43.png", "图 13｜智能取餐柜使用流程：交付材料可复用样式", "delivery", 4.6),
        ("ai-checkout-flow", "images/xlsx-media/41-image46.png", "图 14｜AI 智能识别结算台运营主流程", "delivery", 6.3),
        ("ai-nutrition-dev", "images/xlsx-media/19-image26.png", "图 15｜AI 运动营养师小程序开发与联调证据", "ai", 5.5),
        ("ai-nutrition-sources", "images/xlsx-media/20-image27.png", "图 16｜AI 运动营养师：参考来源与延伸阅读界面", "ai", 3.6),
        ("ai-network-triage", "images/xlsx-media/36-image41.png", "图 17｜AI 辅助问题排查：网络/设备问题分析示例", "ai", 5.6),
        ("source-gallery", "images/xlsx-media-contact-sheet.png", "图 18｜本次 Excel 原始图片素材总览（47 张）", "appendix", 6.4),
    ]
    return [
        Figure(key, prepare_image(image_path(rel), key), caption, group, width)
        for key, rel, caption, group, width in raw
    ]


def extract_source_text() -> dict:
    wb = load_workbook(XLSX, data_only=True, read_only=True)
    ws = wb.active
    cells = []
    for row in ws.iter_rows():
        for cell in row:
            if cell.value is not None:
                cells.append({"coordinate": cell.coordinate, "value": str(cell.value)})
    wb.close()

    reader = PdfReader(str(PDF))
    pdf_pages = []
    for i, page in enumerate(reader.pages, start=1):
        pdf_pages.append({"page": i, "text": page.extract_text() or ""})

    extracted = {"xlsx_cells": cells, "pdf_pages": pdf_pages}
    (EXTRACTED / "source-text.json").write_text(json.dumps(extracted, ensure_ascii=False, indent=2), encoding="utf-8")
    return extracted


def write_source_index() -> None:
    rows = [
        ["source_id", "file", "type", "role", "evidence_level", "note"],
        ["S1", str((ROOT / "source-docs" / "周报0605.xlsx").relative_to(ROOT)), "xlsx", "周报原始工作记录与图片素材", "B", "内部周报，包含 6月1日-6月7日当周事项及 47 张嵌入图片"],
        ["S2", str((ROOT / "source-docs" / "纪要_产品标准化与AI赋能周会纪要.pdf").relative_to(ROOT)), "pdf", "周会纪要、会议分析与待办", "B", "3 页会议纪要，抽取产品标准化、AI赋能、项目闭环和风险待办"],
        ["U1", "current Codex thread user instruction", "user_instruction", "周报底层逻辑：五层分解、SOP、产品规划、AI资产沉淀、Before/After", "U", "用户明确要求按该思路调整周报并沉淀到项目 skill"],
        ["G1", "charts/*.png", "generated_image", "根据 S1/S2/U1 生成的经营表达图", "D", "用于汇报表达，不作为新增事实来源"],
    ]
    with SOURCE_INDEX.open("w", encoding="utf-8", newline="") as f:
        csv.writer(f).writerows(rows)


REPORT = {
    "title": "产品标准化与 AI 赋能项目周报",
    "subtitle": "数字技术中心-产品开发部 | 2026.06.01-06.07 | 图文版",
    "one_liner": "本周不按“做了很多事”汇报，而按“产品规划 + 项目 SOP + AI 赋能 + 标准化沉淀 + Before/After 价值”汇报：把零散事项分层讲清、按周落地、转成可复用资产。",
    "logic_points": [
        "先分层，再讲事：把本周事项归入产品层、项目层、AI 层、沉淀层和价值层。",
        "先讲阶段，再讲动作：项目不写“推进中”，要写清 SOP 阶段、本周产物、下周交付和复用价值。",
        "先讲结果，再讲价值：AI、自动化、产品能力都用 Before / After 说明效率、质量、表达和交付变化。",
        "一次性业务不应只完成一次交付，而要沉淀成 SOP、测试用例、自动化脚本、操作视频、用户手册和 FAQ。",
    ],
    "layer_rows": [
        ("产品层", "这个能力属于哪个产品 / 模块？", "国信样板、教委端 PC、硬件矩阵、Playwright 测试、AI 手册视频"),
        ("项目层", "当前项目处于什么 SOP 阶段？", "客户确认、开发准备、部署上线、验收收口、售后复盘"),
        ("AI 层", "AI 在里面做了什么？", "需求拆解、PRD、测试用例、自动化测试、HTML 报告、视频字幕、用户手册"),
        ("沉淀层", "能不能复用？", "标准能力、测试脚本、操作视频、FAQ、售后知识库、交付检查清单"),
        ("价值层", "Before / After 变化是什么？", "事项更聚焦、阶段更清楚、资料可复用、交付成本可下降"),
    ],
    "facts": [
        "产品规划：江苏国信 H5/PC 原型、教委端 PC 八板块、食安大屏、取餐柜、AI 结算台等材料，应进入产品规划表，形成按周可追踪的产品/模块产物。",
        "项目 SOP：江苏国信适合定位为样板项目，当前重点是客户确认与开发准备；江西206、莱迪森、金斯瑞、山西焦煤、荣海小学等要按交付/上线/收口清单推进。",
        "AI 赋能：Codex/ChatGPT 已支撑资料与原型整理，Playwright 自动化测试要从自然语言用例推进到中文报告、失败分析、截图/视频和定时跑测。",
        "沉淀资产：业务流程、项目图文资料、演示视频和测试报告不只服务当次项目，应沉淀为标准 SOP、测试用例、操作视频、用户手册和 FAQ。",
        "价值表达：领导指出当前汇报太碎、太散、缺 SOP 和规划；本周调整的关键是用 Before / After 把管理价值、交付价值和复用价值说清楚。",
    ],
    "strategic_read": [
        "SOP 和产品规划表是本周汇报主骨架：SOP 回答项目在哪一步，产品规划表回答 6 月每周落什么产物，二者共同解决“太碎、太散、没有规划”的问题。",
        "AI 的价值要从“辅助写内容”升级为“来说话、干事、沉淀资产”：会议纪要、PRD、演示脚本是说话；云效提需、自动化测试、报告和视频是干事；手册、脚本、FAQ 是资产。",
        "项目成果必须能抽象为售前、交付、售后标准阶段。只有能被复用、被验证、被交付团队重复使用的能力，才能支撑产品标准化和经营效率提升。",
    ],
    "before_after": [
        ("工作事项碎，领导听不出主线", "按产品规划、项目 SOP、AI 赋能、沉淀价值分层表达"),
        ("测试需要人手写用例、手动执行", "自然语言生成测试用例，Playwright 自动执行并输出 HTML 报告"),
        ("做完业务流程后就结束", "一次业务流程沉淀成测试用例、自动化脚本、操作视频和用户手册"),
        ("技术成果讲不清楚价值", "用 Before / After 讲出效率提升、质量提升和可复用价值"),
        ("只会干活，表达弱", "讲清问题、过程、结果、价值，表达更清晰、更自信"),
    ],
    "asset_flow": [
        ("标准操作流程 SOP", "业务操作路径"),
        ("测试用例", "正常 / 异常操作"),
        ("Playwright 自动化脚本", "页面点击过程与断言"),
        ("HTML 测试报告", "执行结果、截图、失败分析"),
        ("演示 / 培训视频", "操作录屏与讲解字幕"),
        ("用户手册", "关键步骤说明"),
        ("FAQ / 售后知识库", "问题与异常复盘"),
    ],
    "next_week": [
        "输出 6 月产品规划表：按周拆分国信样板、教委端 PC、硬件矩阵、Playwright 自动化测试和 AI 手册/视频能力。",
        "输出项目 SOP 阶段表：将重点项目归入售前、交付、售后及客户确认、开发、上线、验收、复盘等阶段。",
        "选择一个高频业务流程打样：以“新增人员 -> 发卡 -> 挂失 -> 冻结 -> 退卡 -> 补卡”为例，产出测试用例、自动化脚本、操作视频和用户手册。",
        "推进 Playwright 能力产品化：中文 HTML 报告、失败分析、异常截图/视频、定时跑测，并接入 AI 协作项目。",
        "将会议纪要、产品资料、项目图文和操作视频统一归档，让 AI Agent 后续能基于资料理解需求、提云效任务、生成测试和发布前检查。",
    ],
}


def report_markdown(figures: list[Figure]) -> str:
    lines = [
        f"# {REPORT['title']}",
        "",
        f"> {REPORT['subtitle']}",
        "",
        f"**一句话结论：**{REPORT['one_liner']}",
        "",
        "## 一、周报新口径：从做事到做体系",
    ]
    for item in REPORT["logic_points"]:
        lines.append(f"- {item}")
    lines += ["", "## 二、五层分解"]
    lines.append("| 层级 | 要回答的问题 | 本周落点 |")
    lines.append("| --- | --- | --- |")
    for row in REPORT["layer_rows"]:
        lines.append(f"| {row[0]} | {row[1]} | {row[2]} |")
    lines += ["", "## 三、SOP + 产品规划表：本周事实重组"]
    for item in REPORT["facts"]:
        lines.append(f"- {item}")
    lines += ["", "## 四、关键判断"]
    for item in REPORT["strategic_read"]:
        lines.append(f"- {item}")
    lines += ["", "## 五、Before / After"]
    lines.append("| Before | After |")
    lines.append("| --- | --- |")
    for before, after in REPORT["before_after"]:
        lines.append(f"| {before} | {after} |")
    lines += ["", "## 六、一次性业务流程沉淀为资产"]
    lines.append("示例流程：`新增人员 -> 发卡 -> 挂失 -> 冻结 -> 退卡 -> 补卡`。")
    for title, body in REPORT["asset_flow"]:
        lines.append(f"- {title}：{body}")
    lines += ["", "## 七、下周计划"]
    for i, item in enumerate(REPORT["next_week"], start=1):
        lines.append(f"{i}. {item}")
    lines += [
        "",
        "## 八、风险与证据边界",
        "- 本报告不新增未验证的收入、毛利、回款、客户满意度或验收数据。",
        "- 来源证据为内部周报 Excel、会议纪要 PDF 与用户明确教学；经营解读按 D 级假设处理，需后续用云效、合同、验收、客户截图和财务数据补证。",
        "",
        "## 九、图像清单",
    ]
    for fig in figures:
        lines.append(f"- {fig.caption}：`{fig.path.relative_to(ROOT)}`")
    lines.append("")
    return "\n".join(lines)


def write_summary_md() -> None:
    SUMMARY_MD.write_text(
        "\n".join(
            [
                "# 产品标准化与 AI 赋能项目周报材料包",
                "",
                "## Goal",
                "结合 `周报0605.xlsx` 与 `纪要_产品标准化与AI赋能周会纪要.pdf`，生成可直接发出的图文版 Word 周报，并保留 Markdown/HTML/图片证据。",
                "",
                "## Source Context",
                "- S1：周报 Excel，内部周报来源，B 级证据。",
                "- S2：产品标准化与 AI 赋能周会纪要 PDF，内部会议纪要，B 级证据。",
                "- U1：Jack 在当前 Codex 线程明确教学周报底层逻辑：五层分解、SOP、产品规划、AI 资产沉淀、Before / After。",
                "",
                "## Scope",
                "- 生成 Word 图文版周报。",
                "- 生成 Markdown 真源和 HTML 人审预览。",
                "- 抽取并整理 Excel 嵌入图片和 PDF 页面图片。",
                "- 将周报从事项罗列改为“产品规划 + 项目 SOP + AI 赋能 + 标准化沉淀 + Before / After 价值”的汇报结构。",
                "",
                "## Non-goals",
                "- 不新增财务、验收、回款、客户满意度等未在来源中出现的数据。",
                "- 不外发文件、不上传微盘、不修改业务代码。",
                "",
                "## Validation Criteria",
                "- DOCX 可被 LibreOffice 渲染为页面图片。",
                "- Word 内包含多张源材料图片与经营看板图。",
                "- 结论均可追溯到周报 Excel、会议纪要 PDF 或用户明确教学；经营判断标为解释而非事实。",
                "",
                "## Human Review Status",
                "待 Jack 审阅。当前版本为内部汇报初稿。",
                "",
            ]
        ),
        encoding="utf-8",
    )


def set_cell_shading(cell, fill: str) -> None:
    tc_pr = cell._tc.get_or_add_tcPr()
    shd = tc_pr.find(qn("w:shd"))
    if shd is None:
        shd = OxmlElement("w:shd")
        tc_pr.append(shd)
    shd.set(qn("w:fill"), fill)


def set_cell_border(cell, color: str = "dbe4ef") -> None:
    tc = cell._tc
    tc_pr = tc.get_or_add_tcPr()
    borders = tc_pr.first_child_found_in("w:tcBorders")
    if borders is None:
        borders = OxmlElement("w:tcBorders")
        tc_pr.append(borders)
    for edge in ["top", "left", "bottom", "right"]:
        tag = "w:" + edge
        element = borders.find(qn(tag))
        if element is None:
            element = OxmlElement(tag)
            borders.append(element)
        element.set(qn("w:val"), "single")
        element.set(qn("w:sz"), "4")
        element.set(qn("w:space"), "0")
        element.set(qn("w:color"), color)


def set_run_font(run, name: str = "PingFang SC", size: float | None = None, color: str | None = None, bold: bool | None = None) -> None:
    run.font.name = name
    run._element.rPr.rFonts.set(qn("w:eastAsia"), name)
    run._element.rPr.rFonts.set(qn("w:ascii"), name)
    run._element.rPr.rFonts.set(qn("w:hAnsi"), name)
    if size is not None:
        run.font.size = Pt(size)
    if color is not None:
        run.font.color.rgb = RGBColor.from_string(color)
    if bold is not None:
        run.bold = bold


def style_doc(doc: Document) -> None:
    section = doc.sections[0]
    section.page_width = Inches(8.5)
    section.page_height = Inches(11)
    section.orientation = WD_ORIENTATION.PORTRAIT
    for side in ["top_margin", "bottom_margin", "left_margin", "right_margin"]:
        setattr(section, side, Inches(1.0))
    section.header_distance = Inches(0.492)
    section.footer_distance = Inches(0.492)

    styles = doc.styles
    for style_name, size, color, bold in [
        ("Normal", 11, "1f2937", False),
        ("Heading 1", 16, "2E74B5", True),
        ("Heading 2", 13, "2E74B5", True),
        ("Heading 3", 12, "1F4D78", True),
        ("Caption", 9, "64748b", False),
    ]:
        style = styles[style_name]
        font_obj = style.font
        font_obj.name = "PingFang SC"
        font_obj.size = Pt(size)
        font_obj.color.rgb = RGBColor.from_string(color)
        font_obj.bold = bold
        style._element.rPr.rFonts.set(qn("w:eastAsia"), "PingFang SC")
        style.paragraph_format.space_after = Pt(6 if style_name == "Normal" else 8)
        style.paragraph_format.line_spacing = 1.10 if style_name == "Normal" else 1.0

    header = section.header.paragraphs[0]
    header.alignment = WD_ALIGN_PARAGRAPH.RIGHT
    r = header.add_run("数字技术中心-产品开发部 | 产品标准化与 AI 赋能周报")
    set_run_font(r, size=9, color=GRAY)
    footer = section.footer.paragraphs[0]
    footer.alignment = WD_ALIGN_PARAGRAPH.CENTER
    r = footer.add_run("内部汇报初稿 | 来源：周报 Excel + 周会纪要 + 周报底层逻辑")
    set_run_font(r, size=9, color=GRAY)


def add_title_page(doc: Document) -> None:
    p = doc.add_paragraph()
    p.paragraph_format.space_before = Pt(20)
    p.paragraph_format.space_after = Pt(4)
    r = p.add_run(REPORT["title"])
    set_run_font(r, size=26, color=NAVY, bold=True)
    p = doc.add_paragraph()
    p.paragraph_format.space_after = Pt(14)
    r = p.add_run(REPORT["subtitle"])
    set_run_font(r, size=13, color=GRAY)
    add_callout(doc, REPORT["one_liner"], fill=LIGHT_BLUE, color=BLUE)
    add_meta_table(
        doc,
        [
            ("汇报对象", "经营层 / 数字技术中心 / 产品开发部"),
            ("报告类型", "领导可读周报：五层分解 + SOP 阶段 + 产品规划 + Before/After + 图像证据"),
            ("来源材料", "周报0605.xlsx；纪要_产品标准化与AI赋能周会纪要.pdf；Jack 周报底层逻辑"),
            ("证据边界", "内部周报与会议纪要为 B 级证据；用户教学为 U；经营判断为 D 级解释，不新增财务或验收数据"),
        ],
    )


def add_callout(doc: Document, text: str, fill: str = LIGHT_GRAY, color: str = NAVY) -> None:
    table = doc.add_table(rows=1, cols=1)
    table.alignment = WD_TABLE_ALIGNMENT.CENTER
    table.autofit = False
    table.columns[0].width = Inches(6.3)
    cell = table.cell(0, 0)
    set_cell_shading(cell, fill)
    set_cell_border(cell, "dbe4ef")
    p = cell.paragraphs[0]
    p.paragraph_format.space_before = Pt(5)
    p.paragraph_format.space_after = Pt(5)
    r = p.add_run(text)
    set_run_font(r, size=11.5, color=color, bold=True)
    doc.add_paragraph()


def add_meta_table(doc: Document, rows: list[tuple[str, str]]) -> None:
    table = doc.add_table(rows=len(rows), cols=2)
    table.alignment = WD_TABLE_ALIGNMENT.CENTER
    table.autofit = False
    widths = [Inches(1.35), Inches(4.95)]
    for row_idx, (label, value) in enumerate(rows):
        cells = table.rows[row_idx].cells
        for idx, w in enumerate(widths):
            cells[idx].width = w
            set_cell_border(cells[idx])
            cells[idx].vertical_alignment = WD_ALIGN_VERTICAL.CENTER
        set_cell_shading(cells[0], "f2f4f7")
        p = cells[0].paragraphs[0]
        r = p.add_run(label)
        set_run_font(r, size=10.5, color=NAVY, bold=True)
        p = cells[1].paragraphs[0]
        r = p.add_run(value)
        set_run_font(r, size=10.5, color="334155")
    doc.add_paragraph()


def add_bullets(doc: Document, items: list[str]) -> None:
    for item in items:
        p = doc.add_paragraph(style=None)
        p.paragraph_format.left_indent = Inches(0.22)
        p.paragraph_format.first_line_indent = Inches(-0.16)
        p.paragraph_format.space_after = Pt(4)
        r = p.add_run("• ")
        set_run_font(r, size=11, color=BLUE, bold=True)
        r = p.add_run(item)
        set_run_font(r, size=11, color="1f2937")


def add_numbered(doc: Document, items: list[str]) -> None:
    for idx, item in enumerate(items, 1):
        p = doc.add_paragraph()
        p.paragraph_format.left_indent = Inches(0.28)
        p.paragraph_format.first_line_indent = Inches(-0.22)
        p.paragraph_format.space_after = Pt(4)
        r = p.add_run(f"{idx}. ")
        set_run_font(r, size=11, color=BLUE, bold=True)
        r = p.add_run(item)
        set_run_font(r, size=11, color="1f2937")


def add_figure(doc: Document, fig: Figure) -> None:
    p = doc.add_paragraph()
    p.alignment = WD_ALIGN_PARAGRAPH.CENTER
    run = p.add_run()
    run.add_picture(str(fig.path), width=Inches(fig.width_in))
    cap = doc.add_paragraph(style="Caption")
    cap.alignment = WD_ALIGN_PARAGRAPH.CENTER
    r = cap.add_run(fig.caption)
    set_run_font(r, size=9, color=GRAY)


def add_report_table(doc: Document, title: str, headers: list[str], rows: list[tuple[str, ...]], widths: list[float]) -> None:
    doc.add_heading(title, level=2)
    table = doc.add_table(rows=1, cols=len(headers))
    table.alignment = WD_TABLE_ALIGNMENT.CENTER
    table.autofit = False
    inch_widths = [Inches(w) for w in widths]
    for idx, text in enumerate(headers):
        cell = table.rows[0].cells[idx]
        cell.width = inch_widths[idx]
        set_cell_shading(cell, NAVY)
        set_cell_border(cell, "dbe4ef")
        r = cell.paragraphs[0].add_run(text)
        set_run_font(r, size=9.2, color="ffffff", bold=True)
    for row in rows:
        cells = table.add_row().cells
        for idx, text in enumerate(row):
            cell = cells[idx]
            cell.width = inch_widths[idx]
            set_cell_border(cell)
            if idx == 0:
                set_cell_shading(cell, "f2f4f7")
            p = cell.paragraphs[0]
            r = p.add_run(text)
            set_run_font(r, size=8.8, color=NAVY if idx == 0 else "334155", bold=(idx == 0))
            cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
    doc.add_paragraph()


def add_status_table(doc: Document) -> None:
    rows = [
        ("江苏国信", "交付阶段：客户确认 / 开发准备", "H5 与 PC 原型迭代；报餐、订餐、招待权限、核销扣款链路", "确认子部门、代报限制，进入可启动任务开发", "机关/国企招待餐与预约就餐标准样板"),
        ("教委/学校端", "产品阶段：PRD 拆解 / 排期", "PC 端八板块方向确认，准备补齐云效任务", "与开发对齐估时、样式和上线计划", "学校/教委监管标准产品包"),
        ("硬件矩阵", "适配阶段：样机 / 协议 / 联调", "德沃晨检、出入库称、称重台、AI 摄像头进入评估", "验证门禁联动、稳定性和证据链", "硬件白名单与交付前置检查项"),
        ("Playwright 测试", "能力阶段：从 0 到 1 验证", "自然语言用例、自动执行、HTML 报告方向明确", "中文化报告、失败分析、截图/视频、定时跑测", "发布前自动化质量门禁能力"),
        ("AI 手册/视频", "资产阶段：流程转资产", "业务流程转脚本、字幕、手册草稿方向明确", "选取人员卡务等流程打通样例", "一次业务生成测试用例、视频、手册和 FAQ"),
    ]
    add_report_table(
        doc,
        "SOP + 产品规划表（周报主骨架）",
        ["事项", "当前阶段", "本周产物", "下周交付", "沉淀价值"],
        rows,
        [0.95, 1.15, 1.55, 1.45, 1.25],
    )


def build_docx(figures: list[Figure]) -> None:
    generated = [f for f in figures if f.group == "generated"]
    source = [f for f in figures if f.group != "generated"]
    source_by_key = {f.key: f for f in source}

    doc = Document()
    style_doc(doc)
    add_title_page(doc)
    add_figure(doc, generated[0])

    doc.add_heading("一、周报新口径：从做事到做体系", level=1)
    add_bullets(doc, REPORT["logic_points"])
    add_callout(
        doc,
        "本周汇报的重点不是“我做了很多事”，而是“我如何把事情分层讲清楚、按 SOP 管起来、按产品规划落下去，再用 AI 把一次性业务沉淀成可复用资产”。",
        fill=LIGHT_BLUE,
        color=BLUE,
    )
    add_report_table(
        doc,
        "五层分解",
        ["层级", "要回答的问题", "本周落点"],
        REPORT["layer_rows"],
        [1.0, 2.0, 3.3],
    )

    doc.add_heading("二、SOP + 产品规划：把项目状态讲清楚", level=1)
    add_figure(doc, generated[1])
    add_status_table(doc)
    add_bullets(doc, REPORT["facts"])

    doc.add_heading("三、关键判断：为什么这周不是普通项目周报", level=1)
    add_bullets(doc, REPORT["strategic_read"])
    add_callout(
        doc,
        "建议对外/对上汇报时，把“项目很多、事情很多”改写成“标准产品包、项目 SOP、AI 自动化和交付资产正在形成”。",
        fill=LIGHT_ORANGE,
        color=ORANGE,
    )
    add_figure(doc, source_by_key["meeting-p1"])
    add_figure(doc, source_by_key["weekly-matrix"])

    doc.add_heading("四、Before / After：用变化讲出价值", level=1)
    add_figure(doc, generated[3])
    add_report_table(
        doc,
        "Before / After 表达",
        ["Before", "After"],
        REPORT["before_after"],
        [2.8, 3.5],
    )

    doc.add_heading("五、产品标准化：把项目能力沉淀为可复制资产", level=1)
    add_bullets(
        doc,
        [
            "江苏国信是本周最适合讲的样板：它把第三方小程序、报餐预约 H5、内网消费系统、安全策略和人脸/刷卡终端串成闭环。",
            "学校/教委端不是单一页面优化，而是向食安监管、营养达标、采购验收、家长透明和监管驾驶舱沉淀标准场景。",
            "智能取餐柜、AI 识别结算台、食安传感器等图文资料，可以进入售前讲解、交付培训和标准产品说明。",
        ],
    )
    for key in ["guoxin-architecture", "food-safety-dashboard", "food-safety-platform", "ai-dish-manage", "pickup-cabinet", "ai-checkout-flow"]:
        add_figure(doc, source_by_key[key])

    doc.add_heading("六、AI 赋能：来说话、干事、沉淀资产", level=1)
    add_figure(doc, generated[2])
    add_bullets(
        doc,
        [
            "AI 来说话：会议纪要、PRD、字段说明、演示脚本、操作视频字幕、周报、FAQ 和用户培训材料。",
            "AI 来干事：需求拆模块、云效任务草稿、测试用例生成、Playwright 执行、HTML 报告、截图视频和手册草稿。",
            "AI 沉淀资产：业务流程做完不能结束，要转成手册、视频、FAQ、自动化脚本和售后知识库。",
        ],
    )
    for key in ["ai-nutrition-dev", "ai-nutrition-sources", "ai-network-triage"]:
        add_figure(doc, source_by_key[key])

    doc.add_heading("七、一次性业务流程：沉淀为四类以上资产", level=1)
    add_figure(doc, generated[4])
    add_callout(
        doc,
        "示例流程：新增人员 -> 发卡 -> 挂失 -> 冻结 -> 退卡 -> 补卡。后续要证明“一次业务流程”能同步产出测试用例、自动化脚本、操作视频和用户手册。",
        fill=LIGHT_GREEN,
        color=TEAL,
    )
    add_report_table(
        doc,
        "一次业务流程可沉淀资产",
        ["资产", "来源"],
        REPORT["asset_flow"],
        [2.0, 4.3],
    )

    doc.add_heading("八、下周计划", level=1)
    add_numbered(doc, REPORT["next_week"])
    add_figure(doc, source_by_key["meeting-p3"])

    doc.add_heading("九、图像证据附录", level=1)
    add_callout(
        doc,
        "以下为本次 Excel 内嵌图片素材总览。Word 正文只选取了与本周主线强相关的图片；其余素材仍保留在材料包中，后续可继续做成 PPT、HTML 或产品资料库。",
        fill=LIGHT_GRAY,
        color=NAVY,
    )
    add_figure(doc, source_by_key["source-gallery"])

    doc.add_heading("来源与证据边界", level=1)
    add_meta_table(
        doc,
        [
            ("S1", "周报0605.xlsx：本周周报总表与嵌入图片，B 级内部证据。"),
            ("S2", "纪要_产品标准化与AI赋能周会纪要.pdf：周会纪要、待办和会议分析，B 级内部证据。"),
            ("U1", "Jack 当前线程教学：周报底层逻辑为五层分解、SOP、产品规划、AI资产沉淀、Before/After。"),
            ("G1", "本报告中的经营看板图为基于 S1/S2/U1 的表达加工，D 级解释，不作为新增事实。"),
            ("限制", "未验证收入、毛利、回款、验收、客户满意度等数据，因此不写成已完成经营结果。"),
        ],
    )
    doc.save(DOCX_REPORT)


def build_html(figures: list[Figure]) -> None:
    def img_tag(fig: Figure) -> str:
        rel = fig.path.relative_to(ROOT)
        return f'<figure><img src="../{html.escape(str(rel))}" alt="{html.escape(fig.caption)}"><figcaption>{html.escape(fig.caption)}</figcaption></figure>'

    def html_table(headers: list[str], rows: list[tuple[str, ...]]) -> str:
        head = "".join(f"<th>{html.escape(h)}</th>" for h in headers)
        body_rows = []
        for row in rows:
            body_rows.append("<tr>" + "".join(f"<td>{html.escape(cell)}</td>" for cell in row) + "</tr>")
        return "<table><thead><tr>" + head + "</tr></thead><tbody>" + "".join(body_rows) + "</tbody></table>"

    body = [
        "<!doctype html><html lang='zh-CN'><head><meta charset='utf-8'>",
        "<title>产品标准化与AI赋能项目周报</title>",
        "<style>",
        "body{margin:0;background:#f5f7fb;color:#172033;font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei',Arial,sans-serif;line-height:1.65}",
        ".wrap{max-width:1120px;margin:0 auto;padding:42px 28px 80px}",
        ".hero{background:#0f2437;color:white;padding:42px;border-radius:18px;margin-bottom:24px}",
        ".hero h1{margin:0 0 10px;font-size:34px}.hero p{margin:0;color:#cbd5e1}",
        ".callout{background:#eaf3ff;border-left:6px solid #1f4e79;padding:18px 22px;border-radius:10px;margin:20px 0;font-weight:700;color:#17375e}",
        "section{background:white;border:1px solid #e2e8f0;border-radius:16px;padding:28px;margin:22px 0}",
        "h2{margin-top:0;color:#1f4e79} ul,ol{padding-left:24px}",
        "table{width:100%;border-collapse:collapse;margin:14px 0 18px;font-size:14px} th{background:#0f2437;color:white;text-align:left;padding:10px;border:1px solid #dbe4ef} td{padding:10px;border:1px solid #dbe4ef;vertical-align:top} td:first-child{font-weight:700;color:#0f2437;background:#f8fafc}",
        ".grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:18px}",
        "figure{margin:20px 0;background:#fff;border:1px solid #e2e8f0;border-radius:12px;padding:12px;box-shadow:0 2px 10px rgba(15,36,55,.06)}",
        "figure img{width:100%;height:auto;border-radius:8px} figcaption{font-size:13px;color:#64748b;text-align:center;margin-top:8px}",
        ".evidence{font-size:14px;color:#475569}",
        "</style></head><body><div class='wrap'>",
        f"<div class='hero'><h1>{html.escape(REPORT['title'])}</h1><p>{html.escape(REPORT['subtitle'])}</p></div>",
        f"<div class='callout'>{html.escape(REPORT['one_liner'])}</div>",
        "<section><h2>一、周报新口径：从做事到做体系</h2><ul>",
        *[f"<li>{html.escape(x)}</li>" for x in REPORT["logic_points"]],
        "</ul></section>",
        "<section><h2>二、五层分解</h2>",
        html_table(["层级", "要回答的问题", "本周落点"], REPORT["layer_rows"]),
        "</section>",
        "<section><h2>三、SOP + 产品规划：本周事实重组</h2><ul>",
        *[f"<li>{html.escape(x)}</li>" for x in REPORT["facts"]],
        "</ul></section>",
        "<section><h2>四、关键判断</h2><ul>",
        *[f"<li>{html.escape(x)}</li>" for x in REPORT["strategic_read"]],
        "</ul></section>",
        "<section><h2>五、Before / After</h2>",
        html_table(["Before", "After"], REPORT["before_after"]),
        "</section>",
        "<section><h2>六、一次业务流程沉淀为资产</h2><p>示例流程：新增人员 -> 发卡 -> 挂失 -> 冻结 -> 退卡 -> 补卡。</p>",
        html_table(["资产", "来源"], REPORT["asset_flow"]),
        "</section>",
        "<section><h2>七、下周计划</h2><ol>",
        *[f"<li>{html.escape(x)}</li>" for x in REPORT["next_week"]],
        "</ol></section>",
        "<section><h2>八、图像证据</h2><div class='grid'>",
        *[img_tag(fig) for fig in figures],
        "</div></section>",
        "<section class='evidence'><h2>来源与边界</h2><p>S1：周报0605.xlsx；S2：产品标准化与 AI 赋能周会纪要 PDF；U1：Jack 当前线程教学。内部材料为 B 级证据，用户教学为 U，经营解读为 D 级解释；未新增未验证财务、验收、回款或客户反馈数据。</p></section>",
        "</div></body></html>",
    ]
    HTML_REPORT.write_text("\n".join(body), encoding="utf-8")


def main() -> None:
    ensure_dirs()
    write_source_index()
    write_summary_md()
    extract_source_text()
    figures = make_charts() + selected_source_figures()
    REPORT_MD.write_text(report_markdown(figures), encoding="utf-8")
    build_html(figures)
    build_docx(figures)
    print(json.dumps({
        "docx": str(DOCX_REPORT),
        "html": str(HTML_REPORT),
        "markdown": str(REPORT_MD),
        "summary": str(SUMMARY_MD),
        "source_index": str(SOURCE_INDEX),
        "figure_count": len(figures),
    }, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()
