#!/usr/bin/env node

import { createHash, randomBytes } from 'node:crypto';
import dgram from 'node:dgram';
import fs from 'node:fs';
import {
  access,
  copyFile,
  lstat,
  mkdir,
  readFile,
  rename,
  rm,
  symlink,
  writeFile,
} from 'node:fs/promises';
import path from 'node:path';
import process from 'node:process';
import { spawn } from 'node:child_process';
import { once } from 'node:events';
import { pathToFileURL } from 'node:url';

import { MULTIPLAYER_MAPS } from '../public/js/menu-config.js';
import { RETAIL_FILE_NAMES, validateRetailManifest } from '../server/retail.js';

export const PROFILE_SCHEMA = 1;
export const PROFILE_POLICY = 'openq4-fs-debug-working-set-v1';
export const PERFORMANCE_PRESET = 'balanced';
export const MAX_MACHINE_SPEC = 2;
export const MAX_CAPTURE_JOBS = 2;
export const MAX_CAPTURE_ATTEMPTS = 3;
export const CAPTURE_SERVER_READY_MARKER = 'net_LANServer is enabled. Not sending heartbeats';
export const REQUIRED_COVERAGE_SCENARIOS = Object.freeze([
  'fresh-process-per-map',
  'balanced-machine-spec-2',
  'all-player-models',
  'all-mp-weapons-items-projectiles',
  'dm-tourney-team-ctf-gui-audio',
]);

const root = path.resolve(import.meta.dirname, '..');
const activeProfileDirectory = path.join(root, 'runtime/retail-bundle-profiles');
const defaultCaptureRoot = path.join(root, 'runtime/retail-bundle-profile-captures');
const approvedRetailNames = new Set(RETAIL_FILE_NAMES);
const successfulRetailRead = /idFileSystem::OpenFileRead: (.+?) \(found in '([^']+)'\)/g;
const loggedPk4 = /(?:Loaded pk4 (.+?\.pk4) with checksum|found in '([^']+\.pk4)')/gm;
const fatalRuntimePattern = /fatal signal|segmentation fault|double free|terminate called|stack smashing|assertion .* failed/i;
const safeModePattern = /^[A-Za-z][A-Za-z ]{0,31}$/;
const safeSupplementName = /^[A-Za-z0-9][A-Za-z0-9._-]*\.(?:assets|log)$/;
const MAX_LOG_BYTES = 64 * 1024 * 1024;
const CAPTURE_SETTLE_POLL_MS = 100;
const CAPTURE_RETRY_DELAY_MS = 500;

function usage() {
  return `Usage: node tools/capture-retail-bundle-profiles.mjs [options]

Options:
  --maps <id,id,...>       Capture selected catalog maps (default: all)
  --jobs <1-${MAX_CAPTURE_JOBS}>              Parallel map captures (default: 1)
  --output <directory>     New, atomically published profile directory
  --port-base <port>       First private capture UDP port (default: 27700)
  --timeout-ms <ms>        Timeout for each load/snapshot marker (default: 240000)
  --settle-ms <ms>         Gameplay collection time per mode (default: 10000)
  --retail-dir <directory> Exact 23-file q4base directory
  --retail-manifest <file> Exact retail manifest
  --native-runtime <dir>   Native OpenQ4 runtime
  --common-profile <file>  Version-bound common profile manifest
  --supplement <map=file>  Add an explicit .log/.assets trace to a selected map
                           (repeatable; files are copied and SHA-256 recorded)
  --dry-run                Print the capture plan without touching the filesystem
  --help                   Show this help

The output path must not exist and must not overlap runtime/retail-bundle-profiles.
On failure the hidden sibling staging directory is retained without manifest.json.
Runtime child crashes are retried up to ${MAX_CAPTURE_ATTEMPTS} times with isolated evidence.
`;
}

function integerOption(raw, name, minimum, maximum) {
  if (!/^\d+$/.test(String(raw || ''))) throw new Error(`${name} must be an integer`);
  const value = Number(raw);
  if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
    throw new Error(`${name} must be between ${minimum} and ${maximum}`);
  }
  return value;
}

function defaultOutput(now = new Date(), pid = process.pid) {
  const timestamp = now.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z');
  return path.join(defaultCaptureRoot, `q4profiles-${timestamp}-${pid}`);
}

function pathsOverlap(left, right) {
  const relative = path.relative(left, right);
  if (relative === '') return true;
  if (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative)) return true;
  const inverse = path.relative(right, left);
  return !inverse.startsWith(`..${path.sep}`) && inverse !== '..' && !path.isAbsolute(inverse);
}

function compareCodeUnits(left, right) {
  return left < right ? -1 : left > right ? 1 : 0;
}

export function parseCaptureArgs(argv, { cwd = process.cwd(), now = new Date(), pid = process.pid } = {}) {
  const values = {
    maps: [],
    jobs: 1,
    output: defaultOutput(now, pid),
    portBase: 27700,
    timeoutMs: 240_000,
    settleMs: 10_000,
    retailDir: path.join(root, 'assets-src/gog/q4base'),
    retailManifest: path.join(root, 'runtime/retail-manifest.json'),
    nativeRuntime: path.join(root, 'runtime/native'),
    commonProfile: path.join(activeProfileDirectory, 'manifest.json'),
    supplements: [],
    dryRun: false,
    help: false,
  };

  const resolveArgument = (value) => path.resolve(cwd, value);
  for (let index = 0; index < argv.length; index += 1) {
    const argument = argv[index];
    const next = () => {
      const value = argv[++index];
      if (!value || value.startsWith('--')) throw new Error(`${argument} requires a value`);
      return value;
    };
    if (argument === '--maps') values.maps.push(...next().split(',').map((entry) => entry.trim()));
    else if (argument === '--jobs') values.jobs = integerOption(next(), '--jobs', 1, MAX_CAPTURE_JOBS);
    else if (argument === '--output') values.output = resolveArgument(next());
    else if (argument === '--port-base') values.portBase = integerOption(next(), '--port-base', 1024, 65535);
    else if (argument === '--timeout-ms') values.timeoutMs = integerOption(next(), '--timeout-ms', 30_000, 900_000);
    else if (argument === '--settle-ms') values.settleMs = integerOption(next(), '--settle-ms', 0, 120_000);
    else if (argument === '--retail-dir') values.retailDir = resolveArgument(next());
    else if (argument === '--retail-manifest') values.retailManifest = resolveArgument(next());
    else if (argument === '--native-runtime') values.nativeRuntime = resolveArgument(next());
    else if (argument === '--common-profile') values.commonProfile = resolveArgument(next());
    else if (argument === '--supplement') {
      const raw = next();
      const separator = raw.indexOf('=');
      const map = separator > 0 ? raw.slice(0, separator) : '';
      const fileName = separator > 0 ? raw.slice(separator + 1) : '';
      if (!/^mp\/[a-z0-9_-]+$/.test(map) || !fileName) {
        throw new Error('--supplement must use canonical map-id=file syntax');
      }
      if (fileName.replaceAll('\\', '/').split('/').includes('..')) {
        throw new Error('--supplement path must not contain parent traversal');
      }
      const file = resolveArgument(fileName);
      const name = path.basename(file);
      if (!safeSupplementName.test(name)) {
        throw new Error('--supplement accepts only safely named .log or .assets files');
      }
      values.supplements.push(Object.freeze({ map, file, name }));
    }
    else if (argument === '--dry-run') values.dryRun = true;
    else if (argument === '--help') values.help = true;
    else throw new Error(`unknown argument: ${argument}`);
  }

  if (values.maps.some((entry) => !entry)) throw new Error('--maps contains an empty map id');
  values.maps = [...new Set(values.maps)];
  const supplementTargets = new Set();
  for (const supplement of values.supplements) {
    const target = `${supplement.map}\0${supplement.name.toLowerCase()}`;
    if (supplementTargets.has(target)) {
      throw new Error(`duplicate supplement target for ${supplement.map}: ${supplement.name}`);
    }
    supplementTargets.add(target);
  }
  values.supplements = Object.freeze([...values.supplements]);
  values.output = path.resolve(values.output);
  if (pathsOverlap(values.output, activeProfileDirectory)) {
    throw new Error(`capture output must not overlap the active profile directory: ${activeProfileDirectory}`);
  }
  return Object.freeze(values);
}

