#!/usr/bin/env node

/**
 * Thin Node orchestrator for ADR 0006 GPU promotion scaffolding.
 *
 * Manifest construction / gap reports: tools/moui/gpu_promotion_scaffold
 * Validation: tools/moui/validate_gpu_promotion_manifest
 * This script only spawns smokes and calls those tools.
 *
 * Never flips NativeGpuPlatform::gpu_promoted.
 */

import { spawnSync } from "node:child_process";
import { join } from "node:path";
import {
  PLATFORMS,
  ensureDir,
  interpretMacosGpuSmokeLog,
  normalizeRepoPath,
  readOptionalText,
  repoRoot,
  runGpuPromotionScaffold,
  validateGpuPromotionManifest,
  validateGpuWorkerNoReadback,
} from "./lib/gpu-promotion-manifest.mjs";
import { writeFileSync } from "node:fs";

const usage = `Usage: scripts/record-gpu-promotion-smoke.mjs --platform <id> [options]

Platforms: ${PLATFORMS.join("|")}

Options:
  --mode scaffold|short-smoke|performance|full
                         scaffold (default): MoonBit pending manifest + gap report.
                         short-smoke: macOS Metal short path then scaffold ingest.
                         performance: run macOS GPU present-interval harness
                         (scripts/run-macos-gpu-performance-smoke.mjs) and merge metrics.
                         full: write full-plan.json + empty metrics skeleton,
                         merge via --metrics-json if measurements exist later,
                         then exit 3 until remaining gates land.
  --out-dir PATH         Default artifacts/gpu-promotion/<platform>/<timestamp>
  --duration-seconds N   Requested full-run duration (default 600).
  --metrics-json PATH    Optional measured metrics overlay for scaffold merge.
  --require-passed       Validate with --require-passed (expected fail until gates).
  --skip-static-guard    Skip validate-gpu-worker-no-readback.mjs
  --dry-run              Print plan only.
  -h, --help             Show this help.
`;

const parseArgs = argv => {
  const options = {
    platform: "",
    mode: "scaffold",
    outDir: "",
    durationSeconds: 600,
    metricsJson: "",
    requirePassed: false,
    skipStaticGuard: false,
    dryRun: false,
    help: false,
  };
  for (let index = 0; index < argv.length; ) {
    const arg = argv[index];
    if (arg === "--platform") {
      options.platform = (argv[index + 1] || "").toLowerCase();
      index += 2;
    } else if (arg === "--mode") {
      options.mode = (argv[index + 1] || "").toLowerCase();
      index += 2;
    } else if (arg === "--out-dir") {
      options.outDir = argv[index + 1] || "";
      index += 2;
    } else if (arg === "--duration-seconds") {
      options.durationSeconds = Number.parseInt(argv[index + 1] || "", 10);
      index += 2;
    } else if (arg === "--metrics-json") {
      options.metricsJson = argv[index + 1] || "";
      index += 2;
    } else if (arg === "--require-passed") {
      options.requirePassed = true;
      index += 1;
    } else if (arg === "--skip-static-guard") {
      options.skipStaticGuard = true;
      index += 1;
    } else if (arg === "--dry-run") {
      options.dryRun = true;
      index += 1;
    } else if (arg === "-h" || arg === "--help") {
      options.help = true;
      index += 1;
    } else {
      throw new Error(`unknown argument: ${arg}`);
    }
  }
  return options;
};

const fullPlan = ({ platform, durationSeconds, outDir }) => ({
  schemaVersion: 1,
  status: "not-implemented",
  platform,
  durationSecondsRequested: durationSeconds,
  outDir,
  phases: [
    {
      id: "short-path",
      description: "Metal/Vulkan/D3D/EGL GPU present markers (short smoke)",
      status: platform === "macos" ? "available-via-short-smoke" : "pending",
    },
    {
      id: "performance",
      description: "≥ durationSeconds frame telemetry p95/drop%/input latency",
      status: "missing-runner",
    },
    {
      id: "memory-lifecycle",
      description: "≥ 100 surface recreation + ≥ 100 fg/bg; bounded memory",
      status: "missing-runner",
    },
    {
      id: "mailbox-stress",
      description: "Frame flood with non-droppable lifecycle controls",
      status: "missing-runner",
    },
    {
      id: "context-loss",
      description: "Forced loss recover ≤ 3 VSync; preserve AppRuntime",
      status: "missing-runner",
    },
    {
      id: "raster-fallback",
      description: "Automatic fallback after repeated recovery failure",
      status: "missing-runner",
    },
  ],
  metricsJsonTemplate: {
    notes: "Fill from matching-host full harness; never hand-claim promotion.",
    diagnostics: {
      readbackCount: 0,
      workerThread: false,
      surfaceGeneration: 0,
      contextGeneration: 0,
      recoveryCount: 0,
      fallbackCount: 0,
    },
    promotionGates: {
      readbackEliminated: false,
      rendererThread: false,
      mailboxOk: false,
      performance: {
        refreshHz: 60,
        durationSeconds: 0,
        p95FrameMs: 0,
        droppedFramePercent: 100,
        inputToPresentPVsyncIntervals: 0,
      },
      memory: {
        bounded: false,
        surfaceRecreationCycles: 0,
        foregroundBackgroundCycles: 0,
      },
      contextLoss: {
        recoveredWithinVsyncs: 0,
        rasterFallbackPreservedAppRuntime: false,
      },
      rasterFallback: {
        automaticAfterRepeatedFailure: false,
      },
    },
  },
});

