import { execFileSync } from "node:child_process";
import {
  existsSync,
  mkdirSync,
  readdirSync,
  readFileSync,
  rmSync,
  statSync,
  writeFileSync
} from "node:fs";
import { dirname, extname, 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 deployRoot = join(webRoot, "test-results", "deploy");
const zipPath = resolve(process.argv[2] ?? latestZipPath());
const extractRoot = join(deployRoot, "_verify", zipBasename(zipPath));

assertSafeVerifyPath(extractRoot);
rmSync(extractRoot, { recursive: true, force: true });
mkdirSync(extractRoot, { recursive: true });
expandZip(zipPath, extractRoot);

const manifest = readJson(join(extractRoot, "demo-manifest.json"));
const report = readJson(join(extractRoot, manifest.report_json));
const programManifest = readJson(join(extractRoot, manifest.program_manifest));
const grlFiles = walkFiles(join(extractRoot, "spec-programs")).filter((file) => extname(file).toLowerCase() === ".grl");

const requiredFiles = [
  "index.html",
  "demo-manifest.json",
  "README.md",
  "app/virtual-controller.html",
  "app/virtual-controller.css",
  "app/virtual-controller.js",
  "spec-programs/manifest.json",
  manifest.job_json,
  manifest.report_json,
  manifest.report_html,
  manifest.screenshots.desktop,
  manifest.screenshots.mobile,
  manifest.screenshots.evidence,
  "docs/GRL功能语法逻辑与程序创建使用手册.docx",
  "docs/通用机器人离线编程系统测试文档.docx",
  "docs/working2/README.md",
  "docs/working2/05-验收证据.md"
];

const checks = [];
for (const file of requiredFiles) {
  checks.push(checkFile(file));
}

checks.push(check("program_count", manifest.program_count === 19, `program_count=${manifest.program_count}`));
checks.push(check("manifest_programs", programManifest.programs.length === 19, `manifest programs=${programManifest.programs.length}`));
checks.push(check("grl_files", grlFiles.length === 19, `grl files=${grlFiles.length}`));
checks.push(check("summary_fail", report.summary.fail === 0, `summary.fail=${report.summary.fail}`));
checks.push(check("report_programs", report.summary.programs === 19, `report programs=${report.summary.programs}`));
checks.push(check("runtime_programs", manifest.runtime_programs === 12, `runtime=${manifest.runtime_programs}`));
checks.push(check("static_programs", manifest.static_programs === 4, `static=${manifest.static_programs}`));
checks.push(check("post_programs", manifest.post_programs === 3, `post=${manifest.post_programs}`));
checks.push(check("missing_sections", Array.isArray(report.coverage.missingSections) && report.coverage.missingSections.length === 0, `missing=${report.coverage.missingSections?.join(",") ?? "n/a"}`));
checks.push(check("relative_urls", allManifestUrlsAreRelative(manifest), "manifest URLs are relative"));

for (const program of manifest.programs) {
  checks.push(checkFile(program.source));
  checks.push(checkFile(program.artifacts.compile));
}

for (const doc of manifest.docs ?? []) {
  checks.push(checkFile(doc.href));
}

const runtimeFramePrograms = manifest.programs.filter((program) => program.coverage_level === "Runtime");
let frameProgramCount = 0;
for (const program of runtimeFramePrograms) {
  const trajectory = readJson(join(extractRoot, program.artifacts.trajectory));
  const frames = Array.isArray(trajectory.frames) ? trajectory.frames : [];
  const hasFrameState = frames.some((frame) =>
    Array.isArray(frame.joints) &&
    frame.joints.length === 6 &&
    Array.isArray(frame.tcp?.position) &&
    frame.tcp.position.length === 3 &&
    Array.isArray(frame.tcp?.quaternion) &&
    frame.tcp.quaternion.length === 4
  );
  if (hasFrameState) frameProgramCount += 1;
}
checks.push(check(
  "runtime_trajectory_frames",
  frameProgramCount > 0,
  `${frameProgramCount}/${runtimeFramePrograms.length} runtime programs include frame joints and TCP`
));

const failed = checks.filter((item) => !item.ok);
const summary = {
  zip: zipPath,
  extract_root: extractRoot,
  release_id: manifest.release_id,
  job_id: manifest.job_id,
  program_count: manifest.program_count,
  grl_files: grlFiles.length,
  summary_fail: report.summary.fail,
  checks,
  ok: failed.length === 0
};

writeFileSync(join(deployRoot, `${zipBasename(zipPath)}.verify.json`), `${JSON.stringify(summary, null, 2)}\n`, "utf8");
console.log(JSON.stringify(summary, null, 2));

if (failed.length > 0) {
  throw new Error(`Demo package verification failed: ${failed.map((item) => item.name).join(", ")}`);
}

function checkFile(relativePath) {
  const file = join(extractRoot, relativePath);
  const exists = existsSync(file);
  return check(`file:${relativePath}`, exists && statSync(file).isFile(), exists ? `${statSync(file).size} bytes` : "missing");
}

function check(name, ok, details) {
  return { name, ok: Boolean(ok), details };
}

function allManifestUrlsAreRelative(value) {
  const urls = [];
  collectUrls(value, urls);
  return urls.every((url) => !/^[a-z]+:\/\//i.test(url) && !/^[a-z]:[\\/]/i.test(url) && !url.startsWith("/") && !url.includes("\\"));
}

function collectUrls(value, urls) {
  if (Array.isArray(value)) {
    value.forEach((item) => collectUrls(item, urls));
    return;
  }
  if (!value || typeof value !== "object") return;
  for (const [key, item] of Object.entries(value)) {
    if (typeof item === "string" && (key === "href" || key.includes("json") || key.includes("html") || key.includes("entry") || key.includes("manifest") || key.includes("source") || key.includes("screenshot") || key.includes("report") || key.includes("compile") || key.includes("controller") || key.includes("trace") || key.includes("trajectory") || key.includes("roundtrip") || key.includes("io") || key.includes("queue"))) {
      urls.push(item);
    } else {
      collectUrls(item, urls);
    }
  }
}

function expandZip(source, destination) {
  const script = [
    "Add-Type -AssemblyName System.IO.Compression.FileSystem",
    `$src = ${psSingleQuoted(source)}`,
    `$dst = ${psSingleQuoted(destination)}`,
    "if (Test-Path -LiteralPath $dst) { Remove-Item -LiteralPath $dst -Recurse -Force }",
    "New-Item -ItemType Directory -Force -Path $dst | Out-Null",
    "[System.IO.Compression.ZipFile]::ExtractToDirectory($src, $dst)"
  ].join("\n");
  execFileSync("powershell", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script], {
    stdio: "inherit"
  });
}

function latestZipPath() {
  if (!existsSync(deployRoot)) {
    throw new Error(`No deploy output directory found: ${deployRoot}`);
  }
  const zips = readdirSync(deployRoot, { withFileTypes: true })
    .filter((entry) => entry.isFile() && entry.name.startsWith("kdl-olp-demo-") && entry.name.endsWith(".zip"))
    .map((entry) => ({
      path: join(deployRoot, entry.name),
      mtimeMs: statSync(join(deployRoot, entry.name)).mtimeMs
    }))
    .sort((left, right) => right.mtimeMs - left.mtimeMs);
  if (zips.length === 0) {
    throw new Error(`No kdl-olp-demo-*.zip files found in ${deployRoot}`);
  }
  return zips[0].path;
}

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 readJson(file) {
  return JSON.parse(readFileSync(file, "utf8"));
}

function zipBasename(file) {
  return file.split(/[\\/]/).at(-1).replace(/\.zip$/i, "");
}

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

function assertSafeVerifyPath(path) {
  const allowedRoot = resolve(join(deployRoot, "_verify")) + sep;
  const resolved = resolve(path);
  if (!resolved.startsWith(allowedRoot)) {
    throw new Error(`Refusing to modify path outside verify root: ${resolved}`);
  }
}