export function buildCapturePlan(options, catalog = MULTIPLAYER_MAPS) {
  const byId = new Map(catalog.map((map) => [map.id, map]));
  if (byId.size !== catalog.length) throw new Error('multiplayer map catalog contains duplicate ids');
  const requested = options.maps?.length ? new Set(options.maps) : new Set(byId.keys());
  for (const id of requested) {
    if (!byId.has(id)) throw new Error(`unknown multiplayer map: ${id}`);
  }
  for (const supplement of options.supplements || []) {
    if (!byId.has(supplement.map)) throw new Error(`unknown supplement map: ${supplement.map}`);
    if (!requested.has(supplement.map)) {
      throw new Error(`supplement map is outside the capture plan: ${supplement.map}`);
    }
  }
  const maps = catalog.filter(({ id }) => requested.has(id)).map((map, index) => {
    if (!/^mp\/[a-z0-9_-]+$/.test(map.id) || !Array.isArray(map.modes) || map.modes.length === 0 ||
        map.modes.some((mode) => !safeModePattern.test(mode))) {
      throw new Error(`invalid capture contract in multiplayer map catalog: ${map.id}`);
    }
    const port = options.portBase + index;
    if (port > 65535 || port === 28004 || (port >= 28040 && port <= 28047)) {
      throw new Error(`capture UDP port is invalid or reserved: ${port}`);
    }
    return Object.freeze({
      id: map.id,
      slug: map.id.replaceAll('/', '_'),
      modes: Object.freeze([...map.modes]),
      content: map.content,
      retailGeometry: map.content === 'retail',
      botReady: map.botReady === true,
      port,
    });
  });
  if (maps.length !== requested.size) throw new Error('capture plan did not preserve the requested map set');
  return Object.freeze({
    jobs: options.jobs,
    allCatalogMaps: maps.length === catalog.length,
    maps: Object.freeze(maps),
    supplements: Object.freeze([...(options.supplements || [])].sort((left, right) =>
      compareCodeUnits(left.map, right.map) ||
      compareCodeUnits(left.name, right.name) ||
      compareCodeUnits(left.file, right.file))),
  });
}

export function buildServerArguments({ sourceRoot, cdRoot, saveRoot, map, mode, port }) {
  return [
    '+set', 'fs_basepath', sourceRoot,
    '+set', 'fs_cdpath', cdRoot,
    '+set', 'fs_savepath', saveRoot,
    '+set', 'fs_game', 'baseoq4',
    '+set', 'fs_caseSensitiveOS', '0',
    '+set', 's_noSound', '1',
    '+set', 'net_ip', '127.0.0.1',
    '+set', 'net_port', String(port),
    '+set', 'net_LANServer', '1',
    '+set', 'si_pure', '0',
    '+set', 'net_serverAllowDataChecksumMismatch', '1',
    '+set', 'net_serverDemo', '0',
    '+set', 'si_name', `Q4 bundle capture ${map.slug}`,
    '+set', 'si_maxPlayers', '12',
    '+set', 'si_warmup', '0',
    '+set', 'bot_minplayers', map.botReady ? '4' : '0',
    '+set', 'si_gameType', mode,
    '+set', 'si_map', map.id,
    '+spawnServer', map.id,
  ];
}

export function buildClientArguments({ sourceRoot, cdRoot, saveRoot, map, port }) {
  return [
    '+set', 'fs_basepath', sourceRoot,
    '+set', 'fs_cdpath', cdRoot,
    '+set', 'fs_savepath', saveRoot,
    '+set', 'fs_game', 'baseoq4',
    '+set', 'fs_caseSensitiveOS', '0',
    '+set', 'fs_validateOfficialPaks', '0',
    '+set', 'fs_debug', '1',
    '+set', 'net_verbose', '1',
    '+set', 'sys_allowMultipleInstances', '1',
    '+set', 'r_fullscreen', '0',
    '+set', 'com_machineSpec', String(MAX_MACHINE_SPEC),
    '+set', 'net_LANServer', '1',
    '+set', 'ui_autoJoin', '1',
    '+set', 'ui_name', `BundleCapture_${map.slug}`,
    '+set', 'ui_model', 'model_player_marine',
    '+applyPerformancePreset', PERFORMANCE_PRESET,
    '+connect', `127.0.0.1:${port}`,
  ];
}

export function buildModeCapturePlans(map) {
  const slugs = new Set();
  return Object.freeze(map.modes.map((mode) => {
    const slug = mode.toLowerCase().replaceAll(' ', '_');
    if (!/^[a-z][a-z0-9_]*$/.test(slug) || slugs.has(slug)) {
      throw new Error(`invalid or duplicate capture mode slug for ${map.id}: ${mode}`);
    }
    slugs.add(slug);
    return Object.freeze({
      mode,
      slug,
      runDirectory: path.posix.join('runs', map.slug, slug),
      trace: path.posix.join('maps', `${map.slug}__${slug}.log`),
      serverEvidence: path.posix.join('evidence', `${map.slug}__${slug}.server.log`),
    });
  }));
}

