import { execFileSync } from "node:child_process";
import {
  copyFileSync,
  existsSync,
  mkdirSync,
  readFileSync,
  rmSync,
  writeFileSync
} from "node:fs";
import { join, resolve, sep } from "node:path";
import { fileURLToPath } from "node:url";

const root = resolve(fileURLToPath(new URL("../../..", import.meta.url)));
const webRoot = join(root, "kdl-wasm", "web");
const docRoot = join(root, "work", "doc");
const stagingRoot = join(webRoot, "test-results", "grl-manual-docx-build");
const outputPath = join(docRoot, "GRL功能语法逻辑与程序创建使用手册.docx");

const flowDir = join(docRoot, "通用机器人项目功能与数据流程图-png");
const flowImages = [
  { source: join(flowDir, "flow-01.png"), title: "系统功能总览图", cx: 5943600, cy: 3343275 },
  { source: join(flowDir, "flow-02.png"), title: "GRL 到 IR 编译流程图", cx: 5943600, cy: 3343275 },
  { source: join(flowDir, "flow-03.png"), title: "运动规划与 KDL 数据流图", cx: 5943600, cy: 3343275 },
  { source: join(flowDir, "flow-04.png"), title: "虚拟控制器执行流程图", cx: 5943600, cy: 3343275 },
  { source: join(flowDir, "flow-05.png"), title: "后处理与导入回读流程图", cx: 5943600, cy: 3343275 }
];
const uiImages = [
  {
    source: join(webRoot, "test-results", "virtual-controller", "virtual-controller-desktop.png"),
    title: "虚拟控制器桌面端界面",
    cx: 5943600,
    cy: 3962400
  },
  {
    source: join(webRoot, "test-results", "virtual-controller", "virtual-controller-mobile.png"),
    title: "虚拟控制器移动端界面",
    cx: 2743200,
    cy: 5943600
  }
];

const smokeSourcePath = join(webRoot, "tests", "fixtures", "abb120", "programs", "A120_00_Smoke.grl");
const blendSourcePath = join(webRoot, "tests", "fixtures", "abb120", "programs", "A120_20_CartesianBlend.grl");
const ioSourcePath = join(webRoot, "tests", "fixtures", "abb120", "spec-programs", "runtime", "W2_60_IOWaitPulse.grl");
const fullExamplePath = join(webRoot, "tests", "fixtures", "abb120", "spec-programs", "runtime", "W2_99_FullSpecExample.grl");

const generatedAt = formatShanghaiTime(new Date());

buildDocx();
console.log(JSON.stringify({ outputPath }, null, 2));

function buildDocx() {
  assertSafeStaging(stagingRoot);
  rmSync(stagingRoot, { recursive: true, force: true });
  mkdirSync(join(stagingRoot, "_rels"), { recursive: true });
  mkdirSync(join(stagingRoot, "docProps"), { recursive: true });
  mkdirSync(join(stagingRoot, "word", "_rels"), { recursive: true });
  mkdirSync(join(stagingRoot, "word", "media"), { recursive: true });

  const images = copyImages([...flowImages, ...uiImages]);
  writeFileSync(join(stagingRoot, "[Content_Types].xml"), contentTypesXml(), "utf8");
  writeFileSync(join(stagingRoot, "_rels", ".rels"), packageRelsXml(), "utf8");
  writeFileSync(join(stagingRoot, "docProps", "core.xml"), corePropsXml(), "utf8");
  writeFileSync(join(stagingRoot, "docProps", "app.xml"), appPropsXml(), "utf8");
  writeFileSync(join(stagingRoot, "word", "styles.xml"), stylesXml(), "utf8");
  writeFileSync(join(stagingRoot, "word", "_rels", "document.xml.rels"), documentRelsXml(images), "utf8");
  writeFileSync(join(stagingRoot, "word", "document.xml"), documentXml(images), "utf8");

  if (existsSync(outputPath)) {
    rmSync(outputPath, { force: true });
  }
  zipDirectory(stagingRoot, outputPath);
}

function documentXml(images) {
  const body = [];

  body.push(heading("GRL 功能、语法逻辑与程序创建使用手册", 1));
  body.push(p(`生成时间：${generatedAt}`));
  body.push(p("适用对象：通用机器人离线编程系统用户、工艺工程师、自动编程开发人员、虚拟调试人员。"));
  body.push(p("手册目标：用中文说明 GRL 的功能边界、语法逻辑、程序对象关系、创建程序步骤、仿真检查和后处理输出方式，并通过图示和源码示例帮助用户快速编写可运行程序。"));

  body.push(heading("一、GRL 是什么", 1));
  body.push(p("GRL，全称 Generic Robot Language，是本离线编程系统中的通用机器人程序语言。它不是 ABB、FANUC 或 KUKA 任一品牌语言的拷贝，而是把多品牌机器人共同具备的目标点、运动、路径、工艺、IO、流程控制、异常和后处理语义抽象为统一文本。"));
  body.push(table([
    ["目标", "说明"],
    ["统一描述", "用同一套 GRL 描述机器人程序、路径、工艺动作和 IO 逻辑。"],
    ["可编译", "GRL 经 lexer/parser/semantic analyzer 编译为统一 Executable IR。"],
    ["可仿真", "IR 可进入虚拟控制器运行，产生 trace、motion queue、IO image 和诊断。"],
    ["可后处理", "同一 IR 可输出 ABB RAPID、FANUC LS/TP 风格文本、KUKA KRL 原型程序。"],
    ["可维护", "文本稳定、可读、可 diff，适合版本管理和自动生成。"]
  ]));

  body.push(heading("二、系统功能总览", 1));
  body.push(p("离线编程系统从项目数据、机器人模型和 GRL 程序出发，经过语义编译、KDL 运动规划、虚拟控制器执行、诊断报告和多品牌后处理，形成完整离线开发闭环。"));
  body.push(imageWithCaption(images[0], 1, "图 1 系统功能总览图"));
  body.push(table([
    ["模块", "主要职责"],
    ["GRL 源码", "声明工具、坐标系、目标点、速度、zone、path、operation 和 proc。"],
    ["编译器", "把源码解析为 AST，做符号解析、单位归一化、语义检查，生成 IR 和 KDL request。"],
    ["KDL 运动规划", "处理 FK、IK、MoveJ、MoveL、MoveC、Path planning、可达性和轨迹采样。"],
    ["虚拟控制器", "执行 IR，维护程序计数器、调用栈、报警队列、IO 镜像、运动队列和 trace。"],
    ["后处理器", "按 ABB/FANUC/KUKA 规则输出品牌程序，并记录无法保真映射的转换报告。"]
  ]));

  body.push(heading("三、GRL 程序基本结构", 1));
  body.push(p("一个 GRL 文件通常由 language 声明、module 声明、数据声明、目标点、路径、工艺操作和 main 子程序组成。最小程序只需要定义模块入口和一条运动。"));
  body.push(codeBlock(readFileSync(smokeSourcePath, "utf8")));
  body.push(table([
    ["结构", "作用"],
    ["language grl 0.1", "声明语言版本，便于后续兼容升级。"],
    ["module", "程序模块，类似 ABB MODULE、KUKA DEF 文件或 FANUC 程序容器。"],
    ["const/persistent/var", "声明常量、持久数据或局部变量。"],
    ["target", "声明 joint_target 或 pose_target。"],
    ["proc main()", "默认入口程序。虚拟控制器从 main 开始执行。"],
    ["movej/movel/movec", "基础运动指令，最终进入 KDL 规划或品牌后处理。"]
  ]));

  body.push(heading("四、对象模型和语法逻辑", 1));
  body.push(p("GRL 的语法逻辑建议按“数据先声明、路径再组织、工艺再封装、入口只调用”的方式理解。手写小程序可以直接在 main 中写运动，自动编程和复杂工艺应优先生成 path 和 operation。"));
  body.push(imageWithCaption(images[1], 2, "图 2 GRL 到 IR 编译流程图"));
  body.push(table([
    ["对象", "语法", "使用建议"],
    ["工具", "persistent tool gripper = tool { ... }", "描述 TCP、质量、重心。运动时通过 tool 参数或 path defaults 引用。"],
    ["坐标系", "persistent frame fixture = frame { ... }", "描述工件坐标或基坐标。目标点和路径应显式绑定 frame。"],
    ["速度", "const speed v = linear(300 mm/s) / joint(50 %)", "按运动类型选择 joint、linear、angular。"],
    ["过渡", "const zone z10 = z(10 mm) / fine / cnt(30)", "fine 精确到点；z/cnt/continuous 用于连续过渡。"],
    ["目标点", "target pick = pose_target { pose: pose(...) }", "关节目标用 joint_target；笛卡尔点用 pose_target。"],
    ["路径", "path pick_path { defaults ... point ... event ... }", "自动编程优先生成 path，点名保持稳定。"],
    ["工艺", "operation pick_op { kind: handling path: pick_path ... }", "封装路径、工艺参数、start_action/end_action。"],
    ["入口程序", "proc main() run_operation pick_op end", "入口程序保持短，便于调试和后处理。"]
  ]));

  body.push(heading("五、表达式、单位和常量求值", 1));
  body.push(p("GRL 支持带单位表达式。编译器会在需要常量的位置做单位归一化，例如 mm 转 m、deg 转 rad、ms 转 s、% 转比例。这样同一程序在仿真和后处理中使用统一数值。"));
  body.push(table([
    ["类别", "示例", "说明"],
    ["长度", "100 mm、0.25 m", "内部统一为 meter。"],
    ["角度", "90 deg、1.57 rad", "内部统一为 radian。"],
    ["时间", "200 ms、2 s", "内部统一为 second。"],
    ["速度", "linear(300 mm/s)、joint(50 %)", "分别表示 TCP 线速度和关节速度比例。"],
    ["数学函数", "atan2(0, 1)、max(50 mm/s, 150 mm/s)", "支持常用三角函数和数值函数。"],
    ["单位检查", "10 mm + 2 s", "不同维度相加会产生诊断。"]
  ]));

  body.push(heading("六、运动语法", 1));
  body.push(p("运动指令分为 MoveJ、MoveL、MoveC 三类。MoveJ 走关节空间，MoveL 走 TCP 直线，MoveC 走 TCP 圆弧。每条运动最终都需要确定 target、speed、zone、tool 和 frame。"));
  body.push(imageWithCaption(images[2], 3, "图 3 运动规划与 KDL 数据流图"));
  body.push(table([
    ["指令", "语法", "语义", "品牌映射"],
    ["movej", "movej home speed v_joint zone fine", "关节空间运动，适合回零和快速接近。", "ABB MoveJ / FANUC J / KUKA PTP"],
    ["movel", "movel pick speed v_linear zone z10", "TCP 直线运动，适合接近、加工、搬运直线段。", "ABB MoveL / FANUC L / KUKA LIN"],
    ["movec", "movec via mid target arc_end speed v zone z10", "TCP 圆弧运动，通过 via 点到终点。", "ABB MoveC / FANUC C / KUKA CIRC"],
    ["run_path", "run_path cart_path", "执行已声明路径。", "可展开为品牌运动语句"],
    ["run_operation", "run_operation pick_op", "执行工艺操作，包含 action 和 path。", "按品牌能力展开或报告"]
  ]));
  body.push(p("下面是当前工作区中的笛卡尔混合路径示例，包含 tool/frame、表达式速度、zone clamp、MoveL、MoveC 和 run_path。"));
  body.push(codeBlock(readFileSync(blendSourcePath, "utf8")));

  body.push(heading("七、Path 和 Operation", 1));
  body.push(p("Path 是离线编程的核心对象，适合承载 CAD 曲线、示教点、工艺采样点和路径事件。Operation 是对 Path 的工艺封装，适合表达搬运、焊接、喷涂、打磨、上下料等任务。"));
  body.push(table([
    ["写法", "说明"],
    ["defaults", "为路径点设置默认 speed、zone、tool、frame，减少重复。"],
    ["source", "记录 CAD 曲线、采样间距、规划来源等元数据。"],
    ["point", "定义路径点和该点运动方式。"],
    ["event before/after/at", "定义路径事件，例如到点后开夹爪、距离某点前开焊接。"],
    ["operation process", "记录工艺参数，例如 gripper、weld_mode、paint_recipe。"],
    ["start_action/end_action", "操作开始或结束时执行 IO、wait、pulse 等动作。"]
  ]));

  body.push(heading("八、IO、wait 和 pulse", 1));
  body.push(p("GRL 中的 IO 使用统一引用格式：io.di、io.do、io.ai、io.ao、io.gi、io.go、io.ri、io.ro 和 io.alias。离线系统运行时维护虚拟 IO 镜像，程序、用户和脚本都可以写入这份镜像。"));
  body.push(table([
    ["语法", "含义"],
    ["io.do[1] = true", "写数字输出。"],
    ["io.ao[1] = 0.75", "写模拟输出。"],
    ["wait io.di[1] == true timeout 2 s", "等待输入满足条件，超时产生诊断或报警。"],
    ["wait all(...)", "所有条件满足才继续。"],
    ["wait any(...)", "任一条件满足即继续。"],
    ["rising/falling/changed", "边沿或变化检测。"],
    ["pulse io.do[2] duration 100 ms", "输出置 true，持续指定时间后自动复位。"]
  ]));
  body.push(codeBlock(readFileSync(ioSourcePath, "utf8")));

  body.push(heading("九、流程控制、子程序和异常", 1));
  body.push(p("GRL 支持结构化控制和子程序。简单运行逻辑写在 proc 中，纯计算逻辑写在 func 中。func 默认不应产生运动、IO、wait、pulse 等副作用。"));
  body.push(table([
    ["能力", "语法", "说明"],
    ["条件", "if / elseif / else / end", "按布尔条件选择执行分支。"],
    ["循环", "while、for to step", "执行重复逻辑，break/continue 必须处于合法块内。"],
    ["多分支", "switch / case / default", "case 必须是常量表达式，不能重复。"],
    ["子程序", "proc name(in int slot) ... end", "可定义 in/out/inout 参数。"],
    ["函数", "func bool ok() return ... end", "有返回值，建议无副作用。"],
    ["报警", "alarm ID \"message\" severity warning", "进入虚拟控制器报警队列。"],
    ["异常", "raise、try/catch/finally", "P0 支持基础语义，复杂品牌恢复分阶段扩展。"]
  ]));

  body.push(heading("十、从零创建一个 GRL 程序", 1));
  body.push(p("建议按以下顺序创建程序。先保证数据和坐标清楚，再组织路径和工艺，最后在 main 中调用。"));
  body.push(table([
    ["步骤", "操作", "检查点"],
    ["1", "创建 .grl 文件，写 language 和 module。", "模块名使用 ASCII 标识符，文件 UTF-8。"],
    ["2", "声明 tool 和 frame。", "TCP、质量、工件坐标单位明确。"],
    ["3", "声明 speed 和 zone。", "速度和过渡统一命名，例如 v_fast、v_pick、z10。"],
    ["4", "声明 target。", "home 用 joint_target，工艺点用 pose_target。"],
    ["5", "创建 path。", "设置 defaults，点名按 p000/p001 或语义名称稳定输出。"],
    ["6", "添加 path event 或 operation action。", "夹具、等待、脉冲、工艺开关不要散落在 main 中。"],
    ["7", "创建 operation。", "把 path 和工艺参数封装成可复用操作。"],
    ["8", "编写 proc main。", "只做 set_tool/set_frame、run_operation、回 home 等主流程。"],
    ["9", "运行编译和测试。", "执行 npm test 或 suite，检查 diagnostics、trace、motion queue。"],
    ["10", "后处理输出。", "检查 ABB/FANUC/KUKA 转换报告，不支持项不能忽略。"]
  ]));

  body.push(heading("十一、完整程序模板", 1));
  body.push(p("下面给出一个可直接改写的模板，适合搬运、上下料、夹具动作和基础路径。"));
  body.push(codeBlock(`language grl 0.1
module DemoPickPlace
  persistent tool gripper = tool {
    tcp: pose(0 mm, 0 mm, 120 mm, 0 deg, 0 deg, 0 deg),
    mass: 1.5 kg
  }

  persistent frame fixture = frame {
    origin: pose(800 mm, 0 mm, 0 mm, 0 deg, 0 deg, 0 deg)
  }

  const speed v_joint = joint(50 %)
  const speed v_pick = linear(250 mm/s)
  const zone z10 = z(10 mm)
  const zone zf = fine

  target home = joint_target { joints: [0 deg, 0 deg, 0 deg, 0 deg, 0 deg, 0 deg] }
  target pick = pose_target { pose: pose(500 mm, 100 mm, 200 mm, 180 deg, 0 deg, 90 deg), tool: gripper, frame: fixture }
  target place = pose_target { pose: pose(650 mm, -100 mm, 200 mm, 180 deg, 0 deg, 90 deg), tool: gripper, frame: fixture }

  path pick_path {
    defaults { speed: v_pick, zone: z10, tool: gripper, frame: fixture }
    point approach movel pick offset z 100 mm
    point at_pick movel pick zone zf
    event after at_pick io.do[1] = true
    event after at_pick wait io.di[1] == true timeout 2 s on_timeout alarm "Clamp close timeout"
    point leave movel pick offset z 100 mm
  }

  operation pick_op {
    kind: handling
    path: pick_path
    process { gripper: "main_clamp" }
  }

  proc main()
    set_tool gripper
    set_frame fixture
    movej home speed v_joint zone zf
    run_operation pick_op
    movej home speed v_joint zone zf
  end
end`));

  body.push(heading("十二、编译、仿真和截图验证", 1));
  body.push(imageWithCaption(images[3], 4, "图 4 虚拟控制器执行流程图"));
  body.push(p("编译后，虚拟控制器会加载 IR，从 main 开始执行，采集运行 trace、motion queue、IO image、报警队列和诊断。桌面端和移动端界面可用于检查程序结构、状态、编辑器、面板和运行结果。"));
  body.push(imageWithCaption(images[5], 6, "图 5 虚拟控制器桌面端界面"));
  body.push(imageWithCaption(images[6], 7, "图 6 虚拟控制器移动端界面"));
  body.push(table([
    ["命令", "用途"],
    ["npm run typecheck", "检查 TypeScript 类型。"],
    ["npm test", "运行 KDL、GRL、runtime、post/import、integration 全量测试。"],
    ["npm run suite:abb120-spec", "运行 19 个 ABB120 GRL 规范程序，生成 job/report/program 证据。"],
    ["npm run verify:virtual-controller", "打开虚拟控制器 HTML，执行交互并生成桌面/移动截图。"]
  ]));

  body.push(heading("十三、后处理和跨品牌输出", 1));
  body.push(imageWithCaption(images[4], 5, "图 7 后处理与导入回读流程图"));
  body.push(table([
    ["GRL", "ABB RAPID", "FANUC LS/TP", "KUKA KRL"],
    ["module", "MODULE", "/PROG 或模块", "DEF/.src"],
    ["proc", "PROC", "CALL 子程序", "DEF 子程序"],
    ["movej", "MoveJ", "J P[...] ", "PTP"],
    ["movel", "MoveL", "L P[...] ", "LIN"],
    ["movec", "MoveC", "C P[...] ", "CIRC"],
    ["io.do[1] = true", "SetDO do1, 1", "DO[1]=ON", "$OUT[1]=TRUE"],
    ["wait io.di[1] == true", "WaitDI di1, 1", "WAIT DI[1]=ON", "WAIT FOR $IN[1]==TRUE"]
  ]));
  body.push(p("后处理器遇到品牌不支持或只能近似表达的语义时，应写入转换报告。用户必须检查报告，再到真实控制器中做品牌校验和现场低速试运行。"));

  body.push(heading("十四、常见诊断和处理建议", 1));
  body.push(table([
    ["诊断", "常见原因", "处理方式"],
    ["GRL_EXPR_UNKNOWN_SYMBOL", "表达式引用未声明常量或变量。", "检查拼写和声明顺序。"],
    ["GRL_EXPR_UNIT_MISMATCH", "不同物理单位混合计算。", "统一长度、时间、角度或速度维度。"],
    ["GRL_TARGET_NOT_FOUND", "运动或路径点引用不存在目标点。", "补充 target 或修正名称。"],
    ["KDL_TARGET_UNREACHABLE", "笛卡尔点超出机器人工作空间。", "调整目标点、工具、坐标系或机器人配置。"],
    ["KDL_JOINT_LIMIT", "目标关节超过限位。", "修改 joint_target 或使用 IK 可达姿态。"],
    ["GRL_PATH_EMPTY", "path 没有 point。", "至少添加一个 point。"],
    ["VC_WAIT_TIMEOUT", "wait 条件在 timeout 内未满足。", "检查 IO 输入、脚本、超时时间和 on_timeout 动作。"],
    ["GRL_POST_UNSUPPORTED", "目标品牌后处理暂不支持该语义。", "查看转换报告，必要时改写为品牌可支持结构。"]
  ]));

  body.push(heading("十五、编写规范建议", 1));
  body.push(table([
    ["建议", "说明"],
    ["名称稳定", "target、path point、operation 命名稳定，便于 diff 和回读。"],
    ["显式单位", "自动生成程序尽量输出 mm、deg、mm/s、ms 等单位。"],
    ["入口简短", "main 中只保留主流程调用，复杂路径放入 path/operation。"],
    ["默认集中", "path defaults 中集中设置 speed、zone、tool、frame。"],
    ["IO 事件贴近路径", "与路径点相关的夹具和工艺开关写成 event。"],
    ["诊断必看", "error 必须修复；warning 必须人工确认；info 可作为转换记录。"],
    ["先仿真后后处理", "先通过 GRL 编译和虚拟控制器，再输出品牌程序。"],
    ["真实运行前校验", "后处理程序必须经过品牌控制器验证和现场低速试运行。"]
  ]));

  body.push(heading("十六、参考完整示例", 1));
  body.push(p("以下是规范套件中的完整示例程序，覆盖 tool/frame、path、operation、IO event、run_operation 和回 home，可作为复杂程序的参考。"));
  body.push(codeBlock(readFileSync(fullExamplePath, "utf8")));

  return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:document xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  xmlns:o="urn:schemas-microsoft-com:office:office"
  xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
  xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"
  xmlns:v="urn:schemas-microsoft-com:vml"
  xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing"
  xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
  xmlns:w10="urn:schemas-microsoft-com:office:word"
  xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
  xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml"
  xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml"
  xmlns:w16se="http://schemas.microsoft.com/office/word/2015/wordml/symex"
  xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup"
  xmlns:wpi="http://schemas.microsoft.com/office/word/2010/wordprocessingInk"
  xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml"
  xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape"
  mc:Ignorable="w14 w15 w16se wp14">
  <w:body>
    ${body.join("\n")}
    <w:sectPr>
      <w:pgSz w:w="11906" w:h="16838"/>
      <w:pgMar w:top="1134" w:right="850" w:bottom="1134" w:left="850" w:header="708" w:footer="708" w:gutter="0"/>
      <w:cols w:space="708"/>
      <w:docGrid w:linePitch="312"/>
    </w:sectPr>
  </w:body>
</w:document>`;
}

function copyImages(imageInputs) {
  return imageInputs.map((image, index) => {
    const fileName = `image-${String(index + 1).padStart(2, "0")}.png`;
    copyFileSync(image.source, join(stagingRoot, "word", "media", fileName));
    return {
      ...image,
      id: `rIdImage${index + 1}`,
      fileName
    };
  });
}

function heading(text, level) {
  return paragraph(text, `Heading${Math.min(level, 3)}`);
}

function p(text) {
  return paragraph(text, "BodyText");
}

function paragraph(text, style) {
  return `<w:p><w:pPr><w:pStyle w:val="${style}"/></w:pPr>${run(text)}</w:p>`;
}

function run(text) {
  return `<w:r><w:t xml:space="preserve">${escapeXml(String(text))}</w:t></w:r>`;
}

function table(rows) {
  const width = Math.floor(9200 / Math.max(1, rows[0]?.length ?? 1));
  return `<w:tbl>
    <w:tblPr><w:tblStyle w:val="TableGrid"/><w:tblW w:w="0" w:type="auto"/><w:tblLook w:val="04A0"/></w:tblPr>
    ${rows.map((row, rowIndex) => `<w:tr>${row.map((cell) => tableCell(cell, width, rowIndex === 0)).join("")}</w:tr>`).join("\n")}
  </w:tbl>`;
}

function tableCell(text, width, header) {
  return `<w:tc>
    <w:tcPr><w:tcW w:w="${width}" w:type="dxa"/></w:tcPr>
    <w:p><w:pPr>${header ? "<w:pStyle w:val=\"TableHeader\"/>" : ""}</w:pPr>${run(text)}</w:p>
  </w:tc>`;
}

function codeBlock(text) {
  const normalized = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
  const lines = normalized.endsWith("\n") ? normalized.slice(0, -1).split("\n") : normalized.split("\n");
  return lines.map((line) =>
    `<w:p><w:pPr><w:pStyle w:val="Code"/></w:pPr><w:r><w:t xml:space="preserve">${escapeXml(line === "" ? " " : line)}</w:t></w:r></w:p>`
  ).join("\n");
}

function imageWithCaption(image, docPrId, caption) {
  return `${imageParagraph(image, docPrId)}${paragraph(caption, "Caption")}`;
}

function imageParagraph(image, docPrId) {
  return `<w:p><w:pPr><w:jc w:val="center"/></w:pPr><w:r><w:drawing>
    <wp:inline distT="0" distB="0" distL="0" distR="0">
      <wp:extent cx="${image.cx}" cy="${image.cy}"/>
      <wp:effectExtent l="0" t="0" r="0" b="0"/>
      <wp:docPr id="${docPrId}" name="${escapeXml(image.title)}"/>
      <wp:cNvGraphicFramePr><a:graphicFrameLocks xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" noChangeAspect="1"/></wp:cNvGraphicFramePr>
      <a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
        <a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">
          <pic:pic xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture">
            <pic:nvPicPr><pic:cNvPr id="${docPrId}" name="${escapeXml(image.fileName)}"/><pic:cNvPicPr/></pic:nvPicPr>
            <pic:blipFill><a:blip r:embed="${image.id}"/><a:stretch><a:fillRect/></a:stretch></pic:blipFill>
            <pic:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="${image.cx}" cy="${image.cy}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></pic:spPr>
          </pic:pic>
        </a:graphicData>
      </a:graphic>
    </wp:inline>
  </w:drawing></w:r></w:p>`;
}

function contentTypesXml() {
  return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
  <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
  <Default Extension="xml" ContentType="application/xml"/>
  <Default Extension="png" ContentType="image/png"/>
  <Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
  <Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/>
  <Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>
  <Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>
</Types>`;
}

function packageRelsXml() {
  return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
  <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
  <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>
  <Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/>
</Relationships>`;
}

function documentRelsXml(images) {
  return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
  <Relationship Id="rIdStyles" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
  ${images.map((image) => `<Relationship Id="${image.id}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/${image.fileName}"/>`).join("\n")}
</Relationships>`;
}

function corePropsXml() {
  const iso = new Date().toISOString();
  return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties"
  xmlns:dc="http://purl.org/dc/elements/1.1/"
  xmlns:dcterms="http://purl.org/dc/terms/"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <dc:title>GRL 功能语法逻辑与程序创建使用手册</dc:title>
  <dc:creator>Codex</dc:creator>
  <cp:lastModifiedBy>Codex</cp:lastModifiedBy>
  <dcterms:created xsi:type="dcterms:W3CDTF">${iso}</dcterms:created>
  <dcterms:modified xsi:type="dcterms:W3CDTF">${iso}</dcterms:modified>
</cp:coreProperties>`;
}

function appPropsXml() {
  return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"
  xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">
  <Application>Codex</Application>
  <DocSecurity>0</DocSecurity>
  <ScaleCrop>false</ScaleCrop>
  <Company>KDL Work</Company>
  <LinksUpToDate>false</LinksUpToDate>
  <SharedDoc>false</SharedDoc>
  <HyperlinksChanged>false</HyperlinksChanged>
  <AppVersion>16.0000</AppVersion>
</Properties>`;
}

