#!/usr/bin/env node
// gate-parse.mjs — static JS parse gate for the panel front-ends.
//
// This codifies the 2026-05-08 blank-page deploy gate: a single syntax error
// anywhere in the giant inline <script> block of new.html/index.html renders
// the whole panel as a blank page, and `go build` cannot catch it because the
// HTML is embedded as opaque bytes (//go:embed). This script extracts every
// <script> block from the served HTML files and parses each one.
//
// Zero dependencies: each block is written to a temp file and checked with
// `node --check` (falls back to acorn if a local install of it exists).
//
// Usage:  node gate-parse.mjs
// Exit 0 = every script block in every file parses. Non-zero = at least one
// syntax error (printed with file, block index, and node's error output).

import { readFileSync, writeFileSync, mkdtempSync, rmSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
import { tmpdir } from 'node:os';
import { join, dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

const here = dirname(fileURLToPath(import.meta.url));
const staticDir = resolve(here, '../cmd/controller/static');

// new.html + index.html are the two panel UIs the root route can serve;
// login.html is the unauthenticated landing page. All three brick on a
// syntax error, so all three are gated.
const files = ['new.html', 'index.html', 'login.html'];

// Extract inline <script> blocks. Blocks with a src= attribute have no inline
// body worth parsing; blocks with a non-JS type (application/json templates
// etc.) are skipped.
function extractScripts(html) {
  const out = [];
  const re = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi;
  let m;
  while ((m = re.exec(html)) !== null) {
    const attrs = m[1];
    const body = m[2];
    // External local bundles (e.g. /rcc.js?v=hash) are part of the deploy:
    // resolve them from static/ and gate their contents too.
    const srcMatch = attrs.match(/\bsrc\s*=\s*["']?([^"'\s>]+)/i);
    if (srcMatch) {
      const src = srcMatch[1];
      if (/^(https?:)?\/\//i.test(src)) continue; // never hotlink; skip if ever present
      const local = src.replace(/^\//, '').split('?')[0];
      try {
        const extBody = readFileSync(join(staticDir, local), 'utf8');
        const line = html.slice(0, m.index).split('\n').length;
        out.push({ body: extBody, line, module: /\btype\s*=\s*["']?module/i.test(attrs), external: local });
      } catch (e) {
        out.push({ body: `throw new SyntaxError(${JSON.stringify(`missing external script ${local}: ${e.message}`)});(`, line: 0, module: false, external: local });
      }
      continue;
    }
    const typeMatch = attrs.match(/\btype\s*=\s*["']?([^"'\s>]+)/i);
    const type = typeMatch ? typeMatch[1].toLowerCase() : 'text/javascript';
    if (!['text/javascript', 'application/javascript', 'module'].includes(type)) continue;
    // Line offset of the block start, for error reporting.
    const line = html.slice(0, m.index).split('\n').length;
    out.push({ body, line, module: type === 'module' });
  }
  return out;
}

// Prefer acorn if it happens to be installed next to this suite; otherwise
// use `node --check` on a temp file (always available, needs no npm install).
let acorn = null;
try { acorn = await import('acorn'); } catch { /* fine — node --check path */ }

const tmp = mkdtempSync(join(tmpdir(), 'panel-gate-'));
let failures = 0;
let checkedFiles = 0;

for (const name of files) {
  let html;
  try {
    html = readFileSync(join(staticDir, name), 'utf8');
  } catch (e) {
    console.error(`FAIL  ${name}: cannot read (${e.message})`);
    failures++;
    continue;
  }
  checkedFiles++;
  const blocks = extractScripts(html);
  let fileOk = true;
  blocks.forEach((b, i) => {
    const label = `${name} script-block ${i + 1}/${blocks.length}${b.external ? ` [external ${b.external}]` : ''} (starts html line ${b.line}, ${b.body.length} chars)`;
    try {
      if (acorn) {
        acorn.parse(b.body, {
          ecmaVersion: 'latest',
          sourceType: b.module ? 'module' : 'script',
        });
      } else {
        const tf = join(tmp, `${name.replace(/\W/g, '_')}_${i}${b.module ? '.mjs' : '.js'}`);
        writeFileSync(tf, b.body);
        execFileSync(process.execPath, ['--check', tf], { stdio: ['ignore', 'pipe', 'pipe'] });
      }
      console.log(`PASS  ${label}`);
    } catch (e) {
      fileOk = false;
      failures++;
      const detail = (e.stderr ? e.stderr.toString() : e.message).trim();
      console.error(`FAIL  ${label}\n${detail.split('\n').map(l => '      ' + l).join('\n')}`);
    }
  });
  if (blocks.length === 0) {
    // A panel HTML with zero script blocks is itself a broken deploy.
    console.error(`FAIL  ${name}: 0 inline script blocks found — extraction or file is broken`);
    failures++;
    fileOk = false;
  }
  console.log(`${fileOk ? 'OK   ' : 'BAD  '} ${name}: ${blocks.length} inline script block(s)`);
}

rmSync(tmp, { recursive: true, force: true });

if (failures > 0) {
  console.error(`\ngate-parse: ${failures} failure(s) across ${checkedFiles} file(s) — DO NOT DEPLOY`);
  process.exit(1);
}
console.log(`\ngate-parse: all script blocks parse (${checkedFiles} files, parser: ${acorn ? 'acorn' : 'node --check'})`);