export async function waitForCaptureServerReady(server, map, offset, timeoutMs) {
  await server.waitFor(`msec to load ${map.id}`, offset, timeoutMs);
  await server.waitFor(CAPTURE_SERVER_READY_MARKER, offset, timeoutMs);
}

export function modeMarker(map, mode) {
  return `Q4_PROFILE_${map.slug}_${mode.replaceAll(' ', '_').toUpperCase()}_READY`;
}

export function serverSettledMarker(map, mode) {
  return `Q4_PROFILE_${map.slug}_${mode.replaceAll(' ', '_').toUpperCase()}_SERVER_SETTLED`;
}

function collectExactRetailReads(source, map, {
  archiveNames = approvedRetailNames,
  expectedQ4base = null,
} = {}) {
  const expectedArchiveDirectory = expectedQ4base ? path.resolve(expectedQ4base) : null;
  const found = new Set();
  let match;
  loggedPk4.lastIndex = 0;
  while ((match = loggedPk4.exec(source)) !== null) {
    const archivePath = path.resolve((match[1] || match[2]).replaceAll('\\', '/'));
    const archiveDirectory = path.dirname(archivePath);
    const archive = path.basename(archivePath).toLowerCase();
    const q4baseArchive = path.basename(archiveDirectory).toLowerCase() === 'q4base';
    if (expectedArchiveDirectory && q4baseArchive && archiveDirectory !== expectedArchiveDirectory) {
      throw new Error(`${map.id}: trace read a PK4 from an unexpected q4base: ${archivePath}`);
    }
    if (expectedArchiveDirectory && archiveNames.has(archive) && archiveDirectory !== expectedArchiveDirectory) {
      throw new Error(`${map.id}: approved retail PK4 read escaped the expected q4base: ${archivePath}`);
    }
    if (expectedArchiveDirectory && archiveDirectory === expectedArchiveDirectory && !archiveNames.has(archive)) {
      throw new Error(`${map.id}: expected q4base contains an unapproved PK4: ${archivePath}`);
    }
  }
  successfulRetailRead.lastIndex = 0;
  while ((match = successfulRetailRead.exec(source)) !== null) {
    const archivePath = path.resolve(match[2].replaceAll('\\', '/'));
    const archiveDirectory = path.dirname(archivePath);
    const archive = path.basename(archivePath).toLowerCase();
    if (archiveNames.has(archive)) found.add(match[1].toLowerCase());
  }
  return found;
}

export function validateCapturedTrace(source, map, options = {}) {
  if (fatalRuntimePattern.test(source)) throw new Error(`${map.id}: client log contains a fatal runtime marker`);
  const found = collectExactRetailReads(source, map, options);
  if (found.size === 0) throw new Error(`${map.id}: trace has no successful reads from the exact retail catalog`);
  if (map.retailGeometry) {
    for (const extension of ['proc', 'cm', 'map']) {
      const member = `maps/${map.id}.${extension}`;
      if (!found.has(member)) throw new Error(`${map.id}: trace is missing successful ${member} read`);
    }
  }
  const expectedModes = options.expectedModes || map.modes;
  if (!Array.isArray(expectedModes) || expectedModes.length === 0 ||
      expectedModes.some((mode) => !map.modes.includes(mode)) ||
      new Set(expectedModes).size !== expectedModes.length) {
    throw new Error(`${map.id}: trace has an invalid expected mode contract`);
  }
  const expected = new Set(expectedModes);
  for (const mode of map.modes) {
    const present = source.includes(modeMarker(map, mode));
    if (expected.has(mode) && !present) {
      throw new Error(`${map.id}: trace is missing ${mode} completion marker`);
    }
    if (!expected.has(mode) && present) {
      throw new Error(`${map.id}: trace contains unexpected ${mode} completion marker`);
    }
  }
  return Object.freeze({ successfulRetailReads: found.size });
}

export function validateSupplementLog(source, map, options = {}) {
  if (fatalRuntimePattern.test(source)) throw new Error(`${map.id}: supplement log contains a fatal runtime marker`);
  const found = collectExactRetailReads(source, map, options);
  if (found.size === 0) throw new Error(`${map.id}: supplement log has no successful exact retail reads`);
  return Object.freeze({ successfulRetailReads: found.size });
}

export function buildCaptureLaunchPaths(runRoot, nativeRuntime) {
  const runtime = path.resolve(nativeRuntime);
  return Object.freeze({
    cwd: path.resolve(runRoot),
    client: path.join(runtime, 'openQ4-client_x64'),
    dedicated: path.join(runtime, 'openQ4-ded_x64'),
  });
}

async function pathInfo(file) {
  return lstat(file).catch(() => null);
}

async function requireExecutable(file, label) {
  const info = await pathInfo(file);
  if (!info?.isFile()) throw new Error(`${label} is missing or not a regular file: ${file}`);
  await access(file, fs.constants.X_OK);
}

async function sha256RegularFile(file, expectedSize) {
  const before = await lstat(file);
  if (!before.isFile() || before.isSymbolicLink() || before.size !== expectedSize) {
    throw new Error(`exact retail source is not an approved regular file: ${file}`);
  }
  const hash = createHash('sha256');
  const stream = fs.createReadStream(file, { highWaterMark: 1024 * 1024 });
  for await (const chunk of stream) hash.update(chunk);
  const after = await lstat(file);
  if (!after.isFile() || after.isSymbolicLink() || after.dev !== before.dev || after.ino !== before.ino ||
      after.size !== before.size || after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs) {
    throw new Error(`exact retail source changed while it was being hashed: ${file}`);
  }
  return hash.digest('hex');
}

async function verifyExactRetailFiles(options, manifest) {
  for (const record of manifest.files) {
    const file = path.join(options.retailDir, record.name);
    const digest = await sha256RegularFile(file, record.size);
    if (digest !== record.sha256) {
      throw new Error(`exact retail source SHA-256 differs from its manifest: ${record.name}`);
    }
  }
}

async function commandAvailable(command) {
  const search = spawn('sh', ['-c', 'command -v "$1" >/dev/null 2>&1', 'sh', command], {
    stdio: 'ignore',
  });
  const [code] = await once(search, 'exit');
  return code === 0;
}

async function assertUdpPortAvailable(port) {
  await new Promise((resolve, reject) => {
    const socket = dgram.createSocket('udp4');
    socket.once('error', reject);
    socket.bind(port, '127.0.0.1', () => socket.close(resolve));
  }).catch((error) => {
    throw new Error(`capture UDP port ${port} is unavailable: ${error.message}`);
  });
}

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

export class RetryableCaptureError extends Error {
  constructor(message, options = {}) {
    super(message, options);
    this.name = 'RetryableCaptureError';
    this.retryable = true;
  }
}

function exitDetail(exit) {
  if (!exit) return 'still running';
  return exit.error?.message || exit.signal || `exit ${exit.code}`;
}

