// _darwin.mjs — shared invocation helper for the `@metaharness/darwin` CLI.
//
// Mirrors `_harness.mjs` for the umbrella `metaharness` / `harness` binaries,
// but targets the separate darwin binary (`metaharness-darwin`) which is
// published as its own npm package (`@metaharness/darwin@~0.3.1`).
//
// Three subcommands surfaced (matches darwin 0.3.x):
//   - `metaharness-darwin evolve <repo> [...]`         — harness self-improvement
//   - `metaharness-darwin bench <create|verify> ...`   — bench-suite lifecycle
//   - `metaharness-darwin security bench [...]`        — Darwin Shield (upstream ADR-155)
//
// CONTRACT (identical to runMetaharness/runHarness):
//   - returns `{ stdout, stderr, exitCode, json|null, durationMs, degraded, reason? }`
//   - `--json` is appended automatically when `opts.json !== false`
//   - subprocess hard timeout (default 60s, override with opts.timeoutMs)
//   - on MODULE_NOT_FOUND / network failure / "not installed", returns
//     `degraded: true, reason: 'metaharness-darwin-not-available'` — never throws
//     (ADR-150 graceful-degradation rule #3; ADR-153 §"Architecture" constraint 3)
//
// IMPORTANT — evolve has long timeouts:
//   `evolve` is the only long-running verb in the metaharness family. A real
//   evolution with --generations 10 --children 5 --concurrency 2 takes
//   minutes-to-hours, NOT seconds. Callers MUST pass an explicit `timeoutMs`
//   matched to their --generations × --children × sandbox cost; the 60s
//   default is for `bench verify` and `security bench --population 2 --cycles 1`
//   smoke shapes only.

import { spawnSync, spawn } from 'node:child_process';

const DEFAULT_TIMEOUT_MS = 60_000;

// Pinned semver range. Bump in lock-step with optionalDependencies in
// @claude-flow/cli/package.json + ruflo/package.json. The `~` allows
// patch upgrades without re-pinning.
const DARWIN_PIN = '@metaharness/darwin@~0.3.1';

const DEGRADED_RX = /could not determine executable|404|not installed|MODULE_NOT_FOUND|ENOTFOUND|getaddrinfo|ECONNREFUSED|ETIMEDOUT/i;

function buildArgv(args, wantJson) {
  // `metaharness-darwin` historically does NOT take --json on every
  // subcommand (some emit plain text reports — security bench in
  // particular is markdown). We still append --json for callers that
  // explicitly request it, but never silently inject it on subcommands
  // that don't accept it. The shape that does support --json is the
  // single-verb `evolve` and `bench verify`.
  if (!wantJson || args.includes('--json')) return args;
  return [...args, '--json'];
}

function classifyDegraded(stderr, exitCode) {
  if (exitCode === null) return { degraded: true, reason: 'metaharness-darwin-timeout' };
  if (DEGRADED_RX.test(stderr)) return { degraded: true, reason: 'metaharness-darwin-not-available' };
  return { degraded: false };
}

function maybeParseJson(stdout) {
  // darwin's evolve emits a final JSON object at end of stdout when --json
  // is passed; everything before is human-readable progress. Grab the LAST
  // {...} block, not the first — the first may be a per-generation log line.
  const matches = [...stdout.matchAll(/\{[\s\S]*?\}/g)];
  if (matches.length === 0) return null;
  // Try last match first; fall back to greedy single-block match.
  for (let i = matches.length - 1; i >= 0; i--) {
    try { return JSON.parse(matches[i][0]); } catch { /* try previous */ }
  }
  const greedy = /\{[\s\S]*\}/.exec(stdout);
  if (greedy) { try { return JSON.parse(greedy[0]); } catch { return null; } }
  return null;
}

/**
 * Sync invocation. Use for short subcommands (`bench verify`, smoke shapes).
 * Async variant below for long-running `evolve` calls.
 */
