# -*- coding: utf-8 -*-
from pathlib import Path
from datetime import date

from PIL import Image
from pptx import Presentation
from pptx.dml.color import RGBColor
from pptx.enum.shapes import MSO_AUTO_SHAPE_TYPE
from pptx.enum.text import PP_ALIGN
from pptx.util import Inches, Pt


BASE = Path(__file__).resolve().parent
ASSET_DIR = BASE / "ppt_assets"
OUT = BASE / "MESCommonBase迁移分析资料包.pptx"


COLORS = {
    "bg": RGBColor(247, 249, 252),
    "navy": RGBColor(17, 24, 39),
    "text": RGBColor(52, 64, 84),
    "muted": RGBColor(102, 112, 133),
    "line": RGBColor(199, 210, 229),
    "blue": RGBColor(40, 100, 180),
    "green": RGBColor(34, 122, 69),
    "purple": RGBColor(126, 34, 206),
    "amber": RGBColor(178, 119, 0),
    "red": RGBColor(192, 16, 72),
    "blue_light": RGBColor(232, 241, 255),
    "green_light": RGBColor(234, 247, 239),
    "purple_light": RGBColor(243, 232, 255),
    "amber_light": RGBColor(255, 247, 230),
    "red_light": RGBColor(255, 241, 243),
    "white": RGBColor(255, 255, 255),
}

FONT = "Microsoft YaHei"
FONT_ALT = "SimHei"


def crop_images():
    ASSET_DIR.mkdir(exist_ok=True)
    specs = [
        (
            "MESCommonBase流程图.png",
            [
                ("总流程_完整.png", None),
                ("总流程_入口解析.png", (1450, 0, 3800, 1450)),
                ("总流程_Type分支.png", (110, 1450, 5100, 2580)),
                ("总流程_响应默认.png", (190, 2600, 5000, 3520)),
            ],
        ),
        (
            "MESCommonBase响应输出与SqlWebCall主流程图.png",
            [
                ("响应主_完整.png", None),
                ("响应主_分支出口.png", (120, 760, 5280, 2050)),
                ("响应主_规则异常.png", (300, 2100, 5150, 2880)),
            ],
        ),
        (
            "MESCommonBase响应输出与SqlWebCall次流程图.png",
            [
                ("SqlWebCall_完整.png", None),
                ("SqlWebCall_初始化.png", (1500, 120, 4750, 820)),
                ("SqlWebCall_Type二次分发.png", (110, 900, 5700, 2600)),
                ("SqlWebCall_数据库返回.png", (260, 2680, 5550, 3540)),
                ("SqlWebCall_风险.png", (520, 3650, 5300, 3950)),
            ],
        ),
    ]
    for src_name, crops in specs:
        src = BASE / src_name
        if not src.exists():
            continue
        with Image.open(src) as im:
            for out_name, box in crops:
                out = ASSET_DIR / out_name
                if box is None:
                    im.copy().save(out)
                else:
                    im.crop(box).save(out)


def set_font(paragraph, size=None, bold=None, color=None):
    for run in paragraph.runs:
        run.font.name = FONT
        try:
            run._r.rPr.rFonts.set("{http://schemas.openxmlformats.org/wordprocessingml/2006/main}eastAsia", FONT)
        except Exception:
            pass
        if size is not None:
            run.font.size = Pt(size)
        if bold is not None:
            run.font.bold = bold
        if color is not None:
            run.font.color.rgb = color


def apply_run_font(run, size, color=COLORS["text"], bold=False):
    run.font.name = FONT
    try:
        run._r.rPr.rFonts.set("{http://schemas.openxmlformats.org/wordprocessingml/2006/main}eastAsia", FONT)
    except Exception:
        pass
    run.font.size = Pt(size)
    run.font.color.rgb = color
    run.font.bold = bold