function captureExitRecord(exit) {
  if (!exit) return null;
  if (exit.error) return Object.freeze({ error: exit.error.message });
  return Object.freeze({ code: exit.code, signal: exit.signal });
}

function assertCaptureProcessAlive(managedProcess) {
  if (!managedProcess) return;
  if (managedProcess.exit) {
    throw new RetryableCaptureError(
      `${managedProcess.label}: process ended during capture settle (${exitDetail(managedProcess.exit)})`);
  }
}

export async function waitForCaptureSettle(server, client, settleMs, {
  signal = null,
  pollMs = CAPTURE_SETTLE_POLL_MS,
  sleepFn = sleep,
  now = Date.now,
} = {}) {
  const deadline = now() + settleMs;
  while (true) {
    if (signal?.aborted) throw signal.reason || new Error('capture aborted');
    assertCaptureProcessAlive(server);
    assertCaptureProcessAlive(client);
    const remaining = deadline - now();
    if (remaining <= 0) break;
    await sleepFn(Math.min(pollMs, remaining));
  }
  assertCaptureProcessAlive(server);
  assertCaptureProcessAlive(client);
}

export async function runCaptureAttempts(attemptFn, {
  maxAttempts = MAX_CAPTURE_ATTEMPTS,
  retryDelayMs = CAPTURE_RETRY_DELAY_MS,
  sleepFn = sleep,
  signal = null,
} = {}) {
  const attempts = [];
  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
    if (signal?.aborted) throw signal.reason || new Error('capture aborted');
    try {
      const result = await attemptFn(attempt);
      if (result?.attemptRecord) attempts.push(result.attemptRecord);
      return Object.freeze({ result, attempts: Object.freeze(attempts) });
    } catch (error) {
      if (error?.attemptRecord) attempts.push(error.attemptRecord);
      if (!(error instanceof RetryableCaptureError) || attempt === maxAttempts || signal?.aborted) {
        error.attempts = Object.freeze(attempts);
        throw error;
      }
      await sleepFn(retryDelayMs);
    }
  }
  throw new Error('capture retry loop ended without a result');
}

export function validateCapturedServerEvidence(source, map, mode, {
  boundaryMarker = serverSettledMarker(map, mode),
  stopResult,
} = {}) {
  if (!source.includes(`msec to load ${map.id}`)) {
    throw new Error(`${map.id}: server evidence is missing the map-load marker`);
  }
  if (!source.includes(CAPTURE_SERVER_READY_MARKER)) {
    throw new Error(`${map.id}: server evidence is missing the LAN-ready marker`);
  }
  const boundaryOffset = source.indexOf(boundaryMarker);
  if (boundaryOffset < 0 || source.indexOf(boundaryMarker, boundaryOffset + boundaryMarker.length) >= 0) {
    throw new Error(`${map.id}: server evidence must contain exactly one ${mode} settle marker`);
  }
  if (!stopResult?.commandSent || !Number.isInteger(stopResult.commandOffset)) {
    throw new RetryableCaptureError(
      `${map.id}: server ended before the explicit post-settle shutdown command`);
  }
  if (boundaryOffset >= stopResult.commandOffset) {
    throw new Error(`${map.id}: server settle marker was not acknowledged before shutdown`);
  }

  const fatalIndexes = [];
  const fatalPattern = new RegExp(fatalRuntimePattern.source, 'ig');
  let fatalMatch;
  while ((fatalMatch = fatalPattern.exec(source)) !== null) fatalIndexes.push(fatalMatch.index);
  if (fatalIndexes.some((index) => index < stopResult.commandOffset)) {
    throw new RetryableCaptureError(`${map.id}: server log contains a pre-shutdown fatal runtime marker`);
  }

  const exit = stopResult.exit;
  const teardownFatal = fatalIndexes.length > 0 &&
    (exit?.code === 139 || exit?.signal === 'SIGSEGV');
  if (fatalIndexes.length > 0 && !teardownFatal) {
    throw new Error(`${map.id}: server log contains an unclassified shutdown fatal marker`);
  }
  if (!exit) throw new Error(`${map.id}: server did not exit after shutdown`);
  if (exit.error) throw new Error(`${map.id}: server process error during shutdown: ${exit.error.message}`);
  if (!teardownFatal && (exit.code !== 0 || exit.signal)) {
    throw new Error(`${map.id}: server shutdown failed (${exitDetail(exit)})`);
  }
  return Object.freeze({
    teardownFatal,
    exit: captureExitRecord(exit),
  });
}

class ManagedCaptureProcess {
  constructor({ command, args, cwd, env, logFile, label, signal }) {
    this.label = label;
    this.output = '';
    this.exit = null;
    this.log = fs.createWriteStream(logFile, { flags: 'wx', mode: 0o600 });
    this.child = spawn(command, args, {
      cwd,
      env,
      detached: true,
      stdio: ['pipe', 'pipe', 'pipe'],
    });
    this.exitPromise = new Promise((resolve) => {
      this.child.once('error', (error) => {
        this.exit = { error };
        resolve(this.exit);
      });
      this.child.once('exit', (code, processSignal) => {
        this.exit = { code, signal: processSignal };
        resolve(this.exit);
      });
    });
    this.closePromise = new Promise((resolve) => this.child.once('close', resolve));
    const consume = (chunk) => {
      const text = chunk.toString('utf8').replaceAll('\r', '');
      this.output += text;
      this.log.write(text);
      if (this.output.length > MAX_LOG_BYTES && !this.exit) {
        this.terminate('SIGKILL');
      }
    };
    this.child.stdout.on('data', consume);
    this.child.stderr.on('data', consume);
    this.abortHandler = () => this.terminate('SIGTERM');
    this.signal = signal;
    signal?.addEventListener('abort', this.abortHandler, { once: true });
  }

  offset() {
    return this.output.length;
  }

  send(command) {
    if (this.exit || !this.child.stdin.writable) throw new Error(`${this.label}: process is not accepting commands`);
    this.child.stdin.write(command.endsWith('\n') ? command : `${command}\n`);
  }

  async waitFor(text, offset, timeoutMs) {
    const deadline = Date.now() + timeoutMs;
    while (Date.now() < deadline) {
      if (this.output.indexOf(text, offset) >= 0) return;
      if (this.output.length > MAX_LOG_BYTES) throw new Error(`${this.label}: log exceeded ${MAX_LOG_BYTES} bytes`);
      if (this.signal?.aborted) throw this.signal.reason || new Error(`${this.label}: capture aborted`);
      if (this.exit) {
        const detail = this.exit.error?.message || this.exit.signal || `exit ${this.exit.code}`;
        throw new Error(`${this.label}: process ended while waiting for ${JSON.stringify(text)} (${detail})`);
      }
      await sleep(100);
    }
    throw new Error(`${this.label}: timed out waiting for ${JSON.stringify(text)} after ${timeoutMs}ms`);
  }

