#!/usr/bin/env node
// verify-release.mjs — the ONE canonical terminal verification command
// (SDLC-D-034, `pnpm verify:release`).
//
// Publication (.woodpecker/publish.yml `verify` step) is bound to terminal
// verification of the exact commit through this command, which is composed
// from the SAME commands the PR CI pipeline (.woodpecker/ci.yml) runs — CI and
// publish share one semantic checklist:
//
//   stage         | mirrors ci.yml step | commands
//   --------------|---------------------|------------------------------------------
//   sanitization  | sanitization        | verify-sanitized.sh, check-resident-
//                |                     | budget.sh (--self-test + run),
//                |                     | check-test-enumeration.sh
//   upgrade-guard | upgrade-guard       | test-upgrade-manifest-guard.sh,
//                |                     | test-upgrade-rollback.sh,
//                |                     | test-upgrade-durable-snapshot.sh,
//                |                     | test-install-migration.sh
//   typecheck     | typecheck           | pnpm typecheck (runs the checkout
//                |                     | preflight, then turbo typecheck)
//   lint          | lint                | pnpm lint
//   format        | format              | pnpm format:check
//   test          | test                | pnpm test
//   build         | publish.yml build   | pnpm build
//   quality-rails | (canonical-only)    | the TS quality-rails evaluator
//                |                     | (RI-N4, QC-19 monorepo subject). Like
//                |                     | `build`, this stage has no ci.yml
//                |                     | mirror; it is implemented by
//                |                     | importing the evaluator CLI rather
//                |                     | than duplicating its presence logic.
//
// Caller-provided prerequisites (kept at the pipeline level — see the comments
// in .woodpecker/ci.yml): `bash` + `rsync` for the guard stages, `openssl` and
// the pinned @earendil-works/pi-coding-agent for the test stage, and — on the
// postgres path only — the ci-postgres service plus
// `pnpm --filter @mosaicstack/db run db:migrate` before the test stage.
//
// This command works with DATABASE_URL set (CI postgres path) or unset (local
// PGlite path); it never sets, exports, or requires a database itself.
//
// scripts/verify-release.test.mjs enforces that this stage table keeps
// matching .woodpecker/ci.yml step-for-step, so the two surfaces cannot drift
// apart silently.

import { spawnSync } from 'node:child_process';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

export const STAGES = [
  {
    name: 'sanitization',
    // Mirror of the .woodpecker/ci.yml `sanitization` step (minus its
    // `apk add` environment prep). Kept as direct command strings here: the
    // #1017 test-enumeration guard audits these paths through the ci.yml
    // surface, so indirection from ci.yml into this file is not possible.
    commands: [
      'bash packages/mosaic/framework/tools/quality/scripts/verify-sanitized.sh',
      'bash packages/mosaic/framework/tools/quality/scripts/check-resident-budget.sh --self-test',
      'bash packages/mosaic/framework/tools/quality/scripts/check-resident-budget.sh',
      'bash packages/mosaic/framework/tools/quality/scripts/check-test-enumeration.sh',
      'bash packages/mosaic/framework/tools/quality/scripts/check-tools-index.sh --self-test',
      'bash packages/mosaic/framework/tools/quality/scripts/check-tools-index.sh',
      'bash packages/mosaic/framework/tools/git/test-issue-close-fail-closed.sh',
      'bash packages/mosaic/framework/tools/git/test-gitea-login-resolution.sh',
      'bash packages/mosaic/framework/tools/git/test-issue-view-comments.sh',
      'bash packages/mosaic/framework/tools/git/test-wrapper-guard.sh',
      'bash packages/mosaic/framework/tools/git/test-mosaic-worktree-large-repo.sh',
    ],
  },
  {
    name: 'upgrade-guard',
    // Mirror of the .woodpecker/ci.yml `upgrade-guard` step (minus its
    // `apk add` environment prep).
    commands: [
      'bash packages/mosaic/framework/tools/quality/scripts/test-upgrade-manifest-guard.sh',
      'bash packages/mosaic/framework/tools/quality/scripts/test-upgrade-rollback.sh',
      'bash packages/mosaic/framework/tools/quality/scripts/test-upgrade-durable-snapshot.sh',
      'bash packages/mosaic/framework/tools/quality/scripts/test-install-migration.sh',
    ],
  },
  {
    // `pnpm typecheck` is `pnpm preflight && turbo run typecheck`, so the
    // checkout preflight (scripts/preflight.mjs) is part of this stage exactly
    // as it is part of the ci.yml `typecheck` step.
    name: 'typecheck',
    commands: ['pnpm typecheck'],
  },
  {
    name: 'lint',
    commands: ['pnpm lint'],
  },
  {
    name: 'format',
    commands: ['pnpm format:check'],
  },
  {
    // Requires `openssl` and the pinned `pi` binary on the pipeline path; see
    // the caller-provided prerequisites above.
    name: 'test',
    commands: ['pnpm test'],
  },
  {
    name: 'build',
    commands: ['pnpm build'],
  },
  {
    // RI-N4 (QC-19, card RI-3-002): the typed quality-rails evaluator, invoked
    // as the implementation of the check it owns instead of a duplicated
    // presence loop here. Canonical-only stage (no ci.yml mirror — same shape
    // as `build`); runs AFTER build so the evaluator's dist/ exists. Subject
    // is this repository (`.` → monorepo subject kind, per-subject check set).
    name: 'quality-rails',
    commands: ['node packages/quality-rails/dist/cli.js quality-rails evaluate --project .'],
  },
];

