#!/usr/bin/env node
// Local operator surface for release observability (issue #41).
// Consumes only sanitized evidence files or a loopback drill fixture.
// Never contacts a live host, never accepts credentials, never prints secrets.
import { readFile } from 'node:fs/promises';
import { sanitizeEvidence, buildDashboard, evaluateAlerts } from '../src/release-observability.js';

function argValue(args, flag) {
  const index = args.indexOf(flag);
  if (index === -1) return null;
  return args[index + 1] || null;
}

// Terminal safety: strip C0/C1 control characters (newline excepted) from any
// dynamically produced text before it reaches stdout/stderr, so hostile file
// paths or fixture payloads can never smuggle ANSI escapes into a terminal.
function controlSafe(text) {
  return String(text)
    .replace(/[\u0000-\u0009\u000B-\u001F\u007F-\u009F]/g, '');
}

async function loadEvidence(source) {
  if (source.drillOrigin) {
    const response = await fetch(`${source.drillOrigin.replace(/\/$/, '')}/api/drill/checks`, { signal: AbortSignal.timeout(5_000) });
    if (!response.ok) throw new Error(`drill fixture returned ${response.status}`);
    const body = await response.json();
    return {
      schemaVersion: 1,
      releaseTag: controlSafe('daily-2026-08-22.1'),
      commit: 'ca31e6d38bec649407f63880504554c59f2878ae',
      generatedAtUtc: new Date().toISOString().replace(/\.\d{3}Z$/, 'Z'),
      checks: Array.isArray(body.checks) ? body.checks : [],
    };
  }
  const raw = JSON.parse(await readFile(source.evidencePath, 'utf8'));
  return raw;
}

function renderDashboard(dashboard, alerts) {
  const lines = [];
  lines.push('RELEASE OBSERVABILITY DASHBOARD');
  lines.push(`release ${dashboard.identity.releaseTag} · commit ${dashboard.identity.commit.slice(0, 12)} · evidence ${dashboard.identity.generatedAtUtc}`);
  lines.push('');
  for (const boundary of ['app', 'api', 'queue', 'model']) {
    const stats = dashboard.boundaries[boundary];
    const latency = stats.latencyMs == null ? '' : ` · max latency ${stats.latencyMs}ms`;
    const counterBits = Object.keys(stats.counters).sort()
      .map(key => `${key}=${stats.counters[key]}`)
      .join(' ');
    const counterText = counterBits ? ` · ${counterBits}` : '';
    lines.push(`${boundary.padEnd(6)}${stats.pass}/${stats.checks} pass · fail ${stats.fail} · degraded ${stats.degraded}${latency}${counterText}`);
  }
  lines.push('');
  const failureEntries = Object.entries(dashboard.failureClasses);
  if (failureEntries.length === 0) {
    lines.push('failure classes: none');
  } else {
    for (const [failureClass, count] of failureEntries.sort((a, b) => b[1] - a[1])) {
      lines.push(`failure class ${failureClass}: ${count}`);
    }
  }
  lines.push('');
  lines.push(`alerts (${alerts.length})`);
  for (const alert of alerts) {
    lines.push(`[${alert.severity.toUpperCase()}] ${alert.id} — owner ${alert.owner}, observed ${alert.count} >= threshold ${alert.threshold}`);
    lines.push(`  ${alert.message}`);
    lines.push(`  runbook: ${alert.runbook}`);
  }
  if (alerts.length === 0) lines.push('no alert conditions met');
  return lines.join('\n');
}

async function main() {
  const args = process.argv.slice(2);

  if (args.includes('--self-check')) {
    console.log('release-observability module self-check: exports OK');
    return 0;
  }

  const drillOrigin = argValue(args, '--drill-origin');
  const evidencePath = argValue(args, '--evidence');
  if (Boolean(drillOrigin) === Boolean(evidencePath)) {
    console.error('usage: node scripts/release_dashboard.mjs (--evidence <sanitized-evidence.json> | --drill-origin <http://127.0.0.1:port>)');
    return 2;
  }

  // Gate every network path before any fetch can happen: a drill may only
  // ever address a validated bare loopback origin.
  let validatedOrigin = null;
  if (drillOrigin) {
    try {
      validatedOrigin = validateLoopbackOrigin(drillOrigin);
    } catch (error) {
      console.error(error.message);
      return 2;
    }
  }

  let rawEvidence;
  try {
    rawEvidence = await loadEvidence({ drillOrigin: validatedOrigin, evidencePath });
  } catch (error) {
    console.error(`release_dashboard: cannot read sanitized evidence (${controlSafe(error.message)}). No contents are echoed.`);
    return 2;
  }

  const validated = sanitizeEvidence(rawEvidence);
  if (!validated.ok) {
    console.error('release_dashboard: evidence failed sanitization (schema mismatch, forbidden fields, or unbounded values). Nothing was rendered.');
    return 2;
  }

  const dashboard = buildDashboard(validated.evidence);
  const alerts = evaluateAlerts(dashboard);
  console.log(renderDashboard(dashboard, alerts));
  console.log('');
  console.log(`manual fallback: ${dashboard.manualFallback.available ? 'available' : 'unavailable'} — ${fallbackReason(dashboard.manualFallback.reason)}`);

  if (drillOrigin) return runDrill({ drillOrigin: validatedOrigin });
  return alerts.some(alert => alert.severity === 'page') ? 1 : 0;
}