  terminate(signal = 'SIGTERM') {
    if (this.exit || !this.child.pid) return;
    try { process.kill(-this.child.pid, signal); } catch {}
    try { this.child.kill(signal); } catch {}
  }

  async stop({ command = 'quit', timeoutMs = 15_000 } = {}) {
    let commandSent = false;
    let commandOffset = null;
    let forcedSignal = null;
    if (!this.exit && command) {
      commandOffset = this.offset();
      try {
        this.send(command);
        commandSent = true;
      } catch {}
    }
    if (!this.exit) {
      await Promise.race([this.exitPromise, sleep(timeoutMs)]);
    }
    if (!this.exit) {
      forcedSignal = 'SIGTERM';
      this.terminate('SIGTERM');
      await Promise.race([this.exitPromise, sleep(3_000)]);
    }
    if (!this.exit) {
      forcedSignal = 'SIGKILL';
      this.terminate('SIGKILL');
      await Promise.race([this.exitPromise, sleep(2_000)]);
    }
    await Promise.race([this.closePromise, sleep(2_000)]);
    this.signal?.removeEventListener('abort', this.abortHandler);
    this.log.end();
    await once(this.log, 'finish');
    return Object.freeze({
      commandSent,
      commandOffset,
      forcedSignal,
      exit: this.exit,
    });
  }
}

export async function createExactRoots(runRoot, options, retailManifest) {
  const sourceRoot = path.join(runRoot, 'source');
  const q4base = path.join(sourceRoot, 'q4base');
  const cdRoot = path.join(runRoot, 'cd');
  await Promise.all([
    mkdir(q4base, { recursive: true }),
    mkdir(cdRoot, { recursive: true }),
  ]);
  for (const record of retailManifest.files) {
    await symlink(path.join(options.retailDir, record.name), path.join(q4base, record.name));
  }
  const baseoq4 = path.join(options.nativeRuntime, 'baseoq4');
  await Promise.all([
    symlink(baseoq4, path.join(cdRoot, 'baseoq4')),
    symlink(baseoq4, path.join(runRoot, 'baseoq4')),
  ]);
  return { sourceRoot, cdRoot };
}

function attemptLogPaths(stageDirectory, map, modePlan, attempt) {
  const suffix = `attempt-${String(attempt).padStart(2, '0')}`;
  const stem = `${map.slug}__${modePlan.slug}__${suffix}`;
  return Object.freeze({
    clientRelative: path.posix.join('evidence', 'attempts', `${stem}.client.log`),
    serverRelative: path.posix.join('evidence', 'attempts', `${stem}.server.log`),
    clientLog: path.join(stageDirectory, 'evidence', 'attempts', `${stem}.client.log`),
    serverLog: path.join(stageDirectory, 'evidence', 'attempts', `${stem}.server.log`),
  });
}

function conciseCaptureError(error) {
  return String(error?.message || error).split('\n', 1)[0].slice(0, 500);
}

async function stopCaptureProcesses({ server, client, runtimeSettled }) {
  let serverStop = null;
  let clientStop = null;
  let cleanupError = null;
  const stop = async (managedProcess, assign) => {
    if (!managedProcess) return;
    try {
      assign(await managedProcess.stop());
    } catch (error) {
      if (!cleanupError) cleanupError = error;
    }
  };
  if (runtimeSettled) {
    await stop(server, (result) => { serverStop = result; });
    await stop(client, (result) => { clientStop = result; });
  } else {
    await stop(client, (result) => { clientStop = result; });
    await stop(server, (result) => { serverStop = result; });
  }
  return { serverStop, clientStop, cleanupError };
}

function validateCapturedClientExit(map, stopResult) {
  const exit = stopResult?.exit;
  if (!exit) throw new Error(`${map.id}: client did not exit after shutdown`);
  if (exit.error) throw new Error(`${map.id}: client process error during shutdown: ${exit.error.message}`);
  if (exit.code !== 0 || exit.signal) {
    throw new Error(`${map.id}: client shutdown failed (${exitDetail(exit)})`);
  }
  return captureExitRecord(exit);
}