export function runDarwin(args, opts = {}) {
  const start = Date.now();
  const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
  const wantJson = opts.json !== false;
  const argv = buildArgv(args, wantJson);
  const r = spawnSync('npx', ['-y', '-p', DARWIN_PIN, 'metaharness-darwin', ...argv], {
    stdio: ['ignore', 'pipe', 'pipe'],
    encoding: 'utf-8',
    timeout: timeoutMs,
    cwd: opts.cwd,
    env: { ...process.env, ...(opts.env || {}) },
    shell: process.platform === 'win32',
  });
  const durationMs = Date.now() - start;
  const stdout = r.stdout || '';
  const stderr = r.stderr || '';
  const classified = classifyDegraded(stderr, r.status);
  if (classified.degraded) {
    return {
      stdout, stderr,
      exitCode: r.status ?? 127,
      json: null,
      durationMs,
      degraded: true,
      reason: classified.reason,
    };
  }
  return {
    stdout, stderr,
    exitCode: r.status ?? 0,
    json: wantJson ? maybeParseJson(stdout) : null,
    durationMs,
    degraded: false,
  };
}

/**
 * Async invocation with streaming. Required for `evolve` because the run
 * can take 10+ minutes and the caller wants progress visibility.
 *
 * If `opts.onProgress(line)` is provided, called once per stderr line.
 * darwin writes per-generation progress to stderr, structured result to
 * stdout. Cancellable via opts.signal (AbortSignal).
 */
export function runDarwinAsync(args, opts = {}) {
  return new Promise((resolve) => {
    const start = Date.now();
    const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
    const wantJson = opts.json !== false;
    const argv = buildArgv(args, wantJson);
    const p = spawn('npx', ['-y', '-p', DARWIN_PIN, 'metaharness-darwin', ...argv], {
      stdio: ['ignore', 'pipe', 'pipe'],
      cwd: opts.cwd,
      env: { ...process.env, ...(opts.env || {}) },
      shell: process.platform === 'win32',
    });
    let stdout = '', stderr = '', stderrBuf = '';
    p.stdout?.on('data', (d) => { stdout += d.toString(); });
    p.stderr?.on('data', (d) => {
      const s = d.toString();
      stderr += s;
      if (opts.onProgress) {
        stderrBuf += s;
        const lines = stderrBuf.split('\n');
        stderrBuf = lines.pop() ?? '';
        for (const line of lines) opts.onProgress(line);
      }
    });
    const timer = setTimeout(() => {
      try { p.kill('SIGTERM'); } catch { /* ignore */ }
    }, timeoutMs);
    if (opts.signal) {
      opts.signal.addEventListener('abort', () => {
        try { p.kill('SIGTERM'); } catch { /* ignore */ }
      }, { once: true });
    }
    p.on('error', (e) => {
      clearTimeout(timer);
      resolve({
        stdout,
        stderr: stderr + String(e?.message ?? e),
        exitCode: 127, json: null, durationMs: Date.now() - start,
        degraded: true, reason: 'metaharness-darwin-not-available',
      });
    });
    p.on('close', (code) => {
      clearTimeout(timer);
      const durationMs = Date.now() - start;
      const classified = classifyDegraded(stderr, code);
      if (classified.degraded) {
        resolve({
          stdout, stderr,
          exitCode: code ?? 127, json: null, durationMs,
          degraded: true, reason: classified.reason,
        });
        return;
      }
      resolve({
        stdout, stderr,
        exitCode: code ?? 0,
        json: wantJson ? maybeParseJson(stdout) : null,
        durationMs,
        degraded: false,
      });
    });
  });
}

/**
 * Match the existing `emitDegradedJsonAndExit` shape used by the
 * metaharness-umbrella scripts so MCP tool consumers see one contract.
 */
export function emitDarwinDegradedJsonAndExit(reason) {
  const payload = {
    degraded: true,
    reason,
    hint: 'Install with `npm i -D ' + DARWIN_PIN + '` or run `npx -y ' + DARWIN_PIN + ' --help` to verify network access.',
    generatedAt: new Date().toISOString(),
  };
  console.log(JSON.stringify(payload, null, 2));
  // Exit 0 — ADR-150 architectural constraint: ruflo continues to function
  // when MetaHarness is absent. Same posture as the umbrella scripts.
  process.exit(0);
}

export const DARWIN_VERSION_PIN = DARWIN_PIN;
