import { readFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

const PPTX_MIME = "application/vnd.openxmlformats-officedocument.presentationml.presentation";
const MARKDOWN_MIME = "text/markdown; charset=utf-8";
const HTML_MIME = "text/html; charset=utf-8";
const GUIZANG_SWISS_TEMPLATE = join(dirname(fileURLToPath(import.meta.url)), "../vendor/guizang/template-swiss.html");
const COURSEWARE_IMAGE_PATH = join(dirname(fileURLToPath(import.meta.url)), "../vendor/guizang/courseware-icon.png");

const BRAND = {
  ink: "0A0A0A",
  muted: "737373",
  paper: "FAFAF8",
  white: "FFFFFF",
  coral: "FF6B35",
  blue: "FF6B35",
  teal: "3B3B3B",
  gold: "FF6B35",
  lilac: "737373",
  paleCoral: "FBE6DD",
  paleBlue: "F0F0EE",
  paleTeal: "F0F0EE",
  paleGold: "F0F0EE",
  accentOn: "FFFFFF",
};

const BRAND_THEMES = Object.freeze([
  { name: "克莱因蓝", paper: "F7F7F4", ink: "0A0A0A", muted: "686868", accent: "002FA7", accentRgb: "0,47,167", accentOn: "FFFFFF", accentBright: "2858C7", accentTint: "E6ECFA" },
  { name: "安全橙", paper: "FAFAF8", ink: "0A0A0A", muted: "737373", accent: "FF6B35", accentRgb: "255,107,53", accentOn: "FFFFFF", accentBright: "FF9A73", accentTint: "FBE6DD" },
  { name: "柠檬黄", paper: "FAFAF8", ink: "0A0A0A", muted: "737373", accent: "FFD500", accentRgb: "255,213,0", accentOn: "0A0A0A", accentBright: "FFE75E", accentTint: "FFF5CC" },
  { name: "柠檬绿", paper: "FAFAF8", ink: "0A0A0A", muted: "737373", accent: "C5E803", accentRgb: "197,232,3", accentOn: "0A0A0A", accentBright: "D8F24A", accentTint: "EFF7BF" },
]);

function applyBrandTheme(spec) {
  const content = `${spec.title}\n${spec.subtitle}`;
  // 对应 guizang-ppt-skill 的 Swiss 预设色。按语义选一套，一份 deck 只保留一个 accent。
  const theme = /(?:绿色|可持续|健康|生态|自然)/u.test(content)
    ? BRAND_THEMES[3]
    : /(?:零售|消费|年轻|活力|品牌)/u.test(content)
      ? BRAND_THEMES[2]
      : /(?:AI|人工智能|技术|工程|数据|系统|软件)/iu.test(content)
        ? BRAND_THEMES[0]
        : BRAND_THEMES[1];
  Object.assign(BRAND, {
    ink: theme.ink,
    muted: theme.muted,
    paper: theme.paper,
    white: "FFFFFF",
    coral: theme.accent,
    blue: theme.accent,
    teal: "3B3B3B",
    gold: theme.accent,
    lilac: theme.muted,
    paleCoral: theme.accentTint,
    paleBlue: "F0F0EE",
    paleTeal: "F0F0EE",
    paleGold: "F0F0EE",
    accentOn: theme.accentOn,
  });
  return theme;
}

const FONT_FACE = "Microsoft YaHei";
const MAX_SLIDES = 40;
const MAX_DECK_TITLE = 80;
const MAX_SUBTITLE = 160;
const MAX_AUTHOR = 60;
const MAX_SLIDE_TITLE = 72;
const MAX_BODY_ITEM = 240;
const MAX_BODY_ITEMS = 8;
const MAX_BODY_TOTAL = 1_200;
const MAX_NOTES = 4_000;

// PPTX 生成器属于生产能力，启动后复用同一个加载结果，避免每次导出才发现依赖缺失。
let pptxGeneratorPromise;

async function loadPptxGenerator() {
  if (!pptxGeneratorPromise) {
    pptxGeneratorPromise = import("pptxgenjs")
      .then((module) => module.default ?? module)
      .catch((error) => {
        const dependencyError = new Error("当前服务未安装 PPTX 生成组件，请联系管理员修复运行环境");
        dependencyError.code = "PPTXGENJS_MISSING";
        dependencyError.cause = error;
        throw dependencyError;
      });
  }
  return pptxGeneratorPromise;
}

/**
 * 检查静态 PPTX 生成能力。结果只包含可公开的运行状态，不返回底层异常或路径。
 */
export async function getCoursewareRuntimeStatus() {
  try {
    await loadPptxGenerator();
    return { status: "ready", component: "pptxgenjs" };
  } catch (error) {
    return { status: "unavailable", code: error?.code || "PPTXGENJS_UNAVAILABLE" };
  }
}

// 40 个名称分别对应 40 种轮廓；一套课件内同一名称只能使用一次。
const LAYOUT_TYPES = Object.freeze([
  "cover",
  "statement",
  "split-left",
  "split-right",
  "three-column",
  "four-quadrant",
  "timeline-horizontal",
  "timeline-vertical",
  "process-chevron",
  "staircase",
  "concentric",
  "orbit",
  "comparison",
  "before-after",
  "pyramid",
  "funnel",
  "roadmap",
  "bridge",
  "spotlight",
  "quote",
  "metric",
  "checklist",
  "do-dont",
  "cause-effect",
  "problem-solution",
  "question-answer",
  "ladder",
  "wave",
  "zigzag",
  "stack",
  "spectrum",
  "iceberg",
  "tree",
  "hub-spoke",
  "flywheel",
  "canvas",
  "table",
  "gallery",
  "manifesto",
  "closing",
]);

const LAYOUT_SET = new Set(LAYOUT_TYPES);
const LAYOUT_ALIASES = new Map([
  ["封面", "cover"],
  ["大字报", "statement"],
  ["左分栏", "split-left"],
  ["右分栏", "split-right"],
  ["三列", "three-column"],
  ["十字", "four-quadrant"],
  ["矩阵", "four-quadrant"],
  ["横向时间线", "timeline-horizontal"],
  ["纵向时间线", "timeline-vertical"],
  ["流程", "process-chevron"],
  ["阶梯", "staircase"],
  ["环形", "orbit"],
  ["对比", "comparison"],
  ["前后对比", "before-after"],
  ["金字塔", "pyramid"],
  ["漏斗", "funnel"],
  ["路线图", "roadmap"],
  ["桥梁", "bridge"],
  ["聚光", "spotlight"],
  ["引用", "quote"],
  ["数据", "metric"],
  ["清单", "checklist"],
  ["正反", "do-dont"],
  ["因果", "cause-effect"],
  ["问题方案", "problem-solution"],
  ["问答", "question-answer"],
  ["梯子", "ladder"],
  ["波浪", "wave"],
  ["折线", "zigzag"],
  ["堆叠", "stack"],
  ["光谱", "spectrum"],
  ["冰山", "iceberg"],
  ["树形", "tree"],
  ["中心辐射", "hub-spoke"],
  ["飞轮", "flywheel"],
  ["画布", "canvas"],
  ["表格", "table"],
  ["画廊", "gallery"],
  ["宣言", "manifesto"],
  ["结尾", "closing"],
]);

const SAFE_ROOT_KEYS = new Set(["title", "subtitle", "author", "filename", "slides"]);
const SAFE_SLIDE_KEYS = new Set(["title", "body", "layout", "layoutType", "notes"]);

const UNSAFE_CONTENT_PATTERNS = Object.freeze([
  { pattern: /(?:https?|ftp|file|data|javascript|vbscript):/iu, label: "外部 URL 或协议" },
  { pattern: /(?:^|[\s"'(])\/\/[\p{L}\p{N}]/iu, label: "外部 URL" },
  { pattern: /\bwww\s*\./iu, label: "外部 URL" },
  { pattern: /\b[\p{L}\p{N}](?:[\p{L}\p{N}-]{0,61}[\p{L}\p{N}])?\.(?:com|cn|net|org|io|ai|co|gov|edu|biz|info|app|dev|cloud|site|me|xyz|top|tech|online|store|cc|tv)(?:[\/:?#][^\s]*)?/iu, label: "外部 URL" },
  { pattern: /<\s*\/?\s*(?:script|iframe|object|embed|link|meta|svg)\b/iu, label: "脚本或可执行标签" },
  { pattern: /\bon(?:error|load|click|mouseover|focus|submit)\s*=/iu, label: "脚本事件处理器" },
  { pattern: /\b(?:eval|Function|setTimeout|setInterval)\s*\(/u, label: "脚本调用" },
]);

function isPlainObject(value) {
  if (!value || typeof value !== "object" || Array.isArray(value)) return false;
  const prototype = Object.getPrototypeOf(value);
  return prototype === Object.prototype || prototype === null;
}

function assertPlainObject(value, path) {
  if (!isPlainObject(value)) throw new TypeError(`${path} 必须是普通对象`);
}

function assertAllowedKeys(value, allowed, path) {
  for (const key of Object.keys(value)) {
    if (!allowed.has(key)) throw new TypeError(`${path} 包含不支持的字段：${key}`);
  }
}

function normalizeText(value, path, { required = false, maxLength }) {
  if (value == null && !required) return "";
  if (typeof value !== "string") throw new TypeError(`${path} 必须是字符串`);
  const normalized = value.normalize("NFKC").replace(/\r\n?/g, "\n").trim();
  if (required && !normalized) throw new TypeError(`${path} 不能为空`);
  if (normalized.length > maxLength) throw new RangeError(`${path} 不能超过 ${maxLength} 个字符`);
  assertSafeContent(normalized, path);
  return normalized;
}

function assertSafeContent(text, path) {
  for (const { pattern, label } of UNSAFE_CONTENT_PATTERNS) {
    if (pattern.test(text)) throw new TypeError(`${path} 含有禁止的${label}`);
  }
}

function normalizeBody(value, path) {
  if (value == null) return [];
  const rawItems = Array.isArray(value) ? value : [value];
  if (rawItems.length > MAX_BODY_ITEMS) {
    throw new RangeError(`${path} 最多包含 ${MAX_BODY_ITEMS} 条正文`);
  }
  const items = rawItems.map((item, index) =>
    normalizeText(item, `${path}[${index}]`, { required: true, maxLength: MAX_BODY_ITEM }),
  );
  const total = items.reduce((sum, item) => sum + item.length, 0);
  if (total > MAX_BODY_TOTAL) throw new RangeError(`${path} 总长度不能超过 ${MAX_BODY_TOTAL} 个字符`);
  return items;
}

function normalizeLayout(value, index) {
  if (value == null || value === "") return LAYOUT_TYPES[index];
  if (typeof value !== "string") throw new TypeError(`slides[${index}].layoutType 必须是字符串`);
  const candidate = value.normalize("NFKC").trim().toLowerCase();
  const normalized = LAYOUT_ALIASES.get(candidate) ?? candidate;
  if (!LAYOUT_SET.has(normalized)) {
    throw new TypeError(`slides[${index}].layoutType 不受支持：${value}`);
  }
  return normalized;
}

function safeBaseName(value) {
  let base = String(value || "课程课件")
    .normalize("NFKC")
    .replace(/[\u0000-\u001f\u007f]/g, "")
    .replace(/\.(?:pptx|md|markdown)$/iu, "")
    .replace(/[\\/:*?"<>|]+/g, "_")
    .replace(/\.\.+/g, "_")
    .replace(/[^\p{L}\p{N}\s_-]+/gu, "_")
    .replace(/[\s_]+/g, "_")
    .replace(/^[_\s.-]+|[_\s.-]+$/g, "")
    .slice(0, 64);
  if (!base || /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])$/iu.test(base)) base = "课程课件";
  return base;
}

function asDownloadBuffer(value, filename, mimeType) {
  const buffer = Buffer.isBuffer(value) ? value : Buffer.from(value);
  Object.defineProperties(buffer, {
    filename: { value: filename, enumerable: false },
    mimeType: { value: mimeType, enumerable: false },
    contentType: { value: mimeType, enumerable: false },
  });
  return buffer;
}

/**
 * 严格校验并规范化课件结构。返回值不共享输入对象或数组。
 */
export function validateCoursewareSpec(spec) {
  assertPlainObject(spec, "课件参数");
  assertAllowedKeys(spec, SAFE_ROOT_KEYS, "课件参数");

  const title = normalizeText(spec.title, "title", { required: true, maxLength: MAX_DECK_TITLE });
  const subtitle = normalizeText(spec.subtitle, "subtitle", { maxLength: MAX_SUBTITLE });
  const author = normalizeText(spec.author, "author", { maxLength: MAX_AUTHOR });
  const filename = normalizeText(spec.filename, "filename", { maxLength: 120 });

  if (!Array.isArray(spec.slides)) throw new TypeError("slides 必须是数组");
  if (spec.slides.length < 1 || spec.slides.length > MAX_SLIDES) {
    throw new RangeError(`slides 页数必须在 1-${MAX_SLIDES} 页之间`);
  }

  const usedLayouts = new Set();
  const slides = spec.slides.map((slide, index) => {
    assertPlainObject(slide, `slides[${index}]`);
    assertAllowedKeys(slide, SAFE_SLIDE_KEYS, `slides[${index}]`);
    if (slide.layout != null && slide.layoutType != null && slide.layout !== slide.layoutType) {
      throw new TypeError(`slides[${index}] 的 layout 与 layoutType 不能冲突`);
    }
    const layoutType = normalizeLayout(slide.layoutType ?? slide.layout, index);
    if (usedLayouts.has(layoutType)) {
      throw new TypeError(`课件内版式不能重复：${layoutType}`);
    }
    usedLayouts.add(layoutType);
    return {
      title: normalizeText(slide.title, `slides[${index}].title`, {
        required: true,
        maxLength: MAX_SLIDE_TITLE,
      }),
      body: normalizeBody(slide.body, `slides[${index}].body`),
      layoutType,
      notes: normalizeText(slide.notes, `slides[${index}].notes`, { maxLength: MAX_NOTES }),
    };
  });

  return {
    title,
    subtitle,
    author,
    filename: safeBaseName(filename || title),
    slides,
  };
}

function textOptions(overrides = {}) {
  return {
    fontFace: FONT_FACE,
    color: BRAND.ink,
    margin: 0,
    breakLine: false,
    valign: "mid",
    ...overrides,
  };
}

function addText(slide, text, x, y, w, h, overrides = {}) {
  slide.addText(String(text ?? ""), {
    x,
    y,
    w,
    h,
    fit: "shrink",
    ...textOptions(overrides),
  });
}

function addShape(slide, shapeType, x, y, w, h, options = {}) {
  slide.addShape(shapeType, {
    x,
    y,
    w,
    h,
    line: { color: options.lineColor ?? options.fill ?? BRAND.ink, transparency: options.lineTransparency ?? 100, width: options.lineWidth ?? 1 },
    fill: { color: options.fill ?? BRAND.white, transparency: options.transparency ?? 0 },
    rotate: options.rotate,
    radius: options.radius,
  });
}

function addLine(slide, shapeType, x, y, w, h, color = BRAND.ink, width = 2, dash = "solid") {
  let normalizedX = x;
  let normalizedY = y;
  let normalizedW = w;
  let normalizedH = h;
  let flipH = false;
  let flipV = false;
  if (normalizedW < 0) {
    normalizedX += normalizedW;
    normalizedW = Math.abs(normalizedW);
    flipH = true;
  }
  if (normalizedH < 0) {
    normalizedY += normalizedH;
    normalizedH = Math.abs(normalizedH);
    flipV = true;
  }
  slide.addShape(shapeType, {
    x: normalizedX,
    y: normalizedY,
    w: normalizedW,
    h: normalizedH,
    flipH,
    flipV,
    line: { color, width, dashType: dash, beginArrowType: "none", endArrowType: "none" },
    fill: { color, transparency: 100 },
  });
}

function bodyText(items, { numbered = false, separator = "\n" } = {}) {
  return items.map((item, index) => `${numbered ? `${index + 1}. ` : "• "}${item}`).join(separator);
}

function splitItems(items, groups) {
  const result = Array.from({ length: groups }, () => []);
  items.forEach((item, index) => result[index % groups].push(item));
  return result;
}

function addSlideTitle(slide, title, index) {
  addText(slide, `0${index + 1}`.slice(-2), 0.68, 0.48, 0.62, 0.44, {
    fontSize: 14,
    bold: true,
    color: BRAND.coral,
  });
  addText(slide, title, 1.48, 0.38, 10.7, 0.76, {
    fontSize: 32,
    bold: false,
    color: BRAND.ink,
  });
  addLine(slide, "line", 0.68, 1.28, 11.95, 0, "CFCFCB", 1);
}

function addFooter(slide, pageNumber) {
  addText(slide, "创创内部课件", 0.68, 7.0, 2.5, 0.22, {
    fontSize: 9,
    color: "8A8F98",
  });
  addText(slide, String(pageNumber), 11.95, 7.0, 0.68, 0.24, {
    fontSize: 9,
    color: "8A8F98",
    align: "right",
  });
}

function addBodyBlock(slide, items, x, y, w, h, options = {}) {
  addText(slide, bodyText(items, options), x, y, w, h, {
    fontSize: options.fontSize ?? 18,
    color: options.color ?? BRAND.ink,
    valign: options.valign ?? "top",
    breakLine: true,
    paraSpaceAfterPt: options.paraSpaceAfterPt ?? 9,
    bold: options.bold ?? false,
    align: options.align,
  });
}

function renderCover(slide, page, spec) {
  slide.background = { color: BRAND.paper };
  addShape(slide, "rect", 9.42, 0, 3.91, 7.5, { fill: BRAND.coral });
  addText(slide, "创创商学院 / 课程课件", 0.78, 0.62, 3.8, 0.32, {
    fontSize: 11,
    bold: true,
    color: BRAND.muted,
    charSpacing: 1.8,
  });
  addText(slide, spec.title, 0.78, 1.36, 7.82, 2.18, {
    fontSize: 44,
    bold: false,
    color: BRAND.ink,
    valign: "mid",
  });
  if (spec.subtitle) addText(slide, spec.subtitle, 0.82, 3.82, 7.3, 0.72, { fontSize: 22, color: BRAND.muted });
  addLine(slide, "line", 0.8, 5.0, 7.85, 0, BRAND.ink, 1);
  addBodyBlock(slide, page.body.slice(0, 2), 0.82, 5.28, 7.55, 0.86, { fontSize: 18, color: BRAND.ink });
  if (spec.author) addText(slide, spec.author, 0.82, 6.66, 4.1, 0.28, { fontSize: 12, color: BRAND.muted });
  addText(slide, "01", 9.78, 0.78, 3.0, 1.42, { fontSize: 76, bold: false, color: BRAND.accentOn, align: "right" });
  addText(slide, "STATIC PPTX", 9.82, 6.52, 2.98, 0.32, { fontSize: 11, bold: true, color: BRAND.accentOn, align: "right", charSpacing: 1.6 });
}

function renderStatement(slide, page) {
  addShape(slide, "rect", 0.68, 1.66, 0.14, 3.9, { fill: BRAND.coral });
  addText(slide, page.body[0] || page.title, 1.25, 1.76, 10.75, 2.62, { fontSize: 40, bold: false, color: BRAND.ink, valign: "mid" });
  addLine(slide, "line", 1.25, 4.68, 10.7, 0, "CFCFCB", 1);
  addBodyBlock(slide, page.body.slice(1, 4), 1.25, 4.98, 10.55, 1.12, { fontSize: 18, color: BRAND.muted });
}

function renderSplit(slide, page, imageSide) {
  if (imageSide === "left") {
    addShape(slide, "rect", 0.68, 1.62, 4.22, 4.95, { fill: BRAND.coral });
    addText(slide, "01", 1.05, 1.92, 3.3, 0.92, { fontSize: 42, bold: false, color: BRAND.accentOn });
    addText(slide, page.body[0] || "核心判断", 1.05, 3.18, 3.34, 2.12, { fontSize: 26, bold: true, color: BRAND.accentOn, valign: "mid" });
    addBodyBlock(slide, page.body.slice(1, 6), 5.55, 1.86, 6.45, 4.38, { fontSize: 20, numbered: true, paraSpaceAfterPt: 15 });
    return;
  }
  addBodyBlock(slide, page.body.slice(1, 6), 0.9, 1.86, 7.1, 4.38, { fontSize: 20, numbered: true, paraSpaceAfterPt: 15 });
  addShape(slide, "rect", 8.42, 1.62, 4.2, 4.95, { fill: BRAND.coral });
  addText(slide, "01", 8.82, 1.92, 3.3, 0.92, { fontSize: 42, bold: false, color: BRAND.accentOn, align: "right" });
  addText(slide, page.body[0] || "核心判断", 8.82, 3.18, 3.3, 2.12, { fontSize: 26, bold: true, color: BRAND.accentOn, align: "right", valign: "mid" });
}

function renderColumns(slide, page, count) {
  const groups = splitItems(page.body, count);
  const gap = 0.22;
  const totalWidth = 11.95;
  const cardWidth = (totalWidth - gap * (count - 1)) / count;
  const colors = [BRAND.paleBlue, BRAND.paleTeal, BRAND.paleGold, BRAND.paleCoral];
  groups.forEach((items, index) => {
    const x = 0.68 + index * (cardWidth + gap);
    addShape(slide, "rect", x, 1.72 + index * 0.16, cardWidth, 4.55 - index * 0.16, { fill: colors[index] });
    addText(slide, String(index + 1).padStart(2, "0"), x + 0.25, 1.98 + index * 0.16, cardWidth - 0.5, 0.45, { fontSize: 17, bold: true, color: [BRAND.blue, BRAND.teal, BRAND.gold, BRAND.coral][index] });
    addBodyBlock(slide, items, x + 0.25, 2.62 + index * 0.16, cardWidth - 0.5, 2.9 - index * 0.16, { fontSize: count === 3 ? 16 : 14 });
  });
}

function renderQuadrant(slide, page) {
  const groups = splitItems(page.body, 4);
  addLine(slide, "line", 6.65, 1.72, 0, 4.62, "CFCFCB", 1);
  addLine(slide, "line", 0.9, 4.03, 11.45, 0, "CFCFCB", 1);
  const points = [[0.9, 1.72], [6.92, 1.72], [0.9, 4.28], [6.92, 4.28]];
  points.forEach(([x, y], index) => {
    addText(slide, `0${index + 1}`, x, y, 0.72, 0.42, { fontSize: 18, bold: true, color: BRAND.coral });
    addBodyBlock(slide, groups[index], x + 0.9, y + 0.02, 4.65, 1.72, { fontSize: 18, paraSpaceAfterPt: 10 });
  });
}

function renderTimeline(slide, page, vertical) {
  const items = (page.body.length ? page.body : [page.title]).slice(0, vertical ? 6 : 5);
  if (vertical) {
    addLine(slide, "line", 3.0, 1.82, 0, 4.55, BRAND.blue, 3);
    items.forEach((item, index) => {
      const y = 1.85 + index * (4.2 / Math.max(items.length, 1));
      addShape(slide, "ellipse", 2.82, y, 0.36, 0.36, { fill: index % 2 ? BRAND.coral : BRAND.blue });
      addText(slide, String(index + 1).padStart(2, "0"), 1.42, y - 0.02, 1.05, 0.38, { fontSize: 12, bold: true, color: BRAND.muted, align: "right" });
      addText(slide, item, 3.52, y - 0.13, 8.0, 0.68, { fontSize: 16, bold: index === 0 });
    });
  } else {
    addLine(slide, "line", 1.05, 3.72, 11.0, 0, BRAND.coral, 2);
    items.forEach((item, index) => {
      const x = 1.05 + index * (11.0 / Math.max(items.length - 1, 1));
      addShape(slide, "rect", x - 0.09, 3.63, 0.18, 0.18, { fill: BRAND.coral });
      addText(slide, `0${index + 1}`, Math.max(0.78, x - 0.82), index % 2 ? 4.13 : 2.06, 1.64, 0.34, { fontSize: 14, bold: true, color: BRAND.coral, align: "center" });
      addText(slide, item, Math.max(0.78, x - 1.08), index % 2 ? 4.53 : 2.42, 2.16, 0.96, { fontSize: 17, bold: true, align: "center", valign: index % 2 ? "top" : "bottom" });
    });
  }
}

function renderProcess(slide, page) {
  const items = (page.body.length ? page.body : [page.title]).slice(0, 5);
  const w = 10.9 / items.length;
  items.forEach((item, index) => {
    const fill = index % 2 === 0 ? BRAND.coral : BRAND.ink;
    const textColor = index % 2 === 0 ? BRAND.accentOn : BRAND.white;
    const shapeX = 1.0 + index * w;
    const textX = shapeX + 0.42;
    const textW = Math.max(0.65, w - 0.7);
    addShape(slide, "chevron", shapeX, 2.32, w + 0.14, 2.76, { fill });
    addText(slide, `0${index + 1}`, textX, 2.68, textW, 0.4, { fontSize: 14, bold: true, color: textColor });
    addText(slide, item, textX, 3.34, textW, 1.18, { fontSize: 16, bold: true, color: textColor, align: "center", valign: "mid" });
  });
}

function renderStaircase(slide, page) {
  const items = (page.body.length ? page.body : [page.title]).slice(0, 4);
  const stepW = 10.7 / items.length;
  items.forEach((item, index) => {
    const h = 1.4 + index * (2.75 / Math.max(items.length - 1, 1));
    addShape(slide, "rect", 1.0 + index * stepW, 6.28 - h, stepW - 0.08, h, { fill: index === items.length - 1 ? BRAND.coral : "ECECEA" });
    addText(slide, `0${index + 1}`, 1.2 + index * stepW, 6.0 - h, stepW - 0.44, 0.36, { fontSize: 14, bold: true, color: index === items.length - 1 ? BRAND.accentOn : BRAND.coral });
    addText(slide, item, 1.2 + index * stepW, 6.04 - Math.min(h, 1.72), stepW - 0.44, 1.28, { fontSize: 17, bold: true, color: index === items.length - 1 ? BRAND.accentOn : BRAND.ink, valign: "bottom" });
  });
}

function renderConcentric(slide, page, orbit = false) {
  const items = page.body.length ? page.body : [page.title];
  const cx = 6.65;
  const cy = 4.05;
  if (orbit) {
    [4.7, 3.55, 2.35].forEach((diameter) => addShape(slide, "ellipse", cx - diameter / 2, cy - diameter / 2, diameter, diameter, { fill: BRAND.white, transparency: 100, lineColor: "B8C3E8", lineTransparency: 0, lineWidth: 1 }));
    addShape(slide, "ellipse", cx - 0.78, cy - 0.78, 1.56, 1.56, { fill: BRAND.ink });
    addText(slide, page.title, cx - 0.61, cy - 0.47, 1.22, 0.94, { fontSize: 12, bold: true, color: BRAND.white, align: "center" });
    items.slice(0, 6).forEach((item, index) => {
      const angle = (Math.PI * 2 * index) / Math.min(items.length, 6) - Math.PI / 2;
      const x = cx + Math.cos(angle) * 3.1;
      const y = cy + Math.sin(angle) * 1.95;
      addShape(slide, "ellipse", x - 0.55, y - 0.38, 1.1, 0.76, { fill: index % 2 ? BRAND.coral : BRAND.blue });
      addText(slide, item, x - 0.46, y - 0.27, 0.92, 0.54, { fontSize: 10, bold: true, color: BRAND.white, align: "center" });
    });
  } else {
    const colors = [BRAND.paleCoral, BRAND.paleGold, BRAND.paleTeal, BRAND.paleBlue];
    [5.1, 3.95, 2.8, 1.65].forEach((diameter, index) => addShape(slide, "ellipse", cx - diameter / 2, cy - diameter / 2, diameter, diameter, { fill: colors[index] }));
    items.slice(0, 4).forEach((item, index) => addText(slide, item, index % 2 ? 7.8 : 1.2, 2.0 + index * 1.04, 3.2, 0.75, { fontSize: 15, bold: true, align: index % 2 ? "left" : "right" }));
  }
}

function renderComparison(slide, page, beforeAfter = false) {
  const [left, right] = splitItems(page.body, 2);
  if (beforeAfter) {
    addShape(slide, "rect", 0.85, 2.05, 4.65, 3.85, { fill: "EFECE6" });
    addShape(slide, "rightArrow", 5.75, 3.35, 1.85, 1.1, { fill: BRAND.coral });
    addShape(slide, "rect", 7.85, 1.72, 4.65, 4.5, { fill: BRAND.paleBlue });
  } else {
    addShape(slide, "rect", 0.85, 1.78, 5.58, 4.5, { fill: BRAND.paleBlue });
    addShape(slide, "rect", 6.9, 1.78, 5.58, 4.5, { fill: BRAND.paleCoral });
    addShape(slide, "ellipse", 6.27, 3.3, 0.8, 0.8, { fill: BRAND.ink });
    addText(slide, "VS", 6.4, 3.54, 0.54, 0.24, { fontSize: 11, bold: true, color: BRAND.white, align: "center" });
  }
  addBodyBlock(slide, left, 1.25, 2.25, 3.9, 3.1, { fontSize: 16 });
  addBodyBlock(slide, right, 8.25, 2.25, 3.85, 3.1, { fontSize: 16 });
}

function renderPyramid(slide, page, funnel = false) {
  const items = (page.body.length ? page.body : [page.title]).slice(0, 6);
  const levels = [...items];
  if (funnel) levels.reverse();
  levels.forEach((item, index) => {
    const width = funnel ? 9.5 - index * 1.2 : 4.2 + index * 1.15;
    const x = 6.65 - width / 2;
    const y = 1.78 + index * (4.6 / levels.length);
    addShape(slide, funnel ? "trapezoid" : "rect", x, y, width, 0.58, { fill: [BRAND.blue, BRAND.teal, BRAND.gold, BRAND.coral, BRAND.lilac][index % 5] });
    addText(slide, item, x + 0.2, y + 0.08, width - 0.4, 0.38, { fontSize: 12, bold: true, color: BRAND.white, align: "center" });
  });
}

function renderRoadmap(slide, page, bridge = false) {
  const items = page.body.length ? page.body : [page.title];
  if (bridge) {
    addLine(slide, "arc", 1.2, 2.4, 10.8, 3.5, BRAND.blue, 5);
    addShape(slide, "rect", 0.9, 5.32, 2.35, 0.85, { fill: BRAND.ink });
    addShape(slide, "rect", 10.08, 5.32, 2.35, 0.85, { fill: BRAND.coral });
  } else {
    addLine(slide, "line", 1.1, 2.5, 10.8, 3.1, BRAND.ink, 5);
  }
  items.slice(0, 6).forEach((item, index) => {
    const x = 1.18 + index * (10.1 / Math.max(items.length - 1, 1));
    const y = bridge ? 4.78 - Math.sin((Math.PI * index) / Math.max(items.length - 1, 1)) * 2.05 : 2.85 + index * (2.85 / Math.max(items.length - 1, 1));
    addShape(slide, "ellipse", x - 0.18, y - 0.18, 0.36, 0.36, { fill: index === items.length - 1 ? BRAND.coral : BRAND.white, lineColor: BRAND.ink, lineTransparency: 0 });
    addText(slide, item, x - 0.68, y + (index % 2 ? 0.38 : -0.85), 1.36, 0.65, { fontSize: 11, bold: true, align: "center" });
  });
}

function renderSpotlight(slide, page, quote = false) {
  if (quote) {
    addText(slide, "“", 0.95, 1.62, 1.6, 1.35, { fontSize: 72, bold: true, color: BRAND.coral });
    addText(slide, page.body[0] || page.title, 2.1, 2.1, 9.55, 2.15, { fontSize: 29, bold: true, align: "center" });
    addLine(slide, "line", 4.85, 4.72, 3.65, 0, BRAND.coral, 3);
    addBodyBlock(slide, page.body.slice(1), 3.05, 5.05, 7.3, 0.9, { fontSize: 15, align: "center", color: BRAND.muted });
  } else {
    addShape(slide, "ellipse", 4.62, 1.65, 4.05, 4.05, { fill: BRAND.paleGold });
    addShape(slide, "ellipse", 5.32, 2.35, 2.65, 2.65, { fill: BRAND.coral });
    addText(slide, page.body[0] || page.title, 5.62, 2.75, 2.05, 1.8, { fontSize: 24, bold: true, color: BRAND.white, align: "center" });
    addBodyBlock(slide, page.body.slice(1), 9.1, 2.1, 3.05, 3.3, { fontSize: 15 });
  }
}

function renderMetric(slide, page) {
  const [headline = "关键数字", ...rest] = page.body;
  addText(slide, headline, 0.9, 1.72, 7.55, 2.6, { fontSize: 48, bold: true, color: BRAND.blue, valign: "bottom" });
  addLine(slide, "line", 0.9, 4.62, 7.45, 0, BRAND.coral, 5);
  addBodyBlock(slide, rest, 0.92, 4.95, 7.4, 1.1, { fontSize: 17 });
  addShape(slide, "rect", 9.12, 1.72, 3.12, 4.48, { fill: BRAND.ink });
  addText(slide, String(rest.length).padStart(2, "0"), 9.65, 2.2, 2.05, 1.25, { fontSize: 36, bold: true, color: BRAND.white, align: "center" });
  addText(slide, "关键信号", 9.65, 3.75, 2.05, 0.5, { fontSize: 14, color: BRAND.white, align: "center" });
}

function renderChecklist(slide, page) {
  page.body.forEach((item, index) => {
    const y = 1.72 + index * (4.75 / Math.max(page.body.length, 1));
    addShape(slide, "ellipse", 1.02, y, 0.48, 0.48, { fill: BRAND.teal });
    addText(slide, "✓", 1.12, y + 0.06, 0.28, 0.28, { fontSize: 13, bold: true, color: BRAND.white, align: "center" });
    addText(slide, item, 1.82, y - 0.02, 9.7, 0.55, { fontSize: 17, bold: index === 0 });
    addLine(slide, "line", 1.82, y + 0.62, 9.7, 0, "D9D5CD", 1);
  });
}

function renderDual(slide, page, labels, leftColor, rightColor) {
  const [left, right] = splitItems(page.body, 2);
  addShape(slide, "roundRect", 0.9, 1.72, 5.35, 4.55, { fill: leftColor });
  addShape(slide, "roundRect", 7.08, 1.72, 5.35, 4.55, { fill: rightColor });
  addText(slide, labels[0], 1.3, 2.02, 4.55, 0.5, { fontSize: 19, bold: true, color: BRAND.white, align: "center" });
  addText(slide, labels[1], 7.48, 2.02, 4.55, 0.5, { fontSize: 19, bold: true, color: BRAND.white, align: "center" });
  addBodyBlock(slide, left, 1.32, 2.85, 4.5, 2.8, { fontSize: 15, color: BRAND.white });
  addBodyBlock(slide, right, 7.5, 2.85, 4.5, 2.8, { fontSize: 15, color: BRAND.white });
}

function renderProblemSolution(slide, page) {
  const [problem, solution] = splitItems(page.body, 2);
  addShape(slide, "roundRect", 0.92, 1.82, 4.15, 2.1, { fill: BRAND.ink });
  addText(slide, "问题", 1.28, 2.08, 0.85, 0.42, { fontSize: 17, bold: true, color: BRAND.white });
  addBodyBlock(slide, problem, 1.3, 2.62, 3.35, 0.95, { fontSize: 14, color: BRAND.white });
  addShape(slide, "downArrow", 4.15, 4.16, 1.25, 1.35, { fill: BRAND.coral });
  addShape(slide, "roundRect", 5.55, 3.12, 6.82, 3.05, { fill: BRAND.paleBlue, lineColor: BRAND.blue, lineTransparency: 0, lineWidth: 2 });
  addText(slide, "解决方案", 6.0, 3.48, 1.75, 0.5, { fontSize: 20, bold: true, color: BRAND.blue });
  addBodyBlock(slide, solution, 6.02, 4.17, 5.8, 1.45, { fontSize: 16, numbered: true });
}

function renderQuestionAnswer(slide, page) {
  const [question, ...answers] = page.body;
  addShape(slide, "ellipse", 0.95, 1.72, 2.15, 2.15, { fill: BRAND.lilac });
  addText(slide, "Q", 1.5, 2.16, 1.05, 1.05, { fontSize: 38, bold: true, color: BRAND.white, align: "center" });
  addText(slide, question || page.title, 3.55, 1.92, 8.3, 1.55, { fontSize: 24, bold: true });
  addLine(slide, "line", 3.55, 3.72, 8.3, 0, BRAND.lilac, 3);
  addShape(slide, "rect", 1.75, 4.22, 10.2, 1.92, { fill: BRAND.paleTeal });
  addText(slide, "A", 2.12, 4.62, 0.75, 0.75, { fontSize: 28, bold: true, color: BRAND.teal, align: "center" });
  addBodyBlock(slide, answers, 3.18, 4.52, 8.15, 1.12, { fontSize: 17 });
}

function renderCauseEffect(slide, page) {
  const items = page.body.length ? page.body : [page.title];
  items.forEach((item, index) => {
    const x = index % 2 ? 7.35 : 1.05;
    const y = 1.76 + index * (4.45 / Math.max(items.length, 1));
    addShape(slide, index % 2 ? "rightArrow" : "leftArrow", x, y, 4.5, 0.66, { fill: index % 2 ? BRAND.coral : BRAND.blue });
    addText(slide, item, x + 0.45, y + 0.1, 3.55, 0.4, { fontSize: 12, bold: true, color: BRAND.white, align: "center" });
  });
  addShape(slide, "ellipse", 5.75, 3.35, 1.85, 1.85, { fill: BRAND.ink });
  addText(slide, "因果", 6.15, 3.87, 1.05, 0.55, { fontSize: 18, bold: true, color: BRAND.white, align: "center" });
}

function renderLadder(slide, page) {
  const items = page.body.length ? page.body : [page.title];
  addLine(slide, "line", 3.1, 1.75, 2.25, 4.6, BRAND.ink, 5);
  addLine(slide, "line", 7.95, 1.75, 2.25, 4.6, BRAND.ink, 5);
  items.forEach((item, index) => {
    const y = 5.9 - index * (3.85 / Math.max(items.length - 1, 1));
    const x = 3.65 + index * 0.45;
    addLine(slide, "line", x, y, 4.35, 0, index === items.length - 1 ? BRAND.coral : BRAND.blue, 7);
    addText(slide, item, x + 0.3, y - 0.58, 3.75, 0.48, { fontSize: 12, bold: true, align: "center" });
  });
}

function renderWave(slide, page, zigzag = false) {
  const items = page.body.length ? page.body : [page.title];
  items.forEach((item, index) => {
    const x = 1.05 + index * (10.55 / Math.max(items.length - 1, 1));
    const y = zigzag ? (index % 2 ? 4.95 : 2.25) : 3.65 + Math.sin(index * 1.55) * 1.45;
    if (index > 0) {
      const px = 1.05 + (index - 1) * (10.55 / Math.max(items.length - 1, 1));
      const py = zigzag ? ((index - 1) % 2 ? 4.95 : 2.25) : 3.65 + Math.sin((index - 1) * 1.55) * 1.45;
      addLine(slide, "line", px, py, x - px, y - py, zigzag ? BRAND.ink : BRAND.teal, 4);
    }
    addShape(slide, zigzag ? "diamond" : "ellipse", x - 0.28, y - 0.28, 0.56, 0.56, { fill: index % 2 ? BRAND.coral : BRAND.blue });
    addText(slide, item, x - 0.7, y + (y > 3.65 ? -1.0 : 0.45), 1.4, 0.65, { fontSize: 11, bold: true, align: "center" });
  });
}

function renderStack(slide, page) {
  const items = page.body.length ? page.body : [page.title];
  items.forEach((item, index) => {
    const x = 1.15 + index * 0.55;
    const y = 5.75 - index * 0.58;
    const w = 10.9 - index * 1.1;
    addShape(slide, "rect", x, y, w, 0.58, { fill: [BRAND.ink, BRAND.blue, BRAND.teal, BRAND.gold, BRAND.coral][index % 5] });
    addText(slide, item, x + 0.28, y + 0.08, w - 0.56, 0.38, { fontSize: 12, bold: true, color: BRAND.white, align: "center" });
  });
}

function renderSpectrum(slide, page) {
  const colors = [BRAND.blue, BRAND.teal, BRAND.gold, BRAND.coral, BRAND.lilac];
  addShape(slide, "rect", 1.0, 3.15, 11.3, 0.9, { fill: BRAND.white });
  colors.forEach((color, index) => addShape(slide, "rect", 1.0 + index * 2.26, 3.15, 2.26, 0.9, { fill: color }));
  page.body.forEach((item, index) => {
    const x = 1.0 + index * (10.7 / Math.max(page.body.length - 1, 1));
    addLine(slide, "line", x, 2.72, 0, 1.78, BRAND.ink, 1);
    addText(slide, item, x - 0.7, index % 2 ? 4.58 : 1.75, 1.4, 0.72, { fontSize: 11, bold: true, align: "center" });
  });
}

function renderIceberg(slide, page) {
  addShape(slide, "triangle", 4.0, 1.72, 5.3, 2.15, { fill: BRAND.paleBlue });
  addShape(slide, "triangle", 3.2, 3.82, 6.9, 2.45, { fill: BRAND.blue, rotate: 180 });
  addLine(slide, "line", 0.9, 3.82, 11.55, 0, BRAND.teal, 3, "dash");
  const [visible, hidden] = splitItems(page.body, 2);
  addBodyBlock(slide, visible, 0.95, 1.95, 2.65, 1.55, { fontSize: 13, align: "right" });
  addBodyBlock(slide, hidden, 9.7, 4.3, 2.55, 1.65, { fontSize: 13 });
}

function renderTree(slide, page) {
  const items = page.body.length ? page.body : [page.title];
  addShape(slide, "rect", 6.25, 3.3, 0.8, 2.55, { fill: BRAND.gold });
  addShape(slide, "ellipse", 4.15, 1.72, 5.0, 3.35, { fill: BRAND.paleTeal });
  items.forEach((item, index) => {
    const x = 1.0 + index * (10.7 / Math.max(items.length - 1, 1));
    const y = 2.25 + (index % 3) * 0.82;
    addShape(slide, "ellipse", x - 0.45, y, 0.9, 0.65, { fill: index % 2 ? BRAND.teal : BRAND.blue });
    addText(slide, item, x - 0.38, y + 0.1, 0.76, 0.42, { fontSize: 9, bold: true, color: BRAND.white, align: "center" });
  });
}

function renderHubSpoke(slide, page, flywheel = false) {
  const items = page.body.length ? page.body : [page.title];
  const cx = 6.65;
  const cy = 4.05;
  if (flywheel) addShape(slide, "circularArrow", 4.3, 1.72, 4.7, 4.7, { fill: BRAND.paleBlue, lineColor: BRAND.blue, lineTransparency: 0 });
  addShape(slide, "ellipse", cx - 0.9, cy - 0.9, 1.8, 1.8, { fill: BRAND.ink });
  addText(slide, page.title, cx - 0.67, cy - 0.45, 1.34, 0.9, { fontSize: 11, bold: true, color: BRAND.white, align: "center" });
  items.slice(0, 7).forEach((item, index) => {
    const angle = (Math.PI * 2 * index) / Math.min(items.length, 7) - Math.PI / 2;
    const x = cx + Math.cos(angle) * 3.8;
    const y = cy + Math.sin(angle) * 2.25;
    if (!flywheel) addLine(slide, "line", cx, cy, x - cx, y - cy, "B4BBC5", 1.5);
    addShape(slide, "roundRect", x - 0.72, y - 0.37, 1.44, 0.74, { fill: index % 2 ? BRAND.coral : BRAND.blue });
    addText(slide, item, x - 0.6, y - 0.25, 1.2, 0.5, { fontSize: 9, bold: true, color: BRAND.white, align: "center" });
  });
}

function renderCanvas(slide, page) {
  const groups = splitItems(page.body, 6);
  const widths = [3.55, 4.3, 3.55];
  let itemIndex = 0;
  for (let row = 0; row < 2; row += 1) {
    let x = 0.75;
    widths.forEach((w, col) => {
      const y = 1.7 + row * 2.3;
      addShape(slide, "rect", x, y, w, 2.05, { fill: [BRAND.paleBlue, BRAND.paleTeal, BRAND.paleGold, BRAND.paleCoral][(row * 3 + col) % 4] });
      addBodyBlock(slide, groups[itemIndex], x + 0.24, y + 0.25, w - 0.48, 1.5, { fontSize: 13 });
      itemIndex += 1;
      x += w + 0.22;
    });
  }
}

function renderTable(slide, page) {
  const items = page.body.length ? page.body : [page.title];
  const rowH = 4.75 / items.length;
  items.forEach((item, index) => {
    const y = 1.7 + index * rowH;
    addShape(slide, "rect", 0.88, y, 1.6, rowH - 0.05, { fill: index % 2 ? BRAND.ink : BRAND.coral });
    addShape(slide, "rect", 2.52, y, 9.82, rowH - 0.05, { fill: index % 2 ? "F2EFE9" : BRAND.white, lineColor: "D9D5CD", lineTransparency: 0 });
    addText(slide, String(index + 1).padStart(2, "0"), 1.2, y + 0.08, 0.95, rowH - 0.22, { fontSize: 13, bold: true, color: BRAND.white, align: "center" });
    addText(slide, item, 2.85, y + 0.08, 9.15, rowH - 0.22, { fontSize: 14, bold: index === 0 });
  });
}

function renderGallery(slide, page) {
  const groups = splitItems(page.body, 5);
  const frames = [[0.82, 1.72, 4.2, 2.05], [5.25, 1.72, 3.25, 2.05], [8.73, 1.72, 3.78, 2.05], [0.82, 4.02, 5.65, 2.12], [6.7, 4.02, 5.81, 2.12]];
  frames.forEach(([x, y, w, h], index) => {
    addShape(slide, "rect", x, y, w, h, { fill: [BRAND.paleBlue, BRAND.paleTeal, BRAND.paleGold, BRAND.paleCoral, "EEE9FA"][index] });
    addText(slide, String(index + 1).padStart(2, "0"), x + 0.22, y + 0.2, 0.55, 0.32, { fontSize: 10, bold: true, color: BRAND.muted });
    addBodyBlock(slide, groups[index], x + 0.65, y + 0.45, w - 1.1, h - 0.72, { fontSize: 13, align: "center", valign: "mid" });
  });
}

function renderManifesto(slide, page, closing = false) {
  slide.background = { color: closing ? BRAND.coral : BRAND.ink };
  if (closing) {
    addText(slide, "ACTION", 0.78, 0.62, 3.2, 0.34, { fontSize: 11, bold: true, color: BRAND.accentOn, charSpacing: 1.8 });
    addText(slide, page.title, 0.78, 1.22, 11.7, 1.62, { fontSize: 44, bold: false, color: BRAND.accentOn });
    addLine(slide, "line", 0.8, 3.2, 11.7, 0, BRAND.accentOn, 1);
    const actions = page.body.slice(0, 3);
    actions.forEach((item, index) => {
      const x = 0.8 + index * 4.0;
      addText(slide, `0${index + 1}`, x, 3.62, 0.72, 0.48, { fontSize: 18, bold: true, color: BRAND.accentOn });
      addText(slide, item, x, 4.42, 3.5, 1.42, { fontSize: 21, bold: true, color: BRAND.accentOn, valign: "top" });
    });
    return;
  }
  addText(slide, page.title, 1.0, 1.5, 11.3, 1.2, { fontSize: 34, bold: true, color: BRAND.white, align: "center" });
  addLine(slide, "line", 4.8, 3.05, 3.7, 0, BRAND.white, 3);
  addBodyBlock(slide, page.body, 2.1, 3.48, 9.1, 2.0, { fontSize: 19, color: BRAND.white, align: "center", numbered: true });
}

function renderSlide(pptx, slide, page, spec, index) {
  slide.background = { color: BRAND.paper };
  if (page.layoutType !== "cover" && page.layoutType !== "manifesto" && page.layoutType !== "closing") {
    addSlideTitle(slide, page.title, index);
  }

  switch (page.layoutType) {
    case "cover": renderCover(slide, page, spec); break;
    case "statement": renderStatement(slide, page); break;
    case "split-left": renderSplit(slide, page, "left"); break;
    case "split-right": renderSplit(slide, page, "right"); break;
    case "three-column": renderColumns(slide, page, 3); break;
    case "four-quadrant": renderQuadrant(slide, page); break;
    case "timeline-horizontal": renderTimeline(slide, page, false); break;
    case "timeline-vertical": renderTimeline(slide, page, true); break;
    case "process-chevron": renderProcess(slide, page); break;
    case "staircase": renderStaircase(slide, page); break;
    case "concentric": renderConcentric(slide, page, false); break;
    case "orbit": renderConcentric(slide, page, true); break;
    case "comparison": renderComparison(slide, page, false); break;
    case "before-after": renderComparison(slide, page, true); break;
    case "pyramid": renderPyramid(slide, page, false); break;
    case "funnel": renderPyramid(slide, page, true); break;
    case "roadmap": renderRoadmap(slide, page, false); break;
    case "bridge": renderRoadmap(slide, page, true); break;
    case "spotlight": renderSpotlight(slide, page, false); break;
    case "quote": renderSpotlight(slide, page, true); break;
    case "metric": renderMetric(slide, page); break;
    case "checklist": renderChecklist(slide, page); break;
    case "do-dont": renderDual(slide, page, ["建议执行", "需要避免"], BRAND.teal, BRAND.coral); break;
    case "cause-effect": renderCauseEffect(slide, page); break;
    case "problem-solution": renderProblemSolution(slide, page); break;
    case "question-answer": renderQuestionAnswer(slide, page); break;
    case "ladder": renderLadder(slide, page); break;
    case "wave": renderWave(slide, page, false); break;
    case "zigzag": renderWave(slide, page, true); break;
    case "stack": renderStack(slide, page); break;
    case "spectrum": renderSpectrum(slide, page); break;
    case "iceberg": renderIceberg(slide, page); break;
    case "tree": renderTree(slide, page); break;
    case "hub-spoke": renderHubSpoke(slide, page, false); break;
    case "flywheel": renderHubSpoke(slide, page, true); break;
    case "canvas": renderCanvas(slide, page); break;
    case "table": renderTable(slide, page); break;
    case "gallery": renderGallery(slide, page); break;
    case "manifesto": renderManifesto(slide, page, false); break;
    case "closing": renderManifesto(slide, page, true); break;
    default: throw new TypeError(`无法渲染版式：${page.layoutType}`);
  }

  addFooter(slide, index + 1);
  if (page.notes && typeof slide.addNotes === "function") slide.addNotes(page.notes.split("\n"));
  void pptx;
}

/**
 * 生成 PowerPoint。返回值是 Buffer，可直接交给 res.send；filename/contentType 可用于下载响应头。
 */
export async function buildCoursewarePptx(spec) {
  const normalized = validateCoursewareSpec(spec);
  applyBrandTheme(normalized);
  const PptxGenJS = await loadPptxGenerator();

  const pptx = new PptxGenJS();
  pptx.layout = "LAYOUT_WIDE";
  pptx.author = normalized.author || "创创";
  pptx.company = "创创";
  pptx.subject = normalized.title;
  pptx.title = normalized.title;
  pptx.lang = "zh-CN";
  pptx.theme = {
    headFontFace: FONT_FACE,
    bodyFontFace: FONT_FACE,
    lang: "zh-CN",
  };
  pptx.defineSlideMaster({
    title: "CHUANG_BASE",
    background: { color: BRAND.paper },
    objects: [],
    slideNumber: { x: 11.95, y: 7.0, w: 0.68, h: 0.24, color: "8A8F98", fontFace: FONT_FACE, fontSize: 9, align: "right", transparency: 100 },
  });

  normalized.slides.forEach((page, index) => {
    const slide = pptx.addSlide();
    renderSlide(pptx, slide, page, normalized, index);
  });

  const output = await pptx.write({ outputType: "nodebuffer", compression: true });
  return asDownloadBuffer(output, `${normalized.filename}.pptx`, PPTX_MIME);
}

/**
 * 生成与课件逐页对应的讲师稿 Markdown。返回值是 UTF-8 Buffer。
 */
export function buildSpeakerNotes(spec) {
  const normalized = validateCoursewareSpec(spec);
  const lines = [
    `# ${normalized.title}`,
    "",
  ];
  if (normalized.subtitle) lines.push(`> ${normalized.subtitle}`, "");
  if (normalized.author) lines.push(`讲师：${normalized.author}`, "");
  lines.push("---", "");

  normalized.slides.forEach((slide, index) => {
    lines.push(`## ${index + 1}. ${slide.title}`, "");
    lines.push(`版式：${slide.layoutType}`, "");
    if (slide.body.length) {
      lines.push("### 页面内容", "", ...slide.body.map((item) => `- ${item}`), "");
    }
    lines.push("### 讲师稿", "", slide.notes || "（本页暂无讲师稿）", "", "---", "");
  });

  const buffer = Buffer.from(`${lines.join("\n").trimEnd()}\n`, "utf8");
  return asDownloadBuffer(buffer, `${normalized.filename}-讲师稿.md`, MARKDOWN_MIME);
}

const GUIZANG_LAYOUTS = Object.freeze(["S03", "S04", "S05", "S11", "S13", "S14", "S17", "S19", "S21", "S12"]);
const GUIZANG_RECIPES = Object.freeze({
  S01: "hero",
  S03: "statement",
  S04: "grid-reveal",
  S05: "stack-build",
  S10: "split-statement",
  S11: "timeline-walk",
  S12: "manifesto",
  S13: "three-forces",
  S14: "loop-form",
  S17: "system-diagram",
  S19: "four-cards",
  S21: "tech-spec",
});

function escapeHtml(value) {
  return String(value ?? "")
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#39;");
}

function htmlText(value) {
  return escapeHtml(value).replace(/\n/g, "<br>");
}

function pageItems(page) {
  return page.body.length ? page.body : [page.title];
}

function guizangChrome(title, index, total, right = "CHUANGCHUANG · COURSEWARE") {
  return `<div class="chrome-min"><div class="l">${htmlText(title)}</div><div class="r">${escapeHtml(String(index + 1).padStart(2, "0"))} / ${escapeHtml(String(total).padStart(2, "0"))} · ${escapeHtml(right)}</div></div>`;
}

function renderGuizangSlide(page, index, total, spec, theme, imageData) {
  const items = pageItems(page);
  const layout = index === 0 ? "S01" : index === total - 1 && total > 1 ? "S10" : GUIZANG_LAYOUTS[(index - 1) % GUIZANG_LAYOUTS.length];
  const recipe = GUIZANG_RECIPES[layout];
  const title = htmlText(page.title);
  const kicker = htmlText(spec.subtitle || "创创 · 内部课程课件");
  const body = items.map((item) => htmlText(item));

  if (layout === "S01") {
    return `<section class="slide accent" data-layout="S01" data-animate="${recipe}">
  <div class="canvas-card">
    <canvas class="ascii-bg" aria-hidden="true"></canvas>
    ${guizangChrome(spec.title, index, total, `SWISS · ${theme.name.toUpperCase()}`)}
    <img src="${imageData}" alt="课程图示" style="position:absolute;right:7vw;top:24vh;width:min(18vw,240px);height:min(18vw,240px);object-fit:contain;filter:grayscale(1) contrast(1.05) brightness(1.6);opacity:.9" />
    <div style="flex:1;padding:0;display:grid;grid-template-rows:auto 1fr auto;gap:2.6vh">
      <div data-anim="kicker" class="t-meta" style="color:rgba(255,255,255,.78);letter-spacing:.22em">COURSEWARE / ${kicker}</div>
      <h1 data-anim="title" style="align-self:flex-start;margin-top:10vh;font-family:var(--sans),var(--sans-zh);font-weight:200;font-size:min(11.6vw,19vh);line-height:.94;letter-spacing:-.025em;color:#fff">${title}</h1>
      <div data-anim="bottom" style="display:grid;grid-template-rows:auto auto;gap:1.6vh;border-top:1px solid rgba(255,255,255,.22);padding-top:2vh">
        <div data-anim="lead" class="lead" style="max-width:52ch;color:rgba(255,255,255,.86)">${body[0] || "从问题出发，形成可执行的课程行动。"}</div>
        <div style="display:flex;justify-content:space-between;align-items:end"><div class="t-meta" style="color:rgba(255,255,255,.6)">${htmlText(spec.author || "创创")}</div><div class="t-meta" style="color:rgba(255,255,255,.6)">→ swipe / arrow keys</div></div>
      </div>
    </div>
  </div>
</section>`;
  }

  if (layout === "S10") {
    const takeaways = items.slice(0, 3);
    while (takeaways.length < 3) takeaways.push("把这一页转成下一步行动");
    return `<section class="slide split" data-layout="S10" data-animate="${recipe}">
  <div class="canvas-card"><div class="split-half">
    <div class="half b-accent" style="padding:5.6vh 3.6vw 4.4vh;justify-content:space-between;position:relative;overflow:hidden"><canvas class="ascii-bg" aria-hidden="true"></canvas><img src="${imageData}" alt="图文结合示意图" style="position:absolute;right:2vw;bottom:11vh;width:8vw;max-width:120px;opacity:.78;filter:grayscale(1) contrast(1.05) brightness(1.65);z-index:1" />
      <div class="chrome-min" style="position:relative;z-index:1"><div class="l">${escapeHtml(String(total).padStart(2, "0"))} / ${escapeHtml(String(total).padStart(2, "0"))}</div><div class="r">CLOSING</div></div>
      <div data-anim="manifesto" style="display:flex;flex-direction:column;gap:2vh;position:relative;z-index:1"><div class="t-meta" style="color:rgba(255,255,255,.78);letter-spacing:.22em">TAKEAWAYS</div><h2 style="font-family:var(--sans),var(--sans-zh);font-size:min(8vw,14vh);line-height:.94;letter-spacing:-.025em;font-weight:200;color:#fff">${title}</h2><div style="font-family:var(--sans),var(--sans-zh);font-size:max(14px,1vw);line-height:1.6;color:rgba(255,255,255,.82);max-width:36ch">${body[0] || "把课程要点带回真实工作现场。"}</div></div>
      <div data-anim="signature" class="t-meta" style="color:rgba(255,255,255,.62);border-top:1px solid rgba(255,255,255,.22);padding-top:2vh">${htmlText(spec.author || "创创")}</div>
    </div>
    <div class="half" style="padding:5.6vh 3.6vw 4.4vh;justify-content:space-between"><div class="chrome-min"><div class="l">TAKEAWAYS</div><div class="r">03 RULES</div></div><div data-anim="rules" style="display:flex;flex-direction:column;gap:0">${takeaways.map((item, i) => `<div style="display:grid;grid-template-columns:auto 1fr;gap:2vw;align-items:start;padding:2.6vh 0;border-top:1px solid var(--border-subtle);${i === 2 ? "border-bottom:2px solid var(--accent)" : ""}"><div style="font-family:var(--sans);font-weight:200;font-size:min(4.4vw,7.8vh);line-height:.9;color:${i === 2 ? "var(--accent)" : "var(--text-primary)"}">${String(i + 1).padStart(2, "0")}</div><div><h3 style="font-family:var(--sans),var(--sans-zh);font-weight:400;font-size:max(18px,1.8vw);line-height:1.2;color:${i === 2 ? "var(--accent)" : "var(--text-primary)"};margin-bottom:1vh">${htmlText(item)}</h3><p style="font-family:var(--sans),var(--sans-zh);font-size:max(16px,.94vw);line-height:1.6;color:var(--text-secondary)">${htmlText(page.notes || "可直接带入下一次工作实践。")}</p></div></div>`).join("")}</div><div data-anim="foot" class="t-meta" style="color:var(--text-helper);text-align:right">→ 完 · END OF COURSEWARE</div></div>
  </div></div>
</section>`;
  }

  const titleBlock = `<div data-anim="line" class="t-meta" style="letter-spacing:.18em;color:var(--text-helper)">${escapeHtml(layout)} · COURSEWARE</div><h2 style="font-family:var(--sans),var(--sans-zh);font-weight:400;font-size:min(4.4vw,8vh);line-height:1.05;letter-spacing:-.025em;color:var(--text-primary);margin:.9vh 0 0">${title}</h2>`;
  const footer = `<div class="t-meta" style="color:var(--text-helper);margin-top:auto;padding-top:2vh">${escapeHtml(spec.author || "创创")} · ${escapeHtml(String(index + 1).padStart(2, "0"))} / ${escapeHtml(String(total).padStart(2, "0"))}</div>`;

  if (layout === "S03") {
    return `<section class="slide" data-layout="S03" data-animate="${recipe}"><div class="canvas-card"><div style="display:flex;flex-direction:column;height:100%">${titleBlock}<div data-anim="statement" style="margin-top:8vh;max-width:78ch"><div style="font-family:var(--sans),var(--sans-zh);font-size:min(5.5vw,10vh);line-height:1.04;letter-spacing:-.03em;font-weight:300;color:var(--text-primary)">“${body[0] || title}”</div><p style="font-size:max(16px,1.1vw);line-height:1.65;color:var(--text-secondary);margin-top:3vh">${body.slice(1, 3).join("<br>")}</p></div>${footer}</div></div></section>`;
  }

  if (layout === "S04") {
    const cards = items.slice(0, 6);
    while (cards.length < 6) cards.push("补充一个可执行动作");
    return `<section class="slide" data-layout="S04" data-animate="${recipe}"><div class="canvas-card"><div style="display:flex;flex-direction:column;height:100%">${titleBlock}<div class="sub-grid-3-2" style="margin-top:4.2vh;flex:1">${cards.map((item, i) => `<article class="sub-card card-fill" data-anim="card"><i data-lucide="${["layers", "scan-text", "workflow", "check", "book-open", "sparkles"][i]}" style="width:2.2vw;height:2.2vw;color:var(--accent)"></i><span class="nb-corner">${String(i + 1).padStart(2, "0")}</span><div class="ttl">${htmlText(item)}</div><p class="desc">${htmlText(page.notes || "围绕这一点补充具体例子与动作。")}</p></article>`).join("")}</div></div></div></section>`;
  }

  if (layout === "S05") {
    const layers = items.slice(0, 3);
    while (layers.length < 3) layers.push("补充一层关键能力");
    return `<section class="slide" data-layout="S05" data-animate="${recipe}"><div class="canvas-card"><div style="display:flex;flex-direction:column;height:100%">${titleBlock}<div class="stack-row" style="margin-top:8vh;flex:1;display:flex;flex-direction:column;justify-content:center;gap:1px">${layers.map((item, i) => `<div class="stack-block ${["b-accent", "b-grey", "b-ink"][i]}" data-anim="layer"><span class="layer-nb">0${i + 1}</span><i data-lucide="${["brain", "workflow", "rocket"][i]}"></i><span class="layer-ttl">${htmlText(item)}</span><span class="layer-desc">${htmlText(page.notes || "把这一层落实到流程、角色和交付物。")}</span><span class="layer-tag">${["FOUNDATION", "SYSTEM", "ACTION"][i]}</span></div>`).join("")}</div>${footer}</div></div></section>`;
  }

  if (layout === "S11") {
    const timeline = items.slice(0, 5);
    while (timeline.length < 5) timeline.push("下一步行动");
    return `<section class="slide" data-layout="S11" data-animate="${recipe}"><div class="canvas-card"><div style="display:flex;flex-direction:column;height:100%">${titleBlock}<div class="timeline-h" style="margin-top:8vh;flex:1;display:flex;align-items:center"><div class="tl-row" style="width:100%">${timeline.map((item, i) => `<div class="th-node ${i % 2 ? "down" : "up"}" data-anim="node"><div class="dot"></div><div class="label"><div class="yr">${String(i + 1).padStart(2, "0")}</div><div class="name">${htmlText(item)}</div><div class="desc">${htmlText(page.notes || "明确负责人、动作与时间。")}</div></div></div>`).join("")}</div></div>${footer}</div></div></section>`;
  }

  if (layout === "S13") {
    const forces = items.slice(0, 3);
    while (forces.length < 3) forces.push("一个关键推动力");
    return `<section class="slide" data-layout="S13" data-animate="${recipe}"><div class="canvas-card"><div style="display:grid;grid-template-columns:minmax(0,1.1fr) minmax(0,1fr);gap:5vw;height:100%"><div style="display:flex;flex-direction:column;justify-content:space-between"><div>${titleBlock}</div><div data-anim="hero" class="card-ink" style="padding:3vh 2.6vw;font-size:min(4vw,7vh);line-height:1.05">${htmlText(body[0] || title)}</div>${footer}</div><div style="display:flex;flex-direction:column;justify-content:center;gap:1.2vh">${forces.map((item, i) => `<article class="card-fill" data-anim="force" style="padding:2.4vh 2vw;border-top:2px solid ${i === 0 ? "var(--accent)" : "var(--border-strong)"}"><div class="t-meta">0${i + 1} / FORCE</div><h3 style="font-size:max(20px,1.6vw);font-weight:400;margin:1vh 0">${htmlText(item)}</h3><p class="t-body-sm">${htmlText(page.notes || "将这股力量转化为可复用的动作。")}</p></article>`).join("")}</div></div></div></section>`;
  }

  if (layout === "S14") {
    const steps = items.slice(0, 4);
    while (steps.length < 4) steps.push("持续复盘与更新");
    return `<section class="slide" data-layout="S14" data-animate="${recipe}"><div class="canvas-card"><div style="display:flex;flex-direction:column;height:100%">${titleBlock}<div style="display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:5vw;align-items:center;flex:1;margin-top:4vh"><div class="loop-steps">${steps.map((item, i) => `<div data-anim="step" style="display:grid;grid-template-columns:3rem 1fr;gap:1vw;align-items:start;padding:1.8vh 0;border-top:1px solid var(--border-subtle)"><span class="t-meta" style="color:var(--accent)">0${i + 1}</span><div><strong style="font-size:max(18px,1.35vw);font-weight:400">${htmlText(item)}</strong><p class="t-body-sm" style="margin-top:.6vh">${htmlText(page.notes || "把结果回流到下一次行动。")}</p></div></div>`).join("")}</div><div class="loop-diagram" style="position:relative;aspect-ratio:1;border:1px solid var(--border-strong);border-radius:50%;display:grid;place-items:center;background:radial-gradient(circle,var(--accent) 0 18%,transparent 18.5%),radial-gradient(circle,transparent 0 38%,var(--border-subtle) 38.5% 39%,transparent 39.5%),radial-gradient(circle,transparent 0 66%,var(--border-subtle) 66.5% 67%,transparent 67.5%)"><span style="color:#fff;font-size:1.2vw;letter-spacing:.18em;position:relative;z-index:1">LOOP</span></div></div>${footer}</div></div></section>`;
  }

  if (layout === "S17") {
    return `<section class="slide" data-layout="S17" data-animate="${recipe}"><div class="canvas-card"><div style="display:grid;grid-template-columns:minmax(0,.85fr) minmax(0,1.15fr);gap:6vw;height:100%;align-items:center"><div style="display:flex;flex-direction:column;height:100%">${titleBlock}<p data-anim="copy" style="font-size:max(17px,1.25vw);line-height:1.7;color:var(--text-secondary);margin-top:5vh">${body.slice(0, 3).join("<br>")}</p>${footer}</div><div data-anim="diagram" style="position:relative;aspect-ratio:1;max-width:32vw;margin:auto;border-radius:50%;border:1px solid var(--border-strong);display:grid;place-items:center;background:radial-gradient(circle,var(--accent) 0 16%,transparent 16.5%),radial-gradient(circle,transparent 0 35%,var(--border-subtle) 35.5% 36%,transparent 36.5%),radial-gradient(circle,transparent 0 60%,var(--border-subtle) 60.5% 61%,transparent 61.5%)"><div style="position:absolute;inset:8%;border:1px dashed var(--border-strong);border-radius:50%"></div><span style="font-size:max(18px,1.3vw);color:#fff;position:relative;z-index:1">${title}</span></div></div></div></section>`;
  }

  if (layout === "S21") {
    const specs = items.slice(0, 3);
    while (specs.length < 3) specs.push("可验证指标");
    return `<section class="slide" data-layout="S21" data-animate="${recipe}"><div class="canvas-card"><div style="display:flex;flex-direction:column;height:100%">${titleBlock}<div data-anim="kpi" style="display:grid;grid-template-columns:repeat(3,1fr);gap:2vw;margin-top:8vh">${specs.map((item, i) => `<div class="kpi-cell" data-anim="kpi-cell" style="border-top:2px solid var(--accent);padding-top:1.6vh"><div class="t-meta">0${i + 1} / SPEC</div><div class="kpi-hero" style="font-size:min(3.8vw,7vh);line-height:1;font-weight:300;margin:1.5vh 0">${htmlText(item)}</div><p class="t-body-sm">${htmlText(page.notes || "定义口径，随后验证结果。")}</p></div>`).join("")}</div><div style="display:grid;grid-template-columns:repeat(6,1fr);gap:1vw;align-items:end;flex:1;margin-top:5vh">${[35, 58, 44, 72, 64, 86].map((height, i) => `<div style="height:${height}%;background:${i === 5 ? "var(--accent)" : "var(--grey-1)"};border-top:2px solid ${i === 5 ? "var(--accent)" : "var(--border-strong)"}" data-anim="bar"></div>`).join("")}</div>${footer}</div></div></section>`;
  }

  if (layout === "S12") {
    return `<section class="slide dark" data-layout="S12" data-animate="${recipe}"><div class="canvas-card" style="background:#0a0a0a;color:#fff"><div style="display:flex;flex-direction:column;height:100%">${guizangChrome(spec.title, index, total, "MANIFESTO")}<div data-anim="manifesto" style="display:flex;flex-direction:column;justify-content:center;flex:1;max-width:72ch"><div class="t-meta" style="color:rgba(255,255,255,.68);letter-spacing:.2em">MANIFESTO</div><h2 style="font-size:min(7vw,13vh);line-height:.96;font-weight:200;margin:2vh 0;color:#fff">${title}</h2><p style="font-size:max(17px,1.2vw);line-height:1.7;color:rgba(255,255,255,.76)">${body.slice(0, 3).join("<br>")}</p></div><div class="ink-banner-full" style="color:#fff;background:var(--accent);padding:1.8vh 2vw">${htmlText(page.notes || "把共识变成每天都能执行的动作。")}</div></div></div></section>`;
  }

  return `<section class="slide" data-layout="S19" data-animate="four-cards"><div class="canvas-card"><div style="display:flex;flex-direction:column;height:100%">${titleBlock}<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:1.4vw;flex:1;margin-top:7vh">${items.slice(0, 4).map((item, i) => `<article class="card-fill" data-anim="card" style="padding:2vh 1.4vw;border-top:2px solid ${i === 0 ? "var(--accent)" : "var(--border-strong)"}"><div class="t-meta">0${i + 1}</div><h3 style="font-size:max(18px,1.2vw);font-weight:400;margin:1.2vh 0">${htmlText(item)}</h3><p class="t-body-sm">${htmlText(page.notes || "补充说明与示例。")}</p></article>`).join("")}</div>${footer}</div></div></section>`;
}

/**
 * 生成符合 guizang-ppt-skill 瑞士锁定规范的单文件网页 PPT。
 * 该产物适合横向翻页预览与分享；传统 .pptx 仍由 buildCoursewarePptx 提供。
 */
export async function buildCoursewareGuizangHtml(spec) {
  const normalized = validateCoursewareSpec(spec);
  const theme = applyBrandTheme(normalized);
  const template = await readFile(GUIZANG_SWISS_TEMPLATE, "utf8");
  const marker = template.indexOf("<!-- SLIDES_HERE");
  const tailMarker = template.indexOf("\n</div>\n\n<div id=\"nav\"></div>\n\n<script>\n/* =============== WebGL", marker);
  if (marker < 0 || tailMarker < 0) {
    const error = new Error("网页 PPT 模板结构不完整");
    error.code = "GUIZANG_TEMPLATE_INVALID";
    throw error;
  }
  const imageData = `data:image/png;base64,${(await readFile(COURSEWARE_IMAGE_PATH)).toString("base64")}`;
  const slides = normalized.slides.map((page, index) => renderGuizangSlide(page, index, normalized.slides.length, normalized, theme, imageData)).join("\n\n");
  const titleTag = `<title>${escapeHtml(normalized.title)} · 创创网页 PPT</title>`;
  const titled = template.replace(/<title>[\s\S]*?<\/title>/u, titleTag);
  const html = `${titled.slice(0, marker)}<!-- SLIDES_HERE · generated by the Swiss layout lock -->\n${slides}\n${titled.slice(tailMarker)}`
    .replace(/#FF6B35/giu, `#${theme.accent}`)
    .replace(/255,107,53/g, theme.accentRgb)
    .replace(/#FF9A73/giu, `#${theme.accentBright}`)
    .replace(/\[必填\]/g, "");
  return asDownloadBuffer(Buffer.from(html, "utf8"), `${normalized.filename}-网页PPT.html`, HTML_MIME);
}