async function captureModeAttempt({
  map,
  modePlan,
  options,
  retailManifest,
  stageDirectory,
  signal,
  attempt,
}) {
  if (signal.aborted) throw signal.reason || new Error('capture aborted');
  await assertUdpPortAvailable(map.port);
  const runRoot = path.join(
    stageDirectory,
    ...modePlan.runDirectory.split('/'),
    `attempt-${String(attempt).padStart(2, '0')}`,
  );
  const serverSave = path.join(runRoot, 'server-save');
  const clientSave = path.join(runRoot, 'client-save');
  const serverHome = path.join(runRoot, 'server-home');
  const clientHome = path.join(runRoot, 'client-home');
  const attemptLogs = attemptLogPaths(stageDirectory, map, modePlan, attempt);
  await Promise.all([
    mkdir(serverSave, { recursive: true }),
    mkdir(clientSave, { recursive: true }),
    mkdir(serverHome, { recursive: true }),
    mkdir(clientHome, { recursive: true }),
    mkdir(path.join(stageDirectory, 'maps'), { recursive: true }),
    mkdir(path.join(stageDirectory, 'evidence'), { recursive: true }),
    mkdir(path.join(stageDirectory, 'evidence', 'attempts'), { recursive: true }),
  ]);
  const roots = await createExactRoots(runRoot, options, retailManifest);
  const launch = buildCaptureLaunchPaths(runRoot, options.nativeRuntime);
  const serverArguments = buildServerArguments({
    ...roots, saveRoot: serverSave, map, mode: modePlan.mode, port: map.port,
  });
  const server = new ManagedCaptureProcess({
    command: 'stdbuf',
    args: ['-oL', '-eL', launch.dedicated, ...serverArguments],
    cwd: launch.cwd,
    env: { ...process.env, HOME: serverHome },
    logFile: attemptLogs.serverLog,
    label: `${map.id} ${modePlan.mode} server`,
    signal,
  });
  let client = null;
  let runtimeSettled = false;
  let primaryError = null;
  let prematureRuntimeFailure = false;
  try {
    await waitForCaptureServerReady(server, map, 0, options.timeoutMs);
    const clientArguments = buildClientArguments({
      ...roots, saveRoot: clientSave, map, port: map.port,
    });
    client = new ManagedCaptureProcess({
      command: 'stdbuf',
      args: [
        '-oL', '-eL', 'xvfb-run', '-a', '-s', '-screen 0 1280x720x24 -nolisten tcp',
        launch.client, ...clientArguments,
      ],
      cwd: launch.cwd,
      env: {
        ...process.env,
        HOME: clientHome,
        SDL_AUDIODRIVER: process.env.SDL_AUDIODRIVER || 'dummy',
        ALSOFT_DRIVERS: process.env.ALSOFT_DRIVERS || 'null',
      },
      logFile: attemptLogs.clientLog,
      label: `${map.id} ${modePlan.mode} client`,
      signal,
    });
    await client.waitFor('--- Common Initialization Complete ---', 0, options.timeoutMs);
    await client.waitFor(`msec to load ${map.id}`, 0, options.timeoutMs);
    await client.waitFor('received first snapshot', 0, options.timeoutMs);
    const marker = modeMarker(map, modePlan.mode);
    client.send(`echo ${marker}`);
    await client.waitFor(marker, 0, 10_000);
    await waitForCaptureSettle(server, client, options.settleMs, { signal });
    const boundary = serverSettledMarker(map, modePlan.mode);
    const boundaryOffset = server.offset();
    server.send(`echo ${boundary}`);
    await server.waitFor(boundary, boundaryOffset, 10_000);
    assertCaptureProcessAlive(server);
    assertCaptureProcessAlive(client);
    runtimeSettled = true;
  } catch (error) {
    primaryError = error;
    prematureRuntimeFailure = error instanceof RetryableCaptureError ||
      Boolean(server.exit) ||
      Boolean(client?.exit) ||
      fatalRuntimePattern.test(server.output) ||
      fatalRuntimePattern.test(client?.output || '');
  }

  const { serverStop, clientStop, cleanupError } = await stopCaptureProcesses({
    server, client, runtimeSettled,
  });
  const serverEvidence = await readFile(attemptLogs.serverLog, 'utf8').catch(() => '');
  const trace = await readFile(attemptLogs.clientLog, 'utf8').catch(() => '');
  if (signal.aborted) throw signal.reason || new Error('capture aborted');
  if (primaryError || cleanupError) {
    const original = primaryError || cleanupError;
    const error = prematureRuntimeFailure && !(original instanceof RetryableCaptureError)
      ? new RetryableCaptureError(
        `${map.id} ${modePlan.mode}: capture process failed before the server settle boundary`,
        { cause: original })
      : original;
    error.attemptRecord = Object.freeze({
      attempt,
      outcome: 'failed',
      retryable: error instanceof RetryableCaptureError,
      error: conciseCaptureError(error),
      clientEvidence: client ? attemptLogs.clientRelative : null,
      serverEvidence: attemptLogs.serverRelative,
      clientExit: captureExitRecord(clientStop?.exit || client?.exit),
      serverExit: captureExitRecord(serverStop?.exit || server.exit),
    });
    throw error;
  }
  if (fatalRuntimePattern.test(trace)) {
    const error = new RetryableCaptureError(`${map.id}: client log contains a fatal runtime marker`);
    error.attemptRecord = Object.freeze({
      attempt,
      outcome: 'failed',
      retryable: true,
      error: conciseCaptureError(error),
      clientEvidence: attemptLogs.clientRelative,
      serverEvidence: attemptLogs.serverRelative,
      clientExit: captureExitRecord(clientStop?.exit),
      serverExit: captureExitRecord(serverStop?.exit),
    });
    throw error;
  }

  let serverValidation;
  try {
    serverValidation = validateCapturedServerEvidence(serverEvidence, map, modePlan.mode, {
      stopResult: serverStop,
    });
  } catch (error) {
    error.attemptRecord = Object.freeze({
      attempt,
      outcome: 'failed',
      retryable: error instanceof RetryableCaptureError,
      error: conciseCaptureError(error),
      clientEvidence: attemptLogs.clientRelative,
      serverEvidence: attemptLogs.serverRelative,
      clientExit: captureExitRecord(clientStop?.exit),
      serverExit: captureExitRecord(serverStop?.exit),
    });
    throw error;
  }
  const clientExit = validateCapturedClientExit(map, clientStop);
  const validation = validateCapturedTrace(trace, map, {
    expectedQ4base: path.join(roots.sourceRoot, 'q4base'),
    expectedModes: [modePlan.mode],
  });
  return {
    attemptLogs,
    trace,
    validation,
    clientExit,
    serverValidation,
  };
}

export async function promoteCaptureAttemptLogs(stageDirectory, modePlan, attemptLogs) {
  const clientTarget = path.join(stageDirectory, ...modePlan.trace.split('/'));
  const serverTarget = path.join(stageDirectory, ...modePlan.serverEvidence.split('/'));
  await rename(attemptLogs.clientLog, clientTarget);
  await rename(attemptLogs.serverLog, serverTarget);
  return Object.freeze({
    trace: modePlan.trace,
    serverEvidence: modePlan.serverEvidence,
  });
}

async function captureMode({ map, modePlan, options, retailManifest, stageDirectory, signal }) {
  const captured = await runCaptureAttempts(async (attempt) => {
    const result = await captureModeAttempt({
      map, modePlan, options, retailManifest, stageDirectory, signal, attempt,
    });
    const promoted = await promoteCaptureAttemptLogs(stageDirectory, modePlan, result.attemptLogs);
    return {
      ...result,
      promoted,
      attemptRecord: Object.freeze({
        attempt,
        outcome: 'completed',
        clientEvidence: promoted.trace,
        serverEvidence: promoted.serverEvidence,
        clientExit: result.clientExit,
        serverExit: result.serverValidation.exit,
        teardownFatal: result.serverValidation.teardownFatal,
      }),
    };
  }, { signal });
  const { result } = captured;
  return Object.freeze({
    mode: modePlan.mode,
    trace: result.promoted.trace,
    serverEvidence: result.promoted.serverEvidence,
    port: map.port,
    successfulRetailReads: result.validation.successfulRetailReads,
    sha256: createHash('sha256').update(result.trace).digest('hex'),
    attempts: captured.attempts,
    teardownFatal: result.serverValidation.teardownFatal,
  });
}

export function aggregateModeCaptures(map, modeCaptures) {
  if (!Array.isArray(modeCaptures) || modeCaptures.length !== map.modes.length) {
    throw new Error(`${map.id}: mode capture count does not match the catalog`);
  }
  const byMode = new Map();
  for (const capture of modeCaptures) {
    if (!capture || !map.modes.includes(capture.mode) || byMode.has(capture.mode) ||
        typeof capture.trace !== 'string' || !capture.trace ||
        typeof capture.serverEvidence !== 'string' || !capture.serverEvidence) {
      throw new Error(`${map.id}: invalid or duplicate mode capture: ${capture?.mode || ''}`);
    }
    byMode.set(capture.mode, capture);
  }
  const ordered = map.modes.map((mode) => {
    const capture = byMode.get(mode);
    if (!capture) throw new Error(`${map.id}: missing completed mode capture: ${mode}`);
    return capture;
  });
  return Object.freeze({
    id: map.id,
    retailGeometry: map.retailGeometry,
    port: map.port,
    modes: Object.freeze([...map.modes]),
    traces: Object.freeze(ordered.map((capture) => capture.trace)),
    successfulRetailReads: ordered.reduce((total, capture) =>
      total + capture.successfulRetailReads, 0),
    modeCaptures: Object.freeze(ordered),
  });
}

