// sysutil/release-check.ts
// Static first-user release integrity checks for Nodarama Verbatim.
// Focus: onboarding gates, account/token controls, first project creation, modal exits, hidden dev UI, and visible button hooks.

import fs from "node:fs";
import path from "node:path";

const ROOT = path.resolve(__dirname, "..");

type Severity = "PASS" | "WARN" | "FAIL";
type Check = { severity: Severity; label: string; detail?: string };

const checks: Check[] = [];

function read(rel: string): string {
  return fs.readFileSync(path.join(ROOT, rel), "utf8");
}

function exists(rel: string): boolean {
  return fs.existsSync(path.join(ROOT, rel));
}

function stripHtmlComments(text: string): string {
  return text.replace(/<!--([\s\S]*?)-->/g, "");
}

function add(severity: Severity, label: string, detail = ""): void {
  checks.push({ severity, label, detail });
}

function pass(label: string, detail = ""): void { add("PASS", label, detail); }
function warn(label: string, detail = ""): void { add("WARN", label, detail); }
function fail(label: string, detail = ""): void { add("FAIL", label, detail); }

function requireFile(rel: string): boolean {
  if (!exists(rel)) {
    fail(`Required file exists: ${rel}`);
    return false;
  }
  pass(`Required file exists: ${rel}`);
  return true;
}

function literalPatterns(id: string): RegExp[] {
  const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
  return [
    new RegExp(`on\\(\\s*['\"]${escaped}['\"]\\s*,`),
    new RegExp(`byId\\(\\s*['\"]${escaped}['\"]\\s*\\)\\?\\.addEventListener`),
    new RegExp(`byId\\(\\s*['\"]${escaped}['\"]\\s*\\)\\.addEventListener`),
    new RegExp(`getElementById\\(\\s*['\"]${escaped}['\"]\\s*\\)\\?\\.addEventListener`),
    new RegExp(`getElementById\\(\\s*['\"]${escaped}['\"]\\s*\\)\\.addEventListener`),
    new RegExp(`#${escaped}[^\n]{0,160}addEventListener`),
    new RegExp(`addEventListener[^\n]{0,180}['\"]${escaped}['\"]`)
  ];
}

function sourceHasHook(source: string, id: string): boolean {
  return literalPatterns(id).some(pattern => pattern.test(source));
}

function sourceMentions(source: string, value: string): boolean {
  return source.includes(value);
}

function buttonTagById(html: string, id: string): string {
  const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
  const idPattern = new RegExp(`\\bid=[\"']${escaped}[\"']`, "i");
  const tags = html.match(/<button\b[\s\S]*?>/gi) || [];
  return tags.find(tag => idPattern.test(tag)) || "";
}
function dataAttrDatasetName(attr: string): string {
  return attr.replace(/^data-/, "").replace(/-([a-z])/g, (_m, ch) => String(ch || "").toUpperCase());
}

function buttonHasSelectorHook(html: string, source: string, id: string): boolean {
  const tag = buttonTagById(html, id);
  if (!tag) return false;
  const attrs = [...tag.matchAll(/\s(data-[a-z0-9-]+)(?:=[\"'][^\"']*[\"'])?/gi)].map(match => String(match[1] || "").toLowerCase());
  return attrs.some(attr => source.includes(`[${attr}]`) || source.includes(`[${attr}=`) || source.includes(`.dataset.${dataAttrDatasetName(attr)}`));
}

function collectButtonIds(html: string): string[] {
  const ids = new Set<string>();
  const buttonRe = /<button\b[\s\S]*?\bid=["']([^"']+)["'][\s\S]*?>/gi;
  let match: RegExpExecArray | null;
  while ((match = buttonRe.exec(html))) ids.add(match[1]);
  return [...ids].sort();
}

function collectLoadedScripts(monitorHtml: string): string[] {
  const scripts = new Set<string>();
  const re = /<script\b[^>]*\bsrc=["']\.\/([^"']+)["'][^>]*><\/script>/gi;
  let match: RegExpExecArray | null;
  while ((match = re.exec(monitorHtml))) scripts.add(match[1]);
  return [...scripts];
}

function checkCriticalButton(html: string, jsSource: string, id: string, purpose: string): void {
  if (!html.includes(`id="${id}"`) && !html.includes(`id='${id}'`)) {
    fail(`Critical control exists: ${id}`, purpose);
    return;
  }
  if (!sourceHasHook(jsSource, id)) {
    fail(`Critical control is hooked: ${id}`, purpose);
    return;
  }
  pass(`Critical control is hooked: ${id}`, purpose);
}

function checkVisibleButtonHooks(html: string, jsSource: string, allowUnhooked: Set<string>): void {
  const buttonIds = collectButtonIds(html);
  let missing = 0;
  for (const id of buttonIds) {
    if (allowUnhooked.has(id)) continue;
    if (sourceHasHook(jsSource, id) || sourceMentions(jsSource, id) || buttonHasSelectorHook(html, jsSource, id)) continue;
    warn(`Visible button has no obvious hook: ${id}`, "Static scan could not find a JS reference. Confirm manually or add an allow-list reason.");
    missing++;
  }
  if (!missing) pass("Visible button hook scan", `${buttonIds.length} button IDs scanned.`);
}

function main(): void {
  const requiredFiles = [
    "monitor.html",
    "monitorTabs.html",
    "monitorLaunchGate.js",
    "monitorProject.js",
    "monitor.js",
    "projectOutline.js",
    "projectSwitch.js",
    "projectSwitch.css",
    "editor.js",
    "sysMenu.js",
    "dubUndo.js",
    "projects/system_messages.json",
    "package.json"
  ];

  const allRequiredPresent = requiredFiles.map(requireFile).every(Boolean);
  if (!allRequiredPresent) {
    printReport();
    process.exitCode = 1;
    return;
  }

  const monitorHtmlRaw = read("monitor.html");
  const monitorTabsRaw = read("monitorTabs.html");
  const monitorHtml = stripHtmlComments(monitorHtmlRaw);
  const monitorTabs = stripHtmlComments(monitorTabsRaw);
  const combinedHtml = `${monitorHtml}\n${monitorTabs}`;

  const loadedScripts = collectLoadedScripts(monitorHtml);
  const jsFiles = loadedScripts.filter(file => exists(file));
  const inlineSource = [monitorHtmlRaw, monitorTabsRaw]
    .map(source => [...source.matchAll(/<script\b[^>]*>([\s\S]*?)<\/script>/gi)].map(match => match[1] || "").join("\n"))
    .join("\n");
  const jsSource = [
    ...jsFiles.map(file => `\n/* ${file} */\n${read(file)}`),
    `\n/* inline monitor scripts */\n${inlineSource}`
  ].join("\n");
  const launchGate = read("monitorLaunchGate.js");
  const pkg = JSON.parse(read("package.json"));

  if (loadedScripts.includes("visualizer.js")) fail("Release UI does not load visualizer.js", "Visualizer should stay hidden until release-ready.");
  else pass("Release UI does not load visualizer.js");

  if (/data-tab=["']visualizer["']/.test(monitorHtml)) fail("Release navigation hides Visualizer tab", "Uncommented Visualizer tab found.");
  else pass("Release navigation hides Visualizer tab");

  if (exists("visualizer.js") && !read("sysutil/export-system-release.ts").includes('"visualizer.js"')) warn("Root visualizer.js exists", "It is not loaded, but release export may still include root files unless excluded.");
  else pass("Root visualizer.js is disconnected from release export");

  if (!pkg.scripts?.["release:check"]) warn("package.json has release:check script", "This check should be runnable with npm run release:check.");
  else pass("package.json has release:check script");

  const criticalControls: Array<[string, string]> = [
    ["launch-settings-btn", "New user can reach Settings from the launch gate."],
    ["launch-auth-signin-btn", "Launch sign-in form cannot freeze behind an inert submit button."],
    ["launch-account-settings-btn", "Create-account path opens the Nodarama account window."],
    ["launch-create-open-btn", "Project select screen can open inline creation."],
    ["launch-create-cancel-btn", "Inline project creation can be cancelled."],
    ["launch-create-project-inline-btn", "Inline project creation submit is wired."],
    ["account-auth-signin-btn", "Settings sign-in button is wired."],
    ["account-save-signin-btn", "Settings save-and-sign-in button is wired."],
    ["account-open-window-btn", "Settings create-account button is wired."],
    ["account-refresh-btn", "Settings account refresh button is wired."],
    ["project-create-btn", "Main header project creation button is wired."]
  ];

  for (const [id, purpose] of criticalControls) checkCriticalButton(combinedHtml, jsSource, id, purpose);

  const expectedApiPaths = [
    ["/status", "Launch status can be fetched."],
    ["/projects", "Project registry can be fetched."],
    ["/projects/create", "Project creation endpoint is used."],
    ["/projects/select", "Project selection fallback endpoint is used."],
    ["/auth/portal-return", "Portal token return endpoint is used."],
    ["AuthSession?.login", "Manual sign-in uses AuthSession login."]
  ];

  for (const [needle, label] of expectedApiPaths) {
    if (jsSource.includes(needle)) pass(`First-user flow dependency present: ${needle}`, label);
    else fail(`First-user flow dependency present: ${needle}`, label);
  }

  if (/async function enforceAgreementGate\s*\([^)]*\)\s*{\s*return false;\s*}/.test(launchGate)) {
    pass("Agreement gate is intentionally disabled", "Account creation, token return, and billing are owned by the Nodarama website portal.");
  } else {
    warn("Agreement gate has active logic", "Confirm any active agreement gate cannot trap first-run users.");
  }

  if (/function OpenSignup\s*\([^)]*\)\s*{\s*return false;\s*}/.test(launchGate)) {
    warn("Legacy OpenSignup stub remains", "Not necessarily active, but remove before stable if no longer used.");
  }

  if (/if \(!signedIn\)\s*{\s*enterLocalLaunchMode\('signed_out'\);\s*return;\s*}/.test(launchGate)) {
    pass("Signed-out startup uses soft account gate", "New users may enter the home screen and use the visible sign-in/create-account controls.");
  }

  if (/if \(!projects\.length\)\s*{\s*enterLocalLaunchMode\('no_projects_available'\);\s*return;\s*}/.test(launchGate)) {
    pass("No-project startup uses home/project-card creation", "The hard first-project gate is intentionally removed.");
  }

  if (combinedHtml.includes("launch-project-required")) fail("Hard first-project gate is removed", "launch-project-required should not ship.");
  else pass("Hard first-project gate is removed");

  if (!jsSource.includes("launchCreateProject")) fail("First-project creation owner exists", "launchCreateProject() missing.");
  else pass("First-project creation owner exists");

  const allowUnhooked = new Set([
    // Dynamically wired or intentionally passive/display-only controls can be added here with a reason in a future pass.
  ]);
  checkVisibleButtonHooks(combinedHtml, jsSource, allowUnhooked);

  printReport();
  const failCount = checks.filter(item => item.severity === "FAIL").length;
  process.exitCode = failCount ? 1 : 0;
}

function printReport(): void {
  const counts = {
    PASS: checks.filter(item => item.severity === "PASS").length,
    WARN: checks.filter(item => item.severity === "WARN").length,
    FAIL: checks.filter(item => item.severity === "FAIL").length
  };

  console.log("\nNodarama Verbatim release check");
  console.log(`PASS ${counts.PASS} · WARN ${counts.WARN} · FAIL ${counts.FAIL}\n`);

  for (const severity of ["FAIL", "WARN", "PASS"] as const) {
    const group = checks.filter(item => item.severity === severity);
    if (!group.length) continue;
    console.log(`${severity}`);
    for (const item of group) {
      console.log(`- ${item.label}${item.detail ? ` — ${item.detail}` : ""}`);
    }
    console.log("");
  }
}

main();
