import { createHash } from "node:crypto";
import {
  copyFileSync,
  cpSync,
  existsSync,
  mkdirSync,
  readdirSync,
  readFileSync,
  rmSync,
  statSync,
  writeFileSync
} from "node:fs";
import { basename, dirname, join, relative, 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 deployRoot = join(webRoot, "test-results", "deploy");
const appRoot = join(webRoot, "app");
const specRoot = join(webRoot, "tests", "fixtures", "abb120", "spec-programs");
const resultRoot = join(webRoot, "test-results", "abb120-spec");
const uiResultRoot = join(webRoot, "test-results", "virtual-controller");
const working2Root = join(root, "working2");
const docsRoot = join(root, "work", "doc");

const jobId = getArg("--job") ?? latestJobId();
const generatedAt = new Date();
const releaseId = getArg("--release") ?? `${formatShanghaiId(generatedAt)}-${jobId}`;
const packageName = `kdl-olp-demo-${releaseId}`;
const packageDir = join(deployRoot, packageName);
const zipPath = join(deployRoot, `${packageName}.zip`);

const jobDir = join(resultRoot, jobId);
const manifest = readJson(join(specRoot, "manifest.json"));
const job = readJson(join(jobDir, "job.json"));
const report = readJson(join(jobDir, "report.json"));
const uiEvidence = readJson(join(uiResultRoot, "evidence.json"));

assertReportIsDeployable(report);
assertJobMatchesInputs(job, report, manifest);
assertSafeDeployPath(packageDir);
mkdirSync(deployRoot, { recursive: true });
rmSync(packageDir, { recursive: true, force: true });
mkdirSync(packageDir, { recursive: true });

copyTree(appRoot, join(packageDir, "app"));
copyTree(specRoot, join(packageDir, "spec-programs"));
copyTree(jobDir, join(packageDir, "test-results", "abb120-spec", jobId));
copyTree(uiResultRoot, join(packageDir, "test-results", "virtual-controller"));
copyDocs(join(packageDir, "docs"));
copyWorking2Docs(join(packageDir, "docs", "working2"));

const demoManifest = buildDemoManifest();
writeJson(join(packageDir, "demo-manifest.json"), demoManifest);
writeFileSync(join(packageDir, "index.html"), indexHtml(demoManifest), "utf8");
writeFileSync(join(packageDir, "README.md"), packageReadme(demoManifest), "utf8");

zipDirectory(packageDir, zipPath);
const zip = {
  path: slash(relative(root, zipPath)),
  bytes: statSync(zipPath).size,
  sha256: sha256File(zipPath)
};
const summary = {
  release_id: releaseId,
  generated_at: generatedAt.toISOString(),
  job_id: jobId,
  package_dir: slash(relative(root, packageDir)),
  zip,
  program_count: demoManifest.program_count,
  report_fail: report.summary.fail
};
writeJson(join(packageDir, "demo-build-summary.json"), summary);
writeJson(join(deployRoot, `${packageName}.summary.json`), summary);

console.log(JSON.stringify(summary, null, 2));

function buildDemoManifest() {
  const levelCounts = countLevels(manifest.programs);
  const programs = manifest.programs.map((entry) => {
    const jobProgram = job.programs.find((program) => program.program_id === entry.program_id);
    return {
      program_id: entry.program_id,
      file: entry.file,
      source: `spec-programs/${entry.file}`,
      coverage_level: entry.coverage_level,
      spec_sections: entry.spec_sections,
      expected_status: entry.expected_status,
      status: jobProgram?.status ?? "missing",
      diagnostics: jobProgram?.diagnostics?.map((diagnostic) => diagnostic.code).filter(Boolean) ?? [],
      artifacts: artifactLinks(entry.program_id)
    };
  });

  return {
    release_id: releaseId,
    generated_at: generatedAt.toISOString(),
    suite_id: manifest.suite_id,
    job_id: job.job_id,
    robot_id: manifest.robot_id,
    spec_source: manifest.spec_source,
    program_count: manifest.programs.length,
    runtime_programs: levelCounts.Runtime,
    static_programs: levelCounts.Static,
    post_programs: levelCounts.Post,
    entry: "app/virtual-controller.html",
    virtual_controller_entry: "app/virtual-controller.html",
    report_json: `test-results/abb120-spec/${job.job_id}/report.json`,
    report_html: `test-results/abb120-spec/${job.job_id}/report.html`,
    job_json: `test-results/abb120-spec/${job.job_id}/job.json`,
    program_manifest: "spec-programs/manifest.json",
    screenshots: {
      desktop: "test-results/virtual-controller/virtual-controller-desktop.png",
      mobile: "test-results/virtual-controller/virtual-controller-mobile.png",
      evidence: "test-results/virtual-controller/evidence.json"
    },
    docs: [
      {
        title: "GRL user manual",
        href: "docs/grl-user-manual.docx",
        kind: "docx"
      },
      {
        title: "OLP test document",
        href: "docs/olp-test-document.docx",
        kind: "docx"
      },
      {
        title: "Working2 acceptance evidence",
        href: "docs/working2/acceptance-evidence.md",
        kind: "markdown"
      }
    ],
    coverage: report.coverage,
    summary: report.summary,
    programs
  };
}

function artifactLinks(programId) {
  const base = `test-results/abb120-spec/${job.job_id}/programs/${programId}`;
  return {
    compile: `${base}/compile.json`,
    controller: `${base}/controller.json`,
    motion_queue: `${base}/motion-queue.json`,
    trace: `${base}/trace.json`,
    io: `${base}/io.json`,
    trajectory: `${base}/trajectory.json`,
    post_report: `${base}/post-report.json`,
    roundtrip: `${base}/roundtrip.json`
  };
}

function copyDocs(targetDir) {
  mkdirSync(targetDir, { recursive: true });
  const docs = [
    ["GRL功能语法逻辑与程序创建使用手册.docx", "grl-user-manual.docx"],
    ["通用机器人离线编程系统测试文档.docx", "olp-test-document.docx"]
  ];
  for (const [doc, alias] of docs) {
    const source = join(docsRoot, doc);
    if (!existsSync(source)) {
      throw new Error(`Required document is missing: ${source}`);
    }
    copyFileSync(source, join(targetDir, doc));
    copyFileSync(source, join(targetDir, alias));
  }
}

function copyWorking2Docs(targetDir) {
  mkdirSync(targetDir, { recursive: true });
  for (const entry of readdirSync(working2Root, { withFileTypes: true })) {
    if (entry.isFile() && entry.name.endsWith(".md")) {
      copyFileSync(join(working2Root, entry.name), join(targetDir, entry.name));
    }
  }
  const aliases = {
    "01-项目功能内容.md": "project-scope.md",
    "02-项目程序开发详细步骤.md": "development-steps.md",
    "03-推进台账.md": "progress-log.md",
    "04-任务矩阵.md": "task-matrix.md",
    "05-验收证据.md": "acceptance-evidence.md",
    "06-决策记录.md": "decisions.md",
    "07-服务器发布与演示包实施方案.md": "server-deploy-plan.md"
  };
  for (const [sourceName, aliasName] of Object.entries(aliases)) {
    const source = join(working2Root, sourceName);
    if (existsSync(source)) {
      copyFileSync(source, join(targetDir, aliasName));
    }
  }
  const manifestDir = join(working2Root, "programs");
  if (existsSync(manifestDir)) {
    copyTree(manifestDir, join(targetDir, "programs"));
    const manifestSource = join(manifestDir, "manifest.md");
    if (existsSync(manifestSource)) {
      copyFileSync(manifestSource, join(targetDir, "program-manifest.md"));
    }
  }
}

function indexHtml(demoManifest) {
  const rows = demoManifest.programs.map((program) => `
        <tr>
          <td><a href="${escapeAttr(program.source)}">${escapeHtml(program.program_id)}</a></td>
          <td>${escapeHtml(program.coverage_level)}</td>
          <td>${escapeHtml(program.status)}</td>
          <td>${escapeHtml(program.spec_sections.join(", "))}</td>
          <td><a href="${escapeAttr(program.artifacts.compile)}">compile</a></td>
        </tr>`).join("");
  return `<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>KDL OLP Demo ${escapeHtml(demoManifest.release_id)}</title>
  <style>
    :root {
      color-scheme: light;
      --ink: #172026;
      --muted: #5e6a71;
      --line: #cfd7dc;
      --panel: #f6f8f9;
      --accent: #1f7a8c;
      --accent-2: #7b6d3a;
      font-family: Arial, "Microsoft YaHei", sans-serif;
    }
    * { box-sizing: border-box; }
    body { margin: 0; color: var(--ink); background: #ffffff; }
    header { padding: 28px 32px 18px; border-bottom: 1px solid var(--line); background: var(--panel); }
    h1 { margin: 0 0 10px; font-size: 28px; line-height: 1.2; letter-spacing: 0; }
    p { margin: 0; color: var(--muted); line-height: 1.55; }
    main { max-width: 1180px; margin: 0 auto; padding: 24px 24px 40px; }
    .actions { display: flex; flex-wrap: wrap; gap: 10px; margin: 18px 0 24px; }
    .actions a { color: #fff; background: var(--accent); text-decoration: none; padding: 10px 13px; border-radius: 6px; font-weight: 700; }
    .actions a.secondary { background: var(--accent-2); }
    .summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; margin-bottom: 24px; }
    .summary div { border: 1px solid var(--line); border-radius: 6px; padding: 12px; background: #fff; }
    .summary strong { display: block; font-size: 22px; }
    section { margin-top: 28px; }
    h2 { font-size: 18px; margin: 0 0 12px; }
    table { width: 100%; border-collapse: collapse; font-size: 14px; }
    th, td { border: 1px solid var(--line); padding: 9px 10px; text-align: left; vertical-align: top; }
    th { background: var(--panel); }
    img { max-width: 100%; height: auto; border: 1px solid var(--line); border-radius: 6px; }
    .screens { display: grid; grid-template-columns: minmax(0, 2fr) minmax(220px, 1fr); gap: 16px; align-items: start; }
    @media (max-width: 760px) {
      header { padding: 22px 18px 14px; }
      main { padding: 18px 14px 32px; }
      h1 { font-size: 23px; }
      .screens { grid-template-columns: 1fr; }
      table { font-size: 13px; }
      th, td { padding: 7px; }
    }
  </style>
</head>
<body>
  <header>
    <h1>KDL OLP ABB120 Working2 Demo</h1>
    <p>Release ${escapeHtml(demoManifest.release_id)} / Job ${escapeHtml(demoManifest.job_id)} / Suite ${escapeHtml(demoManifest.suite_id)}</p>
  </header>
  <main>
    <nav class="actions" aria-label="demo links">
      <a href="${escapeAttr(demoManifest.virtual_controller_entry)}">Open virtual controller</a>
      <a href="${escapeAttr(demoManifest.report_html)}" class="secondary">Open report</a>
      <a href="${escapeAttr(demoManifest.program_manifest)}" class="secondary">Program manifest</a>
      <a href="demo-manifest.json" class="secondary">Demo manifest</a>
    </nav>
    <div class="summary">
      <div><span>Programs</span><strong>${demoManifest.program_count}</strong></div>
      <div><span>Runtime</span><strong>${demoManifest.runtime_programs}</strong></div>
      <div><span>Static</span><strong>${demoManifest.static_programs}</strong></div>
      <div><span>Post</span><strong>${demoManifest.post_programs}</strong></div>
      <div><span>Fail</span><strong>${demoManifest.summary.fail}</strong></div>
    </div>
    <section>
      <h2>Program Evidence</h2>
      <table>
        <thead><tr><th>Program</th><th>Layer</th><th>Status</th><th>Spec Sections</th><th>Artifact</th></tr></thead>
        <tbody>${rows}
        </tbody>
      </table>
    </section>
    <section>
      <h2>Screenshots</h2>
      <div class="screens">
        <a href="${escapeAttr(demoManifest.screenshots.desktop)}"><img src="${escapeAttr(demoManifest.screenshots.desktop)}" alt="Virtual controller desktop screenshot"></a>
        <a href="${escapeAttr(demoManifest.screenshots.mobile)}"><img src="${escapeAttr(demoManifest.screenshots.mobile)}" alt="Virtual controller mobile screenshot"></a>
      </div>
    </section>
    <section>
      <h2>Documents</h2>
      <table>
        <tbody>
          ${demoManifest.docs.map((doc) => `<tr><td>${escapeHtml(doc.title)}</td><td><a href="${escapeAttr(doc.href)}">${escapeHtml(doc.href)}</a></td></tr>`).join("\n          ")}
        </tbody>
      </table>
    </section>
  </main>
</body>
</html>
`;
}

function packageReadme(demoManifest) {
  return [
    `# KDL OLP demo ${demoManifest.release_id}`,
    "",
    `Job: ${demoManifest.job_id}`,
    `Suite: ${demoManifest.suite_id}`,
    `Programs: ${demoManifest.program_count}`,
    `Report: ${demoManifest.report_html}`,
    `Virtual controller: ${demoManifest.virtual_controller_entry}`,
    "",
    "Serve this directory as the HTTPS document root for the demo port."
  ].join("\n");
}

function latestJobId() {
  if (!existsSync(resultRoot)) {
    throw new Error(`No ABB120 spec result directory found: ${resultRoot}`);
  }
  const jobs = readdirSync(resultRoot, { withFileTypes: true })
    .filter((entry) => entry.isDirectory() && entry.name.startsWith("W2-JOB-"))
    .map((entry) => ({
      name: entry.name,
      mtimeMs: statSync(join(resultRoot, entry.name)).mtimeMs
    }))
    .sort((left, right) => right.mtimeMs - left.mtimeMs || right.name.localeCompare(left.name));
  if (jobs.length === 0) {
    throw new Error(`No W2-JOB-* result directories found in ${resultRoot}`);
  }
  return jobs[0].name;
}

function assertReportIsDeployable(report) {
  if (report.summary?.programs !== 19) {
    throw new Error(`Expected report.summary.programs=19, got ${report.summary?.programs}`);
  }
  if (report.summary?.fail !== 0) {
    throw new Error(`Expected report.summary.fail=0, got ${report.summary?.fail}`);
  }
  if (report.coverage?.missingSections?.length !== 0) {
    throw new Error(`Expected no missing sections, got ${report.coverage.missingSections.join(",")}`);
  }
}

function assertJobMatchesInputs(job, report, manifest) {
  if (job.job_id !== jobId || report.job_id !== jobId) {
    throw new Error(`Job id mismatch: requested ${jobId}, job=${job.job_id}, report=${report.job_id}`);
  }
  if (job.suite_id !== manifest.suite_id || report.suite_id !== manifest.suite_id) {
    throw new Error("Suite id mismatch between manifest, job, and report");
  }
  if (manifest.programs.length !== 19 || job.programs.length !== 19) {
    throw new Error(`Expected 19 programs, manifest=${manifest.programs.length}, job=${job.programs.length}`);
  }
}

function countLevels(programs) {
  return programs.reduce((counts, program) => {
    counts[program.coverage_level] = (counts[program.coverage_level] ?? 0) + 1;
    return counts;
  }, { Runtime: 0, Static: 0, Post: 0 });
}

function copyTree(source, target) {
  if (!existsSync(source)) {
    throw new Error(`Required path is missing: ${source}`);
  }
  mkdirSync(dirname(target), { recursive: true });
  cpSync(source, target, { recursive: true, force: true });
}

function readJson(file) {
  return JSON.parse(readFileSync(file, "utf8"));
}

function writeJson(file, value) {
  mkdirSync(dirname(file), { recursive: true });
  writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`, "utf8");
}

function zipDirectory(sourceDir, destinationFile) {
  const files = walkFiles(sourceDir).sort((left, right) => left.localeCompare(right));
  const chunks = [];
  const central = [];
  let offset = 0;

  for (const file of files) {
    const data = readFileSync(file);
    const name = slash(relative(sourceDir, file));
    const nameBuffer = Buffer.from(name, "utf8");
    const stat = statSync(file);
    const { time, date } = dosDateTime(stat.mtime);
    const crc = crc32(data);
    const local = Buffer.alloc(30);
    local.writeUInt32LE(0x04034b50, 0);
    local.writeUInt16LE(20, 4);
    local.writeUInt16LE(0x0800, 6);
    local.writeUInt16LE(0, 8);
    local.writeUInt16LE(time, 10);
    local.writeUInt16LE(date, 12);
    local.writeUInt32LE(crc, 14);
    local.writeUInt32LE(data.length, 18);
    local.writeUInt32LE(data.length, 22);
    local.writeUInt16LE(nameBuffer.length, 26);
    local.writeUInt16LE(0, 28);
    chunks.push(local, nameBuffer, data);

    const header = Buffer.alloc(46);
    header.writeUInt32LE(0x02014b50, 0);
    header.writeUInt16LE(20, 4);
    header.writeUInt16LE(20, 6);
    header.writeUInt16LE(0x0800, 8);
    header.writeUInt16LE(0, 10);
    header.writeUInt16LE(time, 12);
    header.writeUInt16LE(date, 14);
    header.writeUInt32LE(crc, 16);
    header.writeUInt32LE(data.length, 20);
    header.writeUInt32LE(data.length, 24);
    header.writeUInt16LE(nameBuffer.length, 28);
    header.writeUInt16LE(0, 30);
    header.writeUInt16LE(0, 32);
    header.writeUInt16LE(0, 34);
    header.writeUInt16LE(0, 36);
    header.writeUInt32LE(0, 38);
    header.writeUInt32LE(offset, 42);
    central.push(header, nameBuffer);
    offset += local.length + nameBuffer.length + data.length;
  }

  const centralOffset = offset;
  const centralSize = central.reduce((total, chunk) => total + chunk.length, 0);
  const end = Buffer.alloc(22);
  end.writeUInt32LE(0x06054b50, 0);
  end.writeUInt16LE(0, 4);
  end.writeUInt16LE(0, 6);
  end.writeUInt16LE(files.length, 8);
  end.writeUInt16LE(files.length, 10);
  end.writeUInt32LE(centralSize, 12);
  end.writeUInt32LE(centralOffset, 16);
  end.writeUInt16LE(0, 20);

  rmSync(destinationFile, { force: true });
  writeFileSync(destinationFile, Buffer.concat([...chunks, ...central, end]));
}

function sha256File(file) {
  return createHash("sha256").update(readFileSync(file)).digest("hex");
}

function formatShanghaiId(date) {
  const parts = new Intl.DateTimeFormat("en-CA", {
    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}`;
}

function getArg(name) {
  const index = process.argv.indexOf(name);
  return index >= 0 ? process.argv[index + 1] : undefined;
}

function slash(value) {
  return value.split(sep).join("/");
}

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

function assertSafeDeployPath(path) {
  const allowedRoot = resolve(deployRoot) + sep;
  const resolved = resolve(path);
  if (!resolved.startsWith(allowedRoot)) {
    throw new Error(`Refusing to modify path outside deploy root: ${resolved}`);
  }
}

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

function escapeAttr(value) {
  return escapeHtml(value);
}

function walkFiles(dir) {
  const files = [];
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
    const fullPath = join(dir, entry.name);
    if (entry.isDirectory()) {
      files.push(...walkFiles(fullPath));
    } else if (entry.isFile()) {
      files.push(fullPath);
    }
  }
  return files;
}

function dosDateTime(date) {
  const year = Math.max(1980, date.getFullYear());
  const month = date.getMonth() + 1;
  const day = date.getDate();
  const hours = date.getHours();
  const minutes = date.getMinutes();
  const seconds = Math.floor(date.getSeconds() / 2);
  return {
    time: (hours << 11) | (minutes << 5) | seconds,
    date: ((year - 1980) << 9) | (month << 5) | day
  };
}

var crcTable;

function crc32(buffer) {
  crcTable ??= createCrcTable();
  let crc = 0xffffffff;
  for (const byte of buffer) {
    crc = crcTable[(crc ^ byte) & 0xff] ^ (crc >>> 8);
  }
  return (crc ^ 0xffffffff) >>> 0;
}

function createCrcTable() {
  const table = new Uint32Array(256);
  for (let index = 0; index < 256; index += 1) {
    let value = index;
    for (let bit = 0; bit < 8; bit += 1) {
      value = (value & 1) ? (0xedb88320 ^ (value >>> 1)) : (value >>> 1);
    }
    table[index] = value >>> 0;
  }
  return table;
}