export function stageByName(name) {
  return STAGES.find((stage) => stage.name === name);
}

function missingBinaries(bins) {
  return bins.filter(
    (bin) => spawnSync('sh', ['-c', `command -v ${bin} >/dev/null 2>&1`]).status !== 0,
  );
}

function runCommand(command) {
  const result = spawnSync(command, { shell: true, stdio: 'inherit' });
  if (result.error) {
    console.error(`[verify:release] failed to launch '${command}': ${result.error.message}`);
    return false;
  }
  if (result.status !== 0) {
    const reason = result.signal ? `terminated by ${result.signal}` : `exited ${result.status}`;
    console.error(`[verify:release] command '${command}' ${reason}`);
    return false;
  }
  return true;
}

// Runs the complete mandatory verification set (or, with --stage <name>, the
// single named stage — used for wiring/smoke-testing, not for gating: only a
// run of every stage is a terminal verification). Fails fast: the first
// failing command aborts with a non-zero exit code. Returns the exit code.
export function verifyRelease({ stages = STAGES } = {}) {
  const missing = missingBinaries(['bash', 'rsync']);
  if (missing.length > 0) {
    console.error(
      `[verify:release] FATAL: required binaries missing from PATH: ${missing.join(', ')}. ` +
        'The caller provides them (ci-base bakes bash; pipelines apk add rsync).',
    );
    return 1;
  }
  for (const stage of stages) {
    console.log(`\n[verify:release] === stage: ${stage.name} ===`);
    for (const command of stage.commands) {
      console.log(`[verify:release] $ ${command}`);
      if (!runCommand(command)) {
        console.error(
          `[verify:release] FATAL: stage '${stage.name}' failed — verification inconclusive`,
        );
        return 1;
      }
    }
  }
  console.log(`\n[verify:release] all ${stages.length} stage(s) passed`);
  return 0;
}

function main(argv) {
  const stageFlagIndex = argv.indexOf('--stage');
  if (stageFlagIndex !== -1) {
    const name = argv[stageFlagIndex + 1];
    const stage = stageByName(name);
    if (!stage) {
      console.error(
        `[verify:release] unknown stage '${name ?? ''}' — expected one of: ${STAGES.map((entry) => entry.name).join(', ')}`,
      );
      process.exit(2);
    }
    process.exit(verifyRelease({ stages: [stage] }));
  }
  process.exit(verifyRelease());
}

if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
  main(process.argv.slice(2));
}