class Deck:
    def __init__(self):
        self.prs = Presentation()
        self.prs.slide_width = Inches(13.333)
        self.prs.slide_height = Inches(7.5)
        self.slide_no = 0

    def save(self):
        self.prs.save(OUT)

    def blank(self):
        slide = self.prs.slides.add_slide(self.prs.slide_layouts[6])
        self.slide_no += 1
        bg = slide.background.fill
        bg.solid()
        bg.fore_color.rgb = COLORS["bg"]
        return slide

    def add_footer(self, slide, section="MESCommonBase 迁移分析资料包"):
        tx = slide.shapes.add_textbox(Inches(0.45), Inches(7.12), Inches(9.5), Inches(0.25))
        p = tx.text_frame.paragraphs[0]
        run = p.add_run()
        run.text = section
        apply_run_font(run, 8, COLORS["muted"])
        tx2 = slide.shapes.add_textbox(Inches(12.0), Inches(7.12), Inches(0.85), Inches(0.25))
        p2 = tx2.text_frame.paragraphs[0]
        p2.alignment = PP_ALIGN.RIGHT
        run2 = p2.add_run()
        run2.text = str(self.slide_no)
        apply_run_font(run2, 8, COLORS["muted"])

    def title(self, slide, title, subtitle=None):
        tx = slide.shapes.add_textbox(Inches(0.55), Inches(0.32), Inches(12.2), Inches(0.65))
        p = tx.text_frame.paragraphs[0]
        run = p.add_run()
        run.text = title
        apply_run_font(run, 26, COLORS["navy"], True)
        if subtitle:
            st = slide.shapes.add_textbox(Inches(0.58), Inches(0.92), Inches(12.0), Inches(0.32))
            p2 = st.text_frame.paragraphs[0]
            run2 = p2.add_run()
            run2.text = subtitle
            apply_run_font(run2, 10.5, COLORS["muted"])

    def section_slide(self, title, subtitle):
        slide = self.blank()
        bar = slide.shapes.add_shape(MSO_AUTO_SHAPE_TYPE.RECTANGLE, 0, 0, Inches(13.333), Inches(7.5))
        bar.fill.solid()
        bar.fill.fore_color.rgb = RGBColor(238, 244, 255)
        bar.line.color.rgb = RGBColor(238, 244, 255)
        tx = slide.shapes.add_textbox(Inches(1.1), Inches(2.45), Inches(11.0), Inches(0.75))
        p = tx.text_frame.paragraphs[0]
        run = p.add_run()
        run.text = title
        apply_run_font(run, 34, COLORS["navy"], True)
        st = slide.shapes.add_textbox(Inches(1.12), Inches(3.28), Inches(10.5), Inches(0.45))
        p2 = st.text_frame.paragraphs[0]
        run2 = p2.add_run()
        run2.text = subtitle
        apply_run_font(run2, 15, COLORS["text"])
        self.add_footer(slide)
        return slide

    def card(self, slide, x, y, w, h, title, body=None, fill="white", line="line", title_color="navy"):
        shape = slide.shapes.add_shape(MSO_AUTO_SHAPE_TYPE.ROUNDED_RECTANGLE, Inches(x), Inches(y), Inches(w), Inches(h))
        shape.fill.solid()
        shape.fill.fore_color.rgb = COLORS[fill]
        shape.line.color.rgb = COLORS[line]
        shape.line.width = Pt(1.2)
        tx = slide.shapes.add_textbox(Inches(x + 0.18), Inches(y + 0.13), Inches(w - 0.36), Inches(h - 0.22))
        tf = tx.text_frame
        tf.word_wrap = True
        p = tf.paragraphs[0]
        run = p.add_run()
        run.text = title
        apply_run_font(run, 13, COLORS[title_color], True)
        if body:
            p2 = tf.add_paragraph()
            p2.space_before = Pt(4)
            run2 = p2.add_run()
            run2.text = body
            apply_run_font(run2, 9.5, COLORS["text"])
        return shape

    def bullets(self, slide, x, y, w, h, items, font_size=13, color=COLORS["text"], title=None):
        tx = slide.shapes.add_textbox(Inches(x), Inches(y), Inches(w), Inches(h))
        tf = tx.text_frame
        tf.word_wrap = True
        tf.clear()
        if title:
            p = tf.paragraphs[0]
            run = p.add_run()
            run.text = title
            apply_run_font(run, font_size + 2, COLORS["navy"], True)
        for idx, item in enumerate(items):
            p = tf.add_paragraph() if title or idx else tf.paragraphs[0]
            p.text = ""
            p.level = 0
            p.space_after = Pt(3)
            run = p.add_run()
            run.text = f"• {item}"
            apply_run_font(run, font_size, color)
        return tx

    def add_image_fit(self, slide, image_name, x, y, w, h, border=True):
        path = ASSET_DIR / image_name
        if not path.exists():
            return None
        with Image.open(path) as im:
            iw, ih = im.size
        max_w, max_h = Inches(w), Inches(h)
        scale = min(max_w / iw, max_h / ih)
        pic_w, pic_h = int(iw * scale), int(ih * scale)
        left = Inches(x) + int((max_w - pic_w) / 2)
        top = Inches(y) + int((max_h - pic_h) / 2)
        if border:
            rect = slide.shapes.add_shape(MSO_AUTO_SHAPE_TYPE.ROUNDED_RECTANGLE, Inches(x), Inches(y), Inches(w), Inches(h))
            rect.fill.solid()
            rect.fill.fore_color.rgb = COLORS["white"]
            rect.line.color.rgb = COLORS["line"]
            rect.line.width = Pt(1.1)
        return slide.shapes.add_picture(str(path), left, top, width=pic_w, height=pic_h)

    def table(self, slide, x, y, w, h, data, col_widths=None, font_size=8.5):
        rows, cols = len(data), len(data[0])
        shape = slide.shapes.add_table(rows, cols, Inches(x), Inches(y), Inches(w), Inches(h))
        table = shape.table
        if col_widths:
            for idx, cw in enumerate(col_widths):
                table.columns[idx].width = Inches(cw)
        for r, row in enumerate(data):
            for c, value in enumerate(row):
                cell = table.cell(r, c)
                cell.text = str(value)
                cell.margin_left = Inches(0.05)
                cell.margin_right = Inches(0.05)
                fill = cell.fill
                fill.solid()
                fill.fore_color.rgb = RGBColor(238, 244, 255) if r == 0 else COLORS["white"]
                for p in cell.text_frame.paragraphs:
                    p.alignment = PP_ALIGN.CENTER if r == 0 else PP_ALIGN.LEFT
                    for run in p.runs:
                        apply_run_font(run, font_size, COLORS["navy"] if r == 0 else COLORS["text"], r == 0)
        return shape