async function captureMap({ map, options, retailManifest, stageDirectory, signal }) {
  const modeCaptures = [];
  for (const modePlan of buildModeCapturePlans(map)) {
    if (signal.aborted) throw signal.reason || new Error('capture aborted');
    modeCaptures.push(await captureMode({
      map, modePlan, options, retailManifest, stageDirectory, signal,
    }));
  }
  return aggregateModeCaptures(map, modeCaptures);
}

export async function copyCommonTraces(options, stageDirectory, retailManifest) {
  const profile = JSON.parse(await readFile(options.commonProfile, 'utf8'));
  if (profile.schema !== PROFILE_SCHEMA || profile.policy !== PROFILE_POLICY ||
      profile.sourceRevision !== retailManifest.revision ||
      profile.sourceFingerprint !== retailManifest.fingerprint ||
      profile.performancePreset !== PERFORMANCE_PRESET || profile.maxMachineSpec !== MAX_MACHINE_SPEC) {
    throw new Error('common profile does not match the exact retail/performance identity');
  }
  if (!Array.isArray(profile.coverageScenarios) ||
      REQUIRED_COVERAGE_SCENARIOS.some((scenario) => !profile.coverageScenarios.includes(scenario))) {
    throw new Error('common profile is missing required multiplayer coverage scenarios');
  }
  if (!Array.isArray(profile.commonTraces) || profile.commonTraces.length === 0) {
    throw new Error('common profile has no common traces');
  }
  const profileDirectory = path.dirname(options.commonProfile);
  const commonDirectory = path.join(stageDirectory, 'common');
  await mkdir(commonDirectory, { recursive: true });
  const traces = [];
  for (const [index, raw] of profile.commonTraces.entries()) {
    if (typeof raw !== 'string' || !raw) throw new Error('common profile contains an invalid trace path');
    const source = path.resolve(profileDirectory, raw);
    const relative = path.relative(profileDirectory, source);
    if (relative.startsWith(`..${path.sep}`) || relative === '..' || path.isAbsolute(relative)) {
      throw new Error(`common trace escapes its profile directory: ${raw}`);
    }
    const extension = path.extname(source).toLowerCase();
    if (extension !== '.log' && extension !== '.assets') {
      throw new Error(`common trace must be a .log or .assets file: ${raw}`);
    }
    const contents = await readFile(source, 'utf8');
    if (extension === '.log' && !contents.includes('idFileSystem::OpenFileRead: ')) {
      throw new Error(`common trace has no fs_debug records: ${raw}`);
    }
    if (extension === '.assets') {
      const assets = contents.split(/\r?\n/).map((line) => line.trim())
        .filter((line) => line && !line.startsWith('#'));
      if (assets.length === 0 || assets.some((asset) =>
        !/^[A-Za-z0-9_][A-Za-z0-9_./-]*$/.test(asset) ||
        asset.includes('//') ||
        asset.split('/').some((part) => part === '.' || part === '..'))) {
        throw new Error(`common assets trace is empty or contains an unsafe path: ${raw}`);
      }
    }
    const name = `${String(index + 1).padStart(2, '0')}-${path.basename(source)}`;
    const target = path.join(commonDirectory, name);
    await copyFile(source, target, fs.constants.COPYFILE_EXCL);
    traces.push(path.posix.join('common', name));
  }
  return { traces, coverageScenarios: [...profile.coverageScenarios] };
}

function supplementTracePath(supplement) {
  return path.posix.join('supplements', supplement.map.replaceAll('/', '_'), supplement.name);
}

async function readStableSupplement(file) {
  const before = await lstat(file).catch(() => null);
  if (!before?.isFile() || before.isSymbolicLink()) {
    throw new Error(`supplement is missing, not regular, or is a symlink: ${file}`);
  }
  if (before.size <= 0 || before.size > MAX_LOG_BYTES) {
    throw new Error(`supplement size must be between 1 and ${MAX_LOG_BYTES} bytes: ${file}`);
  }
  const contents = await readFile(file);
  const after = await lstat(file).catch(() => null);
  if (!after?.isFile() || after.isSymbolicLink() || after.dev !== before.dev || after.ino !== before.ino ||
      after.size !== before.size || after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs) {
    throw new Error(`supplement changed while it was being read: ${file}`);
  }
  return contents;
}

export async function stageCaptureSupplements(plan, stageDirectory, retailManifest) {
  const maps = new Map(plan.maps.map((map) => [map.id, map]));
  const records = [];
  for (const supplement of plan.supplements) {
    const map = maps.get(supplement.map);
    if (!map) throw new Error(`supplement map is outside the capture plan: ${supplement.map}`);
    const contents = await readStableSupplement(supplement.file);
    let successfulRetailReads = null;
    if (supplement.name.toLowerCase().endsWith('.log')) {
      const validation = validateSupplementLog(contents.toString('utf8'), map, {
        expectedQ4base: path.posix.join(
          '/opfs/q4-retail/revisions', retailManifest.revision, 'runtime/q4base'),
      });
      successfulRetailReads = validation.successfulRetailReads;
    }
    const trace = supplementTracePath(supplement);
    const target = path.join(stageDirectory, ...trace.split('/'));
    await mkdir(path.dirname(target), { recursive: true });
    await writeFile(target, contents, { flag: 'wx', mode: 0o600 });
    records.push(Object.freeze({
      map: supplement.map,
      type: path.extname(supplement.name).slice(1).toLowerCase(),
      source: supplement.file,
      trace,
      size: contents.length,
      sha256: createHash('sha256').update(contents).digest('hex'),
      ...(successfulRetailReads === null ? {} : { successfulRetailReads }),
    }));
  }
  return Object.freeze(records);
}

export function buildCapturedMapProfiles(captures, supplements) {
  return captures.map((capture) => ({
    id: capture.id,
    retailGeometry: capture.retailGeometry,
    traces: [
      ...capture.traces,
      ...supplements.filter((entry) => entry.map === capture.id).map((entry) => entry.trace),
    ],
  }));
}