function stylesXml() {
  return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
  <w:style w:type="paragraph" w:default="1" w:styleId="Normal">
    <w:name w:val="Normal"/>
    <w:qFormat/>
    <w:rPr><w:rFonts w:ascii="Microsoft YaHei" w:hAnsi="Microsoft YaHei" w:eastAsia="Microsoft YaHei"/><w:sz w:val="21"/></w:rPr>
  </w:style>
  <w:style w:type="paragraph" w:styleId="BodyText">
    <w:name w:val="Body Text"/>
    <w:basedOn w:val="Normal"/>
    <w:pPr><w:spacing w:after="120" w:line="276" w:lineRule="auto"/></w:pPr>
    <w:rPr><w:rFonts w:ascii="Microsoft YaHei" w:hAnsi="Microsoft YaHei" w:eastAsia="Microsoft YaHei"/><w:sz w:val="21"/></w:rPr>
  </w:style>
  <w:style w:type="paragraph" w:styleId="Caption">
    <w:name w:val="Caption"/>
    <w:basedOn w:val="Normal"/>
    <w:pPr><w:jc w:val="center"/><w:spacing w:after="160"/></w:pPr>
    <w:rPr><w:i/><w:rFonts w:ascii="Microsoft YaHei" w:hAnsi="Microsoft YaHei" w:eastAsia="Microsoft YaHei"/><w:sz w:val="18"/><w:color w:val="475569"/></w:rPr>
  </w:style>
  <w:style w:type="paragraph" w:styleId="Heading1">
    <w:name w:val="heading 1"/>
    <w:basedOn w:val="Normal"/>
    <w:next w:val="BodyText"/>
    <w:qFormat/>
    <w:pPr><w:keepNext/><w:spacing w:before="360" w:after="180"/></w:pPr>
    <w:rPr><w:b/><w:rFonts w:ascii="Microsoft YaHei" w:hAnsi="Microsoft YaHei" w:eastAsia="Microsoft YaHei"/><w:sz w:val="32"/></w:rPr>
  </w:style>
  <w:style w:type="paragraph" w:styleId="Heading2">
    <w:name w:val="heading 2"/>
    <w:basedOn w:val="Normal"/>
    <w:next w:val="BodyText"/>
    <w:qFormat/>
    <w:pPr><w:keepNext/><w:spacing w:before="280" w:after="140"/></w:pPr>
    <w:rPr><w:b/><w:rFonts w:ascii="Microsoft YaHei" w:hAnsi="Microsoft YaHei" w:eastAsia="Microsoft YaHei"/><w:sz w:val="27"/></w:rPr>
  </w:style>
  <w:style w:type="paragraph" w:styleId="Heading3">
    <w:name w:val="heading 3"/>
    <w:basedOn w:val="Normal"/>
    <w:next w:val="BodyText"/>
    <w:qFormat/>
    <w:pPr><w:keepNext/><w:spacing w:before="180" w:after="100"/></w:pPr>
    <w:rPr><w:b/><w:rFonts w:ascii="Microsoft YaHei" w:hAnsi="Microsoft YaHei" w:eastAsia="Microsoft YaHei"/><w:sz w:val="23"/></w:rPr>
  </w:style>
  <w:style w:type="paragraph" w:styleId="Code">
    <w:name w:val="Code"/>
    <w:basedOn w:val="Normal"/>
    <w:pPr><w:spacing w:before="0" w:after="0" w:line="240" w:lineRule="auto"/></w:pPr>
    <w:rPr><w:rFonts w:ascii="Consolas" w:hAnsi="Consolas" w:eastAsia="Consolas"/><w:sz w:val="16"/></w:rPr>
  </w:style>
  <w:style w:type="paragraph" w:styleId="TableHeader">
    <w:name w:val="Table Header"/>
    <w:basedOn w:val="Normal"/>
    <w:rPr><w:b/><w:rFonts w:ascii="Microsoft YaHei" w:hAnsi="Microsoft YaHei" w:eastAsia="Microsoft YaHei"/><w:sz w:val="20"/></w:rPr>
  </w:style>
  <w:style w:type="table" w:default="1" w:styleId="TableNormal">
    <w:name w:val="Normal Table"/>
    <w:tblPr><w:tblInd w:w="0" w:type="dxa"/><w:tblCellMar><w:top w:w="0" w:type="dxa"/><w:left w:w="108" w:type="dxa"/><w:bottom w:w="0" w:type="dxa"/><w:right w:w="108" w:type="dxa"/></w:tblCellMar></w:tblPr>
  </w:style>
  <w:style w:type="table" w:styleId="TableGrid">
    <w:name w:val="Table Grid"/>
    <w:basedOn w:val="TableNormal"/>
    <w:tblPr><w:tblBorders><w:top w:val="single" w:sz="4" w:space="0" w:color="B7C1CC"/><w:left w:val="single" w:sz="4" w:space="0" w:color="B7C1CC"/><w:bottom w:val="single" w:sz="4" w:space="0" w:color="B7C1CC"/><w:right w:val="single" w:sz="4" w:space="0" w:color="B7C1CC"/><w:insideH w:val="single" w:sz="4" w:space="0" w:color="B7C1CC"/><w:insideV w:val="single" w:sz="4" w:space="0" w:color="B7C1CC"/></w:tblBorders></w:tblPr>
  </w:style>
</w:styles>`;
}

function escapeXml(value) {
  return String(value)
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll("\"", "&quot;")
    .replaceAll("'", "&apos;");
}

function psSingleQuoted(value) {
  return `'${String(value).replaceAll("'", "''")}'`;
}