function fallbackReason(reason) {
  if (reason === 'local-journal') return 'local journal remains usable';
  if (reason === 'app-down') return 'app boundary is down; local journal cannot be served';
  return 'no degradation detected; fallback not required';
}

// A drill may only ever talk to a throwaway fixture on 127.0.0.1. The origin
// must be a bare http URL whose host is exactly the loopback address: no
// credentials, no DNS names, no alternative IP encodings, no IPv6, no path,
// query, or fragment, and no scheme other than http.
const LOOPBACK_ORIGIN_PATTERN = /^http:\/\/127\.0\.0\.1:[0-9]{1,5}$/;

function validateLoopbackOrigin(rawOrigin) {
  if (typeof rawOrigin !== 'string') {
    throw new Error('drill origin rejected: expected an http://127.0.0.1:<port> URL');
  }
  // Grammar gate runs on the RAW string first: the WHATWG URL parser
  // canonicalizes hostile host encodings (hex octets, decimal IP integers,
  // percent-encoded dots) into a clean-looking 127.0.0.1, so a normalized
  // string can never be trusted on its own.
  if (!LOOPBACK_ORIGIN_PATTERN.test(rawOrigin)) {
    throw new Error('drill origin rejected: only a bare http://127.0.0.1:<port> loopback origin is permitted');
  }
  let parsed;
  try {
    parsed = new URL(rawOrigin);
  } catch {
    throw new Error('drill origin rejected: not a valid URL');
  }
  // Round-trip defense in depth: the parsed form must be byte-identical to
  // what was supplied, proving no hidden credentials, path, query, fragment,
  // or alternative host encoding was smuggled past the grammar.
  if (parsed.toString().replace(/\/$/, '') !== rawOrigin) {
    throw new Error('drill origin rejected: URL did not round-trip as a bare loopback origin');
  }
  if (parsed.protocol !== 'http:' || parsed.hostname !== '127.0.0.1') {
    throw new Error('drill origin rejected: host must be exactly http://127.0.0.1');
  }
  const port = Number(parsed.port);
  if (!Number.isInteger(port) || port < 1 || port > 65535) {
    throw new Error('drill origin rejected: port must be between 1 and 65535');
  }
  return rawOrigin;
}

async function runDrill({ drillOrigin }) {
  console.log('');
  console.log('WORKER OUTAGE DRILL (simulated fixture on loopback; no live host is contacted)');
  let origin;
  try {
    origin = validateLoopbackOrigin(drillOrigin);
  } catch (error) {
    console.error(error.message);
    return 2;
  }

  // Genuine baseline: read the fixture's own current state. A drill may only
  // start from proven health — never from an assumed or already-outaged state.
  const preRaw = await loadEvidence({ drillOrigin });
  const preValidated = sanitizeEvidence(preRaw);
  if (!preValidated.ok) {
    console.error('release_dashboard: pre-drill fixture evidence failed sanitization. Nothing was rendered.');
    return 2;
  }
  const preAlerts = evaluateAlerts(buildDashboard(preValidated.evidence));
  const prePages = preAlerts.filter(alert => alert.severity === 'page');
  if (prePages.length > 0) {
    console.error(`release_dashboard: fixture is not healthy before the drill (${prePages.length} page alert[s]); refusing to flip an already-down worker.`);
    return 2;
  }
  console.log('pre-drill: healthy (0 page alerts)');

  try {
    const response = await fetch(`${origin}/drill/outage`, { method: 'POST', signal: AbortSignal.timeout(5_000) });
    if (!response.ok) throw new Error(`fixture returned ${response.status}`);
  } catch (error) {
    console.error(`release_dashboard: drill switch failed (${error.message})`);
    return 2;
  }

  const postRaw = await loadEvidence({ drillOrigin });
  const postValidated = sanitizeEvidence(postRaw);
  if (!postValidated.ok) {
    console.error('release_dashboard: post-outage evidence failed sanitization.');
    return 2;
  }
  const postDashboard = buildDashboard(postValidated.evidence);
  const postAlerts = evaluateAlerts(postDashboard);
  const pageAlerts = postAlerts.filter(alert => alert.severity === 'page');

  if (pageAlerts.length === 0) {
    console.log('no outage observed after flip: the fixture never transitioned from healthy to outage.');
    console.log('DRILL FAIL');
    return 1;
  }
  console.log(`post-outage: exactly 1 alert expected, found ${pageAlerts.length}`);
  for (const alert of pageAlerts) {
    console.log(`  ${alert.severity.toUpperCase()} ${alert.message.replace(/^PAGE /, '')}`);
    console.log(`  owner ${alert.owner} · threshold ${alert.threshold} · runbook ${alert.runbook}`);
  }
  console.log(`manual fallback: ${postDashboard.manualFallback.available ? 'AVAILABLE' : 'UNAVAILABLE'} — ${fallbackReason(postDashboard.manualFallback.reason)}`);
  const pass = pageAlerts.length === 1
    && pageAlerts[0].id === 'worker.outage'
    && postDashboard.manualFallback.available === true;
  console.log(pass ? 'DRILL PASS' : 'DRILL FAIL');
  return pass ? 0 : 1;
}

process.exitCode = await main();