async function runPool(items, jobs, controller, worker) {
  let cursor = 0;
  let firstError = null;
  const results = new Array(items.length);
  const lanes = Array.from({ length: Math.min(jobs, items.length) }, async () => {
    while (!controller.signal.aborted) {
      const index = cursor++;
      if (index >= items.length) return;
      try {
        results[index] = await worker(items[index], controller.signal);
      } catch (error) {
        if (!firstError) firstError = error;
        controller.abort(error);
        return;
      }
    }
  });
  await Promise.all(lanes);
  if (firstError) throw firstError;
  if (results.some((entry) => !entry)) throw new Error('capture pool ended without a complete result set');
  return results;
}

export async function publishCaptureStage(stageDirectory, outputDirectory, manifest) {
  if (await pathInfo(outputDirectory)) throw new Error(`capture output already exists: ${outputDirectory}`);
  const manifestFile = path.join(stageDirectory, 'manifest.json');
  if (await pathInfo(manifestFile)) throw new Error('staging directory already contains manifest.json');
  const temporary = path.join(stageDirectory, `.manifest-${process.pid}-${randomBytes(6).toString('hex')}.tmp`);
  const handle = await fs.promises.open(temporary, 'wx', 0o600);
  try {
    await handle.writeFile(`${JSON.stringify(manifest, null, 2)}\n`);
    await handle.sync();
  } finally {
    await handle.close();
  }
  await rename(temporary, manifestFile);
  await rename(stageDirectory, outputDirectory);
}

function publicPlan(options, plan) {
  return {
    output: options.output,
    jobs: plan.jobs,
    maxAttempts: MAX_CAPTURE_ATTEMPTS,
    source: {
      retailDirectory: options.retailDir,
      retailManifest: options.retailManifest,
      commonProfile: options.commonProfile,
      nativeRuntime: options.nativeRuntime,
    },
    performancePreset: PERFORMANCE_PRESET,
    maxMachineSpec: MAX_MACHINE_SPEC,
    supplements: plan.supplements.map((supplement) => ({
      map: supplement.map,
      source: supplement.file,
      trace: supplementTracePath(supplement),
    })),
    maps: plan.maps.map((map) => ({
      id: map.id,
      modes: [...map.modes],
      retailGeometry: map.retailGeometry,
      botReady: map.botReady,
      port: map.port,
      captures: buildModeCapturePlans(map).map((capture) => ({
        mode: capture.mode,
        runDirectory: capture.runDirectory,
        trace: capture.trace,
        serverEvidence: capture.serverEvidence,
      })),
    })),
  };
}

export async function captureRetailBundleProfiles(options) {
  const plan = buildCapturePlan(options);
  if (options.dryRun) return { dryRun: true, plan: publicPlan(options, plan) };
  for (const command of ['stdbuf', 'xvfb-run']) {
    if (!await commandAvailable(command)) throw new Error(`required capture command is missing: ${command}`);
  }
  const client = path.join(options.nativeRuntime, 'openQ4-client_x64');
  const dedicated = path.join(options.nativeRuntime, 'openQ4-ded_x64');
  await Promise.all([
    requireExecutable(client, 'native OpenQ4 client'),
    requireExecutable(dedicated, 'native OpenQ4 dedicated server'),
  ]);
  const gameLib = await pathInfo(path.join(options.nativeRuntime, 'baseoq4/game-mp_x64.so'));
  if (!gameLib?.isFile() || gameLib.isSymbolicLink()) {
    throw new Error('native multiplayer GameLib is missing or is not a regular file');
  }
  const parsedRetail = JSON.parse(await readFile(options.retailManifest, 'utf8'));
  const retail = validateRetailManifest(parsedRetail, options.retailDir);
  await verifyExactRetailFiles(options, retail.manifest);
  const parent = path.dirname(options.output);
  await mkdir(parent, { recursive: true });
  if (await pathInfo(options.output)) throw new Error(`capture output already exists: ${options.output}`);
  const stageDirectory = path.join(parent,
    `.${path.basename(options.output)}.capture-${process.pid}-${randomBytes(6).toString('hex')}`);
  await mkdir(stageDirectory, { mode: 0o700 });
  const controller = new AbortController();
  const stop = (signal) => controller.abort(new Error(`capture interrupted by ${signal}`));
  const onInterrupt = () => stop('SIGINT');
  const onTerminate = () => stop('SIGTERM');
  process.once('SIGINT', onInterrupt);
  process.once('SIGTERM', onTerminate);
  try {
    const common = await copyCommonTraces(options, stageDirectory, retail.manifest);
    const supplements = await stageCaptureSupplements(plan, stageDirectory, retail.manifest);
    const captures = await runPool(plan.maps, plan.jobs, controller, (map, signal) =>
      captureMap({ map, options, retailManifest: retail.manifest, stageDirectory, signal }));
    await rm(path.join(stageDirectory, 'runs'), { recursive: true, force: true });
    const evidence = {
      schema: 1,
      policy: 'openq4-retail-profile-capture-evidence-v1',
      sourceRevision: retail.manifest.revision,
      sourceFingerprint: retail.manifest.fingerprint,
      capturedAt: new Date().toISOString(),
      jobs: plan.jobs,
      maxAttempts: MAX_CAPTURE_ATTEMPTS,
      supplements,
      maps: captures,
    };
    await writeFile(path.join(stageDirectory, 'capture.json'), `${JSON.stringify(evidence, null, 2)}\n`, {
      flag: 'wx', mode: 0o600,
    });
    const manifest = {
      schema: PROFILE_SCHEMA,
      policy: PROFILE_POLICY,
      sourceRevision: retail.manifest.revision,
      sourceFingerprint: retail.manifest.fingerprint,
      performancePreset: PERFORMANCE_PRESET,
      maxMachineSpec: MAX_MACHINE_SPEC,
      coverage: plan.allCatalogMaps ? 'complete' : 'selected',
      coverageScenarios: common.coverageScenarios,
      commonTraces: common.traces,
      maps: buildCapturedMapProfiles(captures, supplements),
    };
    await publishCaptureStage(stageDirectory, options.output, manifest);
    return { dryRun: false, output: options.output, manifest, evidence };
  } catch (error) {
    error.message = `${error.message}\nIncomplete staging retained at: ${stageDirectory}`;
    throw error;
  } finally {
    process.removeListener('SIGINT', onInterrupt);
    process.removeListener('SIGTERM', onTerminate);
  }
}

export async function main(argv = process.argv.slice(2)) {
  const options = parseCaptureArgs(argv);
  if (options.help) {
    process.stdout.write(usage());
    return;
  }
  const result = await captureRetailBundleProfiles(options);
  if (result.dryRun) {
    process.stdout.write(`${JSON.stringify(result.plan, null, 2)}\n`);
  } else {
    process.stdout.write(`Captured ${result.manifest.maps.length} retail bundle profiles: ${result.output}\n`);
  }
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
  main().catch((error) => {
    process.stderr.write(`capture-retail-bundle-profiles: ${error.stack || error}\n`);
    process.exitCode = 1;
  });
}