function zipDirectory(sourceDir, destinationFile) {
  const script = [
    "Add-Type -AssemblyName System.IO.Compression.FileSystem",
    `$src = ${psSingleQuoted(sourceDir)}`,
    `$dst = ${psSingleQuoted(destinationFile)}`,
    "if (Test-Path -LiteralPath $dst) { Remove-Item -LiteralPath $dst -Force }",
    "[System.IO.Compression.ZipFile]::CreateFromDirectory($src, $dst)"
  ].join("\n");
  execFileSync("powershell", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script], {
    stdio: "inherit"
  });
}

function assertSafeStaging(path) {
  const allowedRoot = join(webRoot, "test-results") + sep;
  const resolved = resolve(path);
  if (!resolved.startsWith(allowedRoot)) {
    throw new Error(`Refusing to clear staging path outside test-results: ${resolved}`);
  }
}

function formatShanghaiTime(date) {
  const parts = new Intl.DateTimeFormat("zh-CN", {
    timeZone: "Asia/Shanghai",
    year: "numeric",
    month: "2-digit",
    day: "2-digit",
    hour: "2-digit",
    minute: "2-digit",
    second: "2-digit",
    hour12: false
  }).formatToParts(date);
  const value = Object.fromEntries(parts.map((part) => [part.type, part.value]));
  return `${value.year}-${value.month}-${value.day} ${value.hour}:${value.minute}:${value.second} Asia/Shanghai`;
}