def build_deck():
    crop_images()
    deck = Deck()

    # 1 cover
    slide = deck.blank()
    bg = slide.shapes.add_shape(MSO_AUTO_SHAPE_TYPE.RECTANGLE, 0, 0, Inches(13.333), Inches(7.5))
    bg.fill.solid()
    bg.fill.fore_color.rgb = RGBColor(238, 244, 255)
    bg.line.color.rgb = RGBColor(238, 244, 255)
    tx = slide.shapes.add_textbox(Inches(0.9), Inches(1.45), Inches(11.6), Inches(1.0))
    p = tx.text_frame.paragraphs[0]
    run = p.add_run()
    run.text = "MESCommonBase 迁移分析资料包"
    apply_run_font(run, 36, COLORS["navy"], True)
    st = slide.shapes.add_textbox(Inches(0.95), Inches(2.45), Inches(10.8), Inches(0.72))
    p2 = st.text_frame.paragraphs[0]
    run2 = p2.add_run()
    run2.text = "响应输出、SqlWebCall 默认分支、参数协议、数据库链路与移植检查清单"
    apply_run_font(run2, 17, COLORS["text"])
    deck.card(slide, 0.95, 4.6, 3.2, 1.05, "分析对象", "MES_Manage/submit/MESCommonBase.ashx", "white", "blue", "blue")
    deck.card(slide, 4.45, 4.6, 3.2, 1.05, "资料来源", "working 目录中的梳理文档、流程图和源码定位", "white", "green", "green")
    deck.card(slide, 7.95, 4.6, 3.2, 1.05, "用途", "为项目移植、风险评估和接口改造提供资料", "white", "purple", "purple")
    deck.add_footer(slide)

    # 2 deck map
    slide = deck.blank()
    deck.title(slide, "资料包结构", "从可视化流程、程序机制、风险到移植步骤，按实际迁移顺序组织。")
    cards = [
        ("1. 范围与入口", "程序定位、资料文件、核心依赖", "blue_light", "blue"),
        ("2. 主流程", "请求解析、Type 首层分发、响应出口", "green_light", "green"),
        ("3. SqlWebCall", "初始化、二次分发、参数处理、数据库执行", "purple_light", "purple"),
        ("4. 迁移资料", "配置、依赖、测试矩阵、风险和建议", "amber_light", "amber"),
    ]
    for i, (title, body, fill, line) in enumerate(cards):
        deck.card(slide, 0.8 + i * 3.1, 1.65, 2.75, 1.3, title, body, fill, line, line.replace("_light", "") if "_light" in line else line)
    deck.bullets(
        slide,
        0.9,
        3.45,
        11.8,
        2.25,
        [
            "PPT 中的流程图采用完整图 + 局部放大方式，避免大图缩放后看不清。",
            "Markdown 文档保留在 working 中，PPT 只提取迁移分析需要的结论和路径。",
            "源 Mermaid 文件、PNG、渲染脚本一并保留，后续可继续改图或补充页面。",
        ],
        font_size=14,
    )
    deck.add_footer(slide)

    # 3 file index
    slide = deck.blank()
    deck.title(slide, "working 目录资料索引", "PPT 已整合这些文件，原文件仍保留作为可追溯资料。")
    data = [
        ["文件", "用途"],
        ["MESCommonBase程序梳理.md", "完整梳理入口、Type 分支、文件上传下载和风险点"],
        ["MESCommonBase流程图.mmd / .png", "主程序总览流程图"],
        ["MESCommonBase响应输出与SqlWebCall默认分支详细文档.md", "响应输出和 SqlWebCall 默认分支专项说明"],
        ["响应输出与SqlWebCall主流程图.mmd / .png", "主图：首层分支与 HTTP 响应出口"],
        ["响应输出与SqlWebCall次流程图.mmd / .png", "次图：SqlWebCall 内部二次分发与数据库链路"],
        ["render_*.py / build_*.py", "流程图和 PPT 自动生成脚本"],
    ]
    deck.table(slide, 0.7, 1.35, 11.95, 4.9, data, col_widths=[4.4, 7.55], font_size=9.2)
    deck.add_footer(slide)

    # 4 program positioning
    slide = deck.blank()
    deck.title(slide, "程序定位与迁移目标", "MESCommonBase 是通用提交网关，不是单一业务接口。")
    deck.card(slide, 0.75, 1.35, 3.7, 4.7, "程序定位", "ASP.NET WebHandler\n实现 IHttpHandler\n入口：ProcessRequest\n根据 Type 分流", "blue_light", "blue", "blue")
    deck.card(slide, 4.75, 1.35, 3.7, 4.7, "关键职责", "文件导出/下载\n文件上传\nIP 查询\n通用数据库调用\n响应统一写出", "green_light", "green", "green")
    deck.card(slide, 8.75, 1.35, 3.7, 4.7, "移植目标", "保留协议兼容\n迁移依赖 DLL 和配置\n还原数据库执行能力\n补齐风险控制和验证用例", "purple_light", "purple", "purple")
    deck.add_footer(slide)

    # 5 source dependencies
    slide = deck.blank()
    deck.title(slide, "程序依赖关系", "移植时需要同时关注 Web 入口、业务库、基础库、Excel 生成库和 Web.config。")
    data = [
        ["依赖", "迁移关注点"],
        ["MES_Manage/submit/MESCommonBase.ashx", "WebHandler 入口，需部署到 submit 路径"],
        ["DataLinkMesWork.dll / 源码", "SqlWebCall、存储过程、SQL 执行、文件上传下载"],
        ["BasicData.dll / jsonobj", "旧协议 DTO，Type/Name/Param/token 等字段"],
        ["WriteExcelNPOI.dll / ExcelWebCall", "2001/2002/2003 Excel/PDF 导出"],
        ["Newtonsoft.Json、NPOI、Spire、Lazy.Captcha 等", "bin 目录运行依赖，版本需一致或验证兼容"],
        ["Web.config appSettings ConnectionString", "默认连接串来源，当前使用明文配置"],
    ]
    deck.table(slide, 0.65, 1.35, 12.05, 4.95, data, col_widths=[4.0, 8.05], font_size=9.1)
    deck.add_footer(slide)

    # 6 section
    deck.section_slide("第一部分：主流程与响应输出", "说明 MESCommonBase 如何从 type 分支走向二进制下载、JSON 文本、IP 文本和异常兜底。")

    # 7 full overview
    slide = deck.blank()
    deck.title(slide, "MESCommonBase 总体流程图", "完整图用于把入口解析、Type 分支、响应出口和风险位置放在一张图中查看。")
    deck.add_image_fit(slide, "总流程_完整.png", 0.55, 1.15, 12.25, 5.85)
    deck.add_footer(slide)

    # 8 entry crop
    slide = deck.blank()
    deck.title(slide, "入口解析与 Type 识别", "迁移时要保持三种入参来源的兼容：JSON body、jsonobj、Request[\"param\"]。")
    deck.add_image_fit(slide, "总流程_入口解析.png", 0.65, 1.2, 7.0, 4.9)
    deck.bullets(
        slide,
        8.0,
        1.45,
        4.55,
        4.6,
        [
            "默认 ContentType 设置为 application/json。",
            "先用 JsonMapper 解析，再尝试反序列化 jsonobj。",
            "multipart/form-data 场景通常依赖 Request[\"param\"]。",
            "解析失败被吞掉，type 可能保持默认 0 并进入 default。",
        ],
        font_size=13.2,
        title="入口兼容点",
    )
    deck.add_footer(slide)

    # 9 type branch crop
    slide = deck.blank()
    deck.title(slide, "Type 首层分支总览", "首层分支决定响应出口：文件流、上传 JSON、IP 文本或进入 SqlWebCall。")
    deck.add_image_fit(slide, "总流程_Type分支.png", 0.55, 1.18, 12.2, 4.05)
    data = [
        ["Type", "方向", "输出"],
        ["2001/2002/2003/2004/16", "文件生成或数据库文件读取", "二进制下载"],
        ["15", "文件上传并调用存储过程", "JSON 文本"],
        ["4000", "获取请求 IP", "文本 IP"],
        ["default", "DataLink.SqlWebCall 二次分发", "JSON 或空字符串"],
    ]
    deck.table(slide, 1.0, 5.45, 11.3, 1.25, data, col_widths=[2.7, 5.2, 3.4], font_size=8.5)
    deck.add_footer(slide)

    # 10 response main
    slide = deck.blank()
    deck.title(slide, "响应输出主流程图", "主图将四类响应出口分开：文件下载、上传 JSON、IP 文本、default JSON。")
    deck.add_image_fit(slide, "响应主_分支出口.png", 0.55, 1.15, 12.25, 5.75)
    deck.add_footer(slide)

    # 11 response rules
    slide = deck.blank()
    deck.title(slide, "响应规则与异常兜底", "迁移时要特别关注 Response.End、空响应和异常不记录的问题。")
    deck.add_image_fit(slide, "响应主_规则异常.png", 0.7, 1.22, 11.9, 3.2)
    deck.bullets(
        slide,
        1.0,
        4.75,
        11.2,
        1.55,
        [
            "文件下载统一设置 application/octet-stream 和 Content-Disposition，前端通过响应头取文件名。",
            "外层 catch 只写 responseText，默认值为 NULL，不设置 HTTP 状态码。",
            "Type=16 若 bytes 为 null 直接 return，调用方可能收到空响应。",
        ],
        font_size=13.1,
    )
    deck.add_footer(slide)

    # 12 section
    deck.section_slide("第二部分：SqlWebCall 默认分支", "说明普通业务请求进入 DataLink.SqlWebCall 后如何初始化、二次分发、解析参数、执行数据库并返回。")

    # 13 secondary full
    slide = deck.blank()
    deck.title(slide, "SqlWebCall 默认分支完整图", "完整图用于快速定位初始化、四类 Type、数据库执行和返回出口。")
    deck.add_image_fit(slide, "SqlWebCall_完整.png", 0.55, 1.1, 12.25, 5.9)
    deck.add_footer(slide)

    # 14 init
    slide = deck.blank()
    deck.title(slide, "SqlWebCall 初始化流程", "第一次进入时读取 Web.config 的 ConnectionString，并通过静态标记复用。")
    deck.add_image_fit(slide, "SqlWebCall_初始化.png", 0.75, 1.25, 7.0, 3.05)
    deck.bullets(
        slide,
        8.1,
        1.35,
        4.45,
        3.7,
        [
            "InitSystemReg 当前直接读取 appSettings[\"ConnectionString\"]。",
            "原注册号、序列号、加密连接串校验逻辑已注释。",
            "移植后需确保 Web.config 中连接串名称和权限可用。",
        ],
        font_size=13,
        title="迁移关注点",
    )
    deck.card(slide, 0.95, 5.35, 11.35, 0.75, "结论", "数据库连接配置是 SqlWebCall 能否工作的第一检查点；同时建议在移植阶段评估明文连接串和账号权限。", "red_light", "red", "red")
    deck.add_footer(slide)

    # 15 type secondary
    slide = deck.blank()
    deck.title(slide, "SqlWebCall Type 二次分发", "default 分支内部按 Type 再拆成四类：登录/加密、旧协议、新协议、SQL/命令。")
    deck.add_image_fit(slide, "SqlWebCall_Type二次分发.png", 0.55, 1.12, 12.25, 5.9)
    deck.add_footer(slide)

    # 16 type table
    slide = deck.blank()
    deck.title(slide, "SqlWebCall Type 能力表", "迁移测试时可按这些 Type 分组准备用例。")
    data = [
        ["分类", "Type", "方法/能力", "迁移重点"],
        ["登录/加密", "8888/5001/5002", "加密、注册/改密、登录生成 token", "确认加密逻辑和 token 存储过程"],
        ["旧协议存储过程", "1/2/5", "jsonobj + 拼接字符串 Param", "兼容旧前端参数格式"],
        ["新协议存储过程", "11/111/12/13/21", "JsonData + JSON 数组字符串 Param", "确认存储过程名、参数、分页输出"],
        ["SQL/命令", "1001/1002/3/4/7/22/3001", "直接 SQL、建表导入、命令执行", "强烈建议白名单和权限隔离"],
    ]
    deck.table(slide, 0.55, 1.3, 12.25, 4.3, data, col_widths=[2.0, 2.3, 4.2, 3.75], font_size=8.2)
    deck.card(slide, 0.75, 6.0, 11.85, 0.65, "关键判断", "这部分是移植风险最大的区域：协议兼容、数据库权限、SQL 执行能力、token 逻辑都集中在 SqlWebCall 下游。", "amber_light", "amber", "amber")
    deck.add_footer(slide)

    # 17 param protocol
    slide = deck.blank()
    deck.title(slide, "参数协议：新协议与旧协议", "迁移时要避免把 Param 当作普通对象，它在新协议中通常是 JSON 数组字符串。")
    deck.card(slide, 0.7, 1.25, 5.9, 4.7, "新协议 Param", "适用：11/111/12/13/15/16/21/2004 等\n\n格式：\n[{\"name\":\"@p\",\"value\":\"v\",\"type\":\"string\"}]\n\n特点：\n- Param 字段本身是字符串\n- 支持 type 和 output\n- 数组 value 会转逗号字符串", "green_light", "green", "green")
    deck.card(slide, 6.95, 1.25, 5.65, 4.7, "旧协议 Param", "适用：1/2/5 等\n\nType 1/2：\n@p=value=type&@out=0=int=output\n\nType 5：\n@p&value&type|@p2&value&type\n\n特点：\n- 分隔符敏感\n- 值包含 &、=、| 时容易出错", "purple_light", "purple", "purple")
    deck.add_footer(slide)

    # 18 db and return
    slide = deck.blank()
    deck.title(slide, "数据库执行链路与返回格式", "SqlWebCall 只路由，真正执行在 DataLinkMesWork.cs 和 SQLCommon.cs。")
    deck.add_image_fit(slide, "SqlWebCall_数据库返回.png", 0.62, 1.15, 12.05, 4.0)
    deck.bullets(
        slide,
        0.85,
        5.45,
        11.8,
        1.1,
        [
            "存储过程使用 ExecuteStoredProcedure，CommandType=StoredProcedure，CommandTimeout=0。",
            "SQL 查询使用 ExecuteSelectMesWork / ExecuteDataTable / ExecuteDataset。",
            "返回格式混合：DataTable JSON、result=1/0、rows-total、code-message-data、空字符串。",
        ],
        font_size=12.6,
    )
    deck.add_footer(slide)

    # 19 risk
    slide = deck.blank()
    deck.title(slide, "风险与迁移控制点", "这些问题不一定阻塞移植，但会影响上线后的安全性和可维护性。")
    left = [
        "token 多数是非空才校验；不传 token 的路径常见不拒绝。",
        "客户端可控制 Name/name，可能是存储过程名、SQL 文本或表名。",
        "Type=7 可按请求内容 DROP/CREATE 表。",
        "CommandTimeout=0，慢 SQL 或阻塞存储过程可能长期占用请求。",
    ]
    right = [
        "下载异常和 bytes=null 缺少统一错误响应。",
        "外层 catch 不记录日志，排障成本高。",
        "明文连接串和数据库账号权限需要重新评估。",
        "响应格式不统一，前端和联调工具需按 Type 分别判断。",
    ]
    deck.card(slide, 0.75, 1.28, 5.85, 4.9, "SqlWebCall 风险", "\n".join(f"• {x}" for x in left), "red_light", "red", "red")
    deck.card(slide, 6.95, 1.28, 5.65, 4.9, "响应与部署风险", "\n".join(f"• {x}" for x in right), "amber_light", "amber", "amber")
    deck.add_footer(slide)

    # 20 section
    deck.section_slide("第三部分：项目移植资料", "给迁移实施人员使用：环境、配置、文件、数据库、接口验证和上线前检查。")

    # 21 migration checklist
    slide = deck.blank()
    deck.title(slide, "移植检查清单", "按部署前必须确认的对象拆分。")
    data = [
        ["类别", "检查项", "说明"],
        ["运行环境", ".NET Framework 4.8 / IIS / ASP.NET", "WebHandler 运行环境需一致或验证兼容"],
        ["Web 文件", "MES_Manage/submit/MESCommonBase.ashx", "路径和 Class 名需保持"],
        ["DLL 依赖", "Bin 下 DataLinkMesWork、BasicData、WriteExcelNPOI 等", "版本差异会影响运行和导出"],
        ["配置", "Web.config / ConnectionString / CORS", "连接串、跨域、默认文档、handlers"],
        ["数据库", "存储过程、表、权限、token 过程", "SqlWebCall 能力依赖数据库对象"],
        ["文件导出", "NPOI、Spire、模板逻辑", "2001/2002/2003 验证"],
        ["安全", "token、白名单、日志、账号权限", "建议迁移阶段同步补强"],
    ]
    deck.table(slide, 0.55, 1.28, 12.25, 5.25, data, col_widths=[1.5, 4.4, 6.35], font_size=8.5)
    deck.add_footer(slide)

    # 22 migration steps
    slide = deck.blank()
    deck.title(slide, "建议移植实施顺序", "先让原协议跑通，再逐步补齐安全和可维护性。")
    steps = [
        ("1", "环境准备", ".NET/IIS/站点路径/应用程序池"),
        ("2", "文件迁移", "ashx、Bin、Web.config、模板和静态资源"),
        ("3", "数据库迁移", "库表、存储过程、权限、token 相关过程"),
        ("4", "接口冒烟", "15/16/2001/2002/4000/default 常用 Type"),
        ("5", "风险补强", "鉴权、白名单、日志、连接串和权限"),
        ("6", "回归验收", "按 Type 分组准备测试矩阵和下载校验"),
    ]
    for i, (num, title, body) in enumerate(steps):
        x = 0.75 + (i % 3) * 4.05
        y = 1.35 + (i // 3) * 2.35
        deck.card(slide, x, y, 3.55, 1.65, f"{num}. {title}", body, "white", "blue" if i < 3 else "green", "navy")
    deck.card(slide, 1.1, 6.3, 11.1, 0.55, "执行原则", "先保持协议兼容，再处理风险收敛；不要在未完成回归测试前重构 Param 协议或响应格式。", "amber_light", "amber", "amber")
    deck.add_footer(slide)

    # 23 test matrix
    slide = deck.blank()
    deck.title(slide, "迁移验证用例矩阵", "每组至少准备一个成功、一个失败、一个边界场景。")
    data = [
        ["场景", "覆盖 Type", "验证点"],
        ["Excel/PDF 导出", "2001/2002/2003", "文件名、扩展名、下载流、跨域响应头"],
        ["数据库文件下载", "16/2004", "bytes 为空、文件名、后缀、二进制一致性"],
        ["文件上传", "15", "无文件、有文件、文件名无扩展名、大文件"],
        ["IP 查询", "4000", "代理头、REMOTE_ADDR、UserHostAddress"],
        ["旧协议", "1/2/5", "Param 分隔符、输出参数、分页、token"],
        ["新协议", "11/111/12/13/21", "JSON 数组 Param、分页、DataSet、新结构返回"],
        ["SQL 执行", "1001/1002/3/4/7/22/3001", "SQL 参数、权限、白名单、错误处理"],
    ]
    deck.table(slide, 0.55, 1.25, 12.25, 5.45, data, col_widths=[2.35, 2.5, 7.4], font_size=8.3)
    deck.add_footer(slide)

    # 24 recommended improvements
    slide = deck.blank()
    deck.title(slide, "移植后的改造建议", "不建议在第一天全部重构，但建议列入上线前后计划。")
    deck.card(slide, 0.75, 1.25, 3.75, 4.95, "短期", "• 统一 token 鉴权\n• 未支持 Type 返回明确错误\n• 下载空文件返回可识别错误\n• 外层 catch 写日志\n• 修正 Type=4000 IP 逻辑", "green_light", "green", "green")
    deck.card(slide, 4.8, 1.25, 3.75, 4.95, "中期", "• 建立 Type + Name 白名单\n• Param 从字符串迁移为 JSON 数组\n• 统一 JSON 返回结构\n• SQL 执行能力与普通接口分离", "blue_light", "blue", "blue")
    deck.card(slide, 8.85, 1.25, 3.75, 4.95, "长期", "• 按业务模块拆分接口\n• 数据库账号最小权限\n• 移除明文连接串\n• 加入审计日志和限流", "purple_light", "purple", "purple")
    deck.add_footer(slide)

    # 25 appendix
    slide = deck.blank()
    deck.title(slide, "附录：资料追溯与后续维护", "PPT 是汇报和迁移视图，Markdown 与 Mermaid 是详细资料和可维护源。")
    deck.bullets(
        slide,
        0.9,
        1.35,
        11.9,
        4.6,
        [
            "详细文字资料：MESCommonBase程序梳理.md、MESCommonBase响应输出与SqlWebCall默认分支详细文档.md。",
            "流程图源文件：MESCommonBase流程图.mmd、MESCommonBase响应输出与SqlWebCall主流程图.mmd、MESCommonBase响应输出与SqlWebCall次流程图.mmd。",
            "高清流程图：三张 PNG 已在 working 中保存，PPT 中按完整图和局部截图引用。",
            "自动生成脚本：render_*.py 与 build_mescommonbase_migration_ppt.py，可在文档更新后重新生成资料。",
        ],
        font_size=14,
    )
    deck.card(slide, 1.1, 6.1, 11.1, 0.65, "输出文件", "working/MESCommonBase迁移分析资料包.pptx", "white", "blue", "blue")
    deck.add_footer(slide)

    deck.save()


if __name__ == "__main__":
    build_deck()
    print(OUT)