const timestamp = () => {
  const d = new Date();
  const pad = n => String(n).padStart(2, "0");
  return [
    d.getUTCFullYear(),
    pad(d.getUTCMonth() + 1),
    pad(d.getUTCDate()),
    "T",
    pad(d.getUTCHours()),
    pad(d.getUTCMinutes()),
    pad(d.getUTCSeconds()),
    "Z",
  ].join("");
};

const run = (cmd, args) =>
  spawnSync(cmd, args, {
    cwd: repoRoot,
    env: process.env,
    encoding: "utf8",
  });

const main = () => {
  let options;
  try {
    options = parseArgs(process.argv.slice(2));
  } catch (error) {
    console.error(String(error.message || error));
    console.error(usage);
    process.exit(2);
  }

  if (options.help) {
    process.stdout.write(usage);
    process.exit(0);
  }

  if (!options.platform || !PLATFORMS.includes(options.platform)) {
    console.error(`--platform must be one of: ${PLATFORMS.join(", ")}`);
    process.exit(2);
  }
  if (!["scaffold", "short-smoke", "performance", "full"].includes(options.mode)) {
    console.error("--mode must be scaffold, short-smoke, performance, or full");
    process.exit(2);
  }
  if (
    (options.mode === "short-smoke" || options.mode === "performance") &&
    options.platform !== "macos"
  ) {
    console.error(`${options.mode} is currently implemented for --platform macos only`);
    process.exit(2);
  }
  if (!Number.isFinite(options.durationSeconds) || options.durationSeconds <= 0) {
    console.error("--duration-seconds must be a positive integer");
    process.exit(2);
  }

  const outDir = normalizeRepoPath(
    options.outDir ||
      join("artifacts", "gpu-promotion", options.platform, timestamp()),
  );
  const relativeArtifact = outDir.startsWith(repoRoot)
    ? outDir.slice(repoRoot.length + 1)
    : outDir;
  const smokeLogPath = join(outDir, "macos-gpu-short-smoke.log");
  const recorderLogPath = join(outDir, "recorder.log");
  const manifestPath = join(outDir, "gpu-promotion-manifest.json");

  const plan = {
    platform: options.platform,
    mode: options.mode,
    outDir,
    tool: "tools/moui/gpu_promotion_scaffold",
    gpuPromoted: false,
  };

  if (options.dryRun) {
    process.stdout.write(`${JSON.stringify(plan, null, 2)}\n`);
    process.exit(0);
  }

  ensureDir(outDir);
  const logLines = [
    `platform=${options.platform}`,
    `mode=${options.mode}`,
    `outDir=${outDir}`,
    `started=${new Date().toISOString()}`,
  ];

  if (!options.skipStaticGuard) {
    const staticGuard = validateGpuWorkerNoReadback();
    logLines.push(`staticGuard.status=${staticGuard.status}`);
    if (staticGuard.status !== 0) {
      writeFileSync(recorderLogPath, `${logLines.join("\n")}\n`);
      console.error("validate-gpu-worker-no-readback failed; refusing to record");
      process.stderr.write(staticGuard.stdout + staticGuard.stderr);
      process.exit(staticGuard.status || 1);
    }
  }

  let shortSmoke = null;
  if (options.mode === "short-smoke") {
    const smoke = run("bash", [
      "scripts/macos-skia-renderer-smoke.sh",
      "--run-gpu-smoke",
      "--run-showcase-smoke",
      "--smoke-log",
      smokeLogPath,
      "--showcase-log",
      join(outDir, "macos-showcase-first-frame.log"),
    ]);
    const combined = [
      smoke.stdout || "",
      smoke.stderr || "",
      readOptionalText(smokeLogPath),
      readOptionalText(join(outDir, "macos-showcase-first-frame.log")),
    ].join("\n");
    writeFileSync(smokeLogPath, combined.endsWith("\n") ? combined : `${combined}\n`);
    shortSmoke = interpretMacosGpuSmokeLog(combined);
    logLines.push(
      `shortSmoke.exit=${smoke.status ?? "error"}`,
      `shortSmoke.shortPathOk=${shortSmoke.shortPathOk}`,
    );
  }

  let metricsJsonPath = options.metricsJson
    ? normalizeRepoPath(options.metricsJson)
    : "";
  if (options.mode === "performance") {
    const perf = run(process.execPath, [
      "scripts/run-macos-gpu-performance-smoke.mjs",
      "--duration-ms",
      String(Math.max(1000, options.durationSeconds * 1000)),
      "--out-dir",
      outDir,
      "--skip-scaffold",
      ...(options.skipStaticGuard ? ["--skip-static-guard"] : []),
    ]);
    logLines.push(`performance.exit=${perf.status ?? "error"}`);
    writeFileSync(
      join(outDir, "performance-runner.log"),
      `${perf.stdout || ""}\n${perf.stderr || ""}\n`,
    );
    metricsJsonPath = join(outDir, "metrics.json");
    if (perf.status !== 0) {
      writeFileSync(recorderLogPath, `${logLines.join("\n")}\n`);
      console.error("performance harness failed");
      process.stderr.write(perf.stdout + perf.stderr);
      process.exit(perf.status || 1);
    }
  }
  if (options.mode === "full") {
    const planPath = join(outDir, "full-plan.json");
    const metricsTemplatePath = join(outDir, "metrics-template.json");
    const plan = fullPlan({
      platform: options.platform,
      durationSeconds: options.durationSeconds,
      outDir: relativeArtifact,
    });
    writeFileSync(planPath, `${JSON.stringify(plan, null, 2)}\n`);
    writeFileSync(
      metricsTemplatePath,
      `${JSON.stringify(plan.metricsJsonTemplate, null, 2)}\n`,
    );
    logLines.push(
      "full mode harness is not implemented yet",
      `wrote ${planPath}`,
      `wrote ${metricsTemplatePath}`,
    );
    if (!metricsJsonPath) {
      metricsJsonPath = metricsTemplatePath;
    }
  }

  const scaffoldMode =
    options.mode === "performance" ? "full" : options.mode;
  const scaffold = runGpuPromotionScaffold({
    platform: options.platform,
    outDir,
    mode: scaffoldMode,
    smokeLog:
      options.mode === "short-smoke"
        ? smokeLogPath
        : options.mode === "performance"
          ? join(outDir, "macos-gpu-performance-smoke.log")
          : "",
    metricsJson: metricsJsonPath,
    runner:
      options.mode === "short-smoke"
        ? "scripts/record-gpu-promotion-smoke.mjs --mode short-smoke"
        : options.mode === "performance"
          ? "scripts/record-gpu-promotion-smoke.mjs --mode performance"
          : options.mode === "full"
            ? "scripts/record-gpu-promotion-smoke.mjs --mode full"
            : "scripts/record-gpu-promotion-smoke.mjs --mode scaffold",
    artifact: relativeArtifact,
    durationSeconds: options.durationSeconds,
    exitOnFailure: false,
  });
  logLines.push(`scaffold.status=${scaffold.status ?? "error"}`);
  if (scaffold.status !== 0) {
    writeFileSync(recorderLogPath, `${logLines.join("\n")}\n`);
    console.error("gpu_promotion_scaffold failed");
    process.exit(scaffold.status || 1);
  }

  const validation = validateGpuPromotionManifest(manifestPath, {
    requirePassed: options.requirePassed,
  });
  logLines.push(
    `validate.status=${validation.status}`,
    validation.stdout.trim(),
    validation.stderr.trim(),
    `finished=${new Date().toISOString()}`,
  );
  writeFileSync(recorderLogPath, `${logLines.filter(Boolean).join("\n")}\n`);

  process.stdout.write(
    [
      `wrote ${manifestPath}`,
      `wrote ${join(outDir, "gap-report.md")}`,
      `wrote ${join(outDir, "gate-inventory.json")}`,
      `gpuPromoted=false gatesComplete=false`,
      `validate_exit=${validation.status}`,
    ].join("\n") + "\n",
  );

  if (options.requirePassed && validation.status !== 0) {
    process.stderr.write(
      "manifest does not satisfy --require-passed (expected until full gates land)\n",
    );
    process.exit(validation.status || 1);
  }
  if (!options.requirePassed && validation.status !== 0) {
    process.stderr.write("schema validation failed\n");
    process.stderr.write(validation.stdout + validation.stderr);
    process.exit(validation.status || 1);
  }
  if (options.mode === "full") {
    process.stderr.write(
      "full GPU promotion harness still missing memory/context-loss/mailbox phases; scaffold written only\n",
    );
    process.exit(3);
  }
  if (options.mode === "short-smoke" && shortSmoke && !shortSmoke.shortPathOk) {
    process.stderr.write(
      "short-smoke completed but Metal GPU short-path markers were not observed\n",
    );
    process.exit(1);
  }
  process.exit(0);
};

main();