#!/usr/bin/env node
import { execFile } from "node:child_process";
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import vm from "node:vm";
import { promisify } from "node:util";

const execFileAsync = promisify(execFile);
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");

const requiredFiles = [
  "Raceworks.js",
  "server.js",
  "public/control.html",
  "public/preflight.html",
  "public/program.html",
  "public/top-banner.html",
  "public/side-banner.html",
  "public/board.html",
  "public/current-leaders.html",
  "public/results.html",
  "public/top3-podium.html",
  "public/weather.html",
  "public/ticker.html",
  "public/story-spotlight.html",
  "public/app.js",
  "public/control.js",
  "public/styles.css",
  "public/styles-1-foundation.css",
  "public/styles-2-control-theme.css",
  "public/styles-3-graphics-podium.css",
  "public/styles-4-top3-cockpit.css",
  "public/styles-5-control-v2.css",
  "public/fonts/Teko-SemiBold.ttf",
  "public/fonts/Teko-Bold.ttf",
  "public/fonts/BarlowCondensed-SemiBold.ttf",
  "public/fonts/BarlowCondensed-Bold.ttf",
  "public/fonts/BarlowCondensed-BlackItalic.ttf",
  "public/fonts/AirstrikeBold3D.otf",
  "public/fonts/Barber-Complete.woff",
  "public/fonts/BuiltTitling-Bold.otf",
  "public/fonts/BuiltTitling-BoldItalic.otf",
  "src/raceMonitor.js",
  "src/myRacePass.js",
  "scripts/pre-race-check.mjs"
];

const httpChecks = [
  "/api/health",
  "/api/state",
  "/api/program",
  "/api/style",
  "/api/top-banner-output",
  "/api/story-output",
  "/api/network",
  "/control.html",
  "/preflight.html",
  "/program.html",
  "/top-banner.html",
  "/side-banner.html",
  "/weather.html",
  "/ticker.html",
  "/story-spotlight.html",
  "/top3-podium.html",
  "/board.html?mode=lineup",
  "/board.html?mode=results",
  "/current-leaders.html",
  "/results.html"
];

const eventStreamChecks = [
  "/events",
  "/program-events",
  "/top-banner-events",
  "/story-output-events"
];

const failures = [];

function reportFailure(message) {
  failures.push(message);
}

async function fileExists(relativePath) {
  try {
    await fs.access(path.join(root, relativePath));
  } catch (_err) {
    reportFailure(`Missing required file: ${relativePath}`);
  }
}

async function walk(dir) {
  const output = [];
  let entries = [];
  try {
    entries = await fs.readdir(dir, { withFileTypes: true });
  } catch (_err) {
    return output;
  }
  for (const entry of entries) {
    const absolute = path.join(dir, entry.name);
    if (entry.isDirectory()) {
      if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
      output.push(...await walk(absolute));
    } else {
      output.push(absolute);
    }
  }
  return output;
}

async function checkJavaScriptSyntax() {
  const files = [
    path.join(root, "Raceworks.js"),
    path.join(root, "server.js"),
    ...(await walk(path.join(root, "src"))).filter(file => file.endsWith(".js")),
    ...(await walk(path.join(root, "public"))).filter(file => file.endsWith(".js"))
  ];
  for (const file of files) {
    try {
      await execFileAsync(process.execPath, ["--check", file], { cwd: root });
    } catch (err) {
      reportFailure(`JavaScript syntax failed in ${path.relative(root, file)}: ${err.stderr || err.message}`);
    }
  }
}

function scriptTypeIsJavaScript(attributes) {
  const typeMatch = attributes.match(/\btype\s*=\s*["']([^"']+)["']/i);
  if (!typeMatch) return true;
  const type = typeMatch[1].toLowerCase();
  return type === "module"
    || type === "text/javascript"
    || type === "application/javascript"
    || type === "application/ecmascript";
}

async function checkInlineScripts() {
  const htmlFiles = (await walk(path.join(root, "public"))).filter(file => file.endsWith(".html"));
  for (const file of htmlFiles) {
    const html = await fs.readFile(file, "utf8");
    const matches = html.matchAll(/<script\b([^>]*)>([\s\S]*?)<\/script>/gi);
    let index = 0;
    for (const match of matches) {
      const attributes = match[1] || "";
      const source = match[2] || "";
      if (/\bsrc\s*=/.test(attributes) || !scriptTypeIsJavaScript(attributes) || !source.trim()) continue;
      index += 1;
      try {
        new vm.Script(source, { filename: `${path.relative(root, file)}#inline-script-${index}` });
      } catch (err) {
        reportFailure(`Inline script syntax failed in ${path.relative(root, file)}: ${err.message}`);
      }
    }
  }
}

async function checkHttpEndpoint(baseUrl, endpoint) {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), 5000);
  try {
    const response = await fetch(`${baseUrl}${endpoint}`, { signal: controller.signal });
    if (!response.ok) {
      reportFailure(`HTTP ${response.status} from ${endpoint}`);
      return;
    }
    if (endpoint.startsWith("/api/")) {
      await response.json();
    } else {
      const html = await response.text();
      if (!html.trim()) reportFailure(`Empty page response from ${endpoint}`);
    }
  } catch (err) {
    reportFailure(`Could not reach ${endpoint}: ${err.message}`);
  } finally {
    clearTimeout(timeout);
  }
}

async function checkEventStream(baseUrl, endpoint) {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), 5000);
  let reader;
  try {
    const response = await fetch(`${baseUrl}${endpoint}`, {
      signal: controller.signal,
      headers: { accept: "text/event-stream" }
    });
    if (!response.ok) {
      reportFailure(`HTTP ${response.status} from ${endpoint}`);
      return;
    }
    const contentType = response.headers.get("content-type") || "";
    if (!contentType.includes("text/event-stream")) {
      reportFailure(`Unexpected content type from ${endpoint}: ${contentType || "none"}`);
      return;
    }
    reader = response.body?.getReader();
    if (!reader) {
      reportFailure(`No readable stream from ${endpoint}`);
      return;
    }
    const { value } = await reader.read();
    const text = new TextDecoder().decode(value || new Uint8Array());
    if (!text.includes("data:")) {
      reportFailure(`No initial event data from ${endpoint}`);
    }
  } catch (err) {
    reportFailure(`Could not read ${endpoint}: ${err.message}`);
  } finally {
    if (reader) {
      try {
        await reader.cancel();
      } catch (_err) {}
    }
    clearTimeout(timeout);
  }
}

async function postJson(baseUrl, endpoint, payload) {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), 5000);
  try {
    const response = await fetch(`${baseUrl}${endpoint}`, {
      method: "POST",
      signal: controller.signal,
      headers: { "content-type": "application/json" },
      body: JSON.stringify(payload)
    });
    let data = {};
    try {
      data = await response.json();
    } catch (_err) {}
    if (!response.ok || data.ok === false) {
      reportFailure(`POST ${endpoint} failed: ${data.error || response.status}`);
    }
    return data;
  } catch (err) {
    reportFailure(`Could not POST ${endpoint}: ${err.message}`);
    return {};
  } finally {
    clearTimeout(timeout);
  }
}

async function checkPostRoundTrips(baseUrl) {
  let style = {};
  let program = {};
  let topBanner = {};
  try {
    style = await (await fetch(`${baseUrl}/api/style`)).json();
    program = await (await fetch(`${baseUrl}/api/program`)).json();
    topBanner = await (await fetch(`${baseUrl}/api/top-banner-output`)).json();
  } catch (err) {
    reportFailure(`Could not prepare POST round-trip checks: ${err.message}`);
    return;
  }
  await postJson(baseUrl, "/api/style", style);
  await postJson(baseUrl, "/api/program", { graphic: program.graphic || "blank" });
  await postJson(baseUrl, "/api/top-banner-output", { visible: topBanner.visible !== false });
}

async function checkHttp() {
  const base = (process.env.SMOKE_BASE || "").replace(/\/+$/, "");
  if (!base) {
    console.log("Skipped live HTTP checks. Set SMOKE_BASE=http://127.0.0.1:4000 to include them.");
    return;
  }
  for (const endpoint of httpChecks) {
    await checkHttpEndpoint(base, endpoint);
  }
  for (const endpoint of eventStreamChecks) {
    await checkEventStream(base, endpoint);
  }
  await checkPostRoundTrips(base);
}

for (const relativePath of requiredFiles) {
  await fileExists(relativePath);
}
await checkJavaScriptSyntax();
await checkInlineScripts();
await checkHttp();

if (failures.length) {
  console.error("Smoke check failed:");
  for (const failure of failures) console.error(`- ${failure}`);
  process.exit(1);
}

console.log("Smoke check passed.");
