import fs from 'node:fs';
import path from 'node:path';
import { spawn } from 'node:child_process';
import { createReleaseGateHermeticEnv } from './release-gate-hermetic-env.js';
import { appendReleaseGateJsonl, writeReleaseGateJson } from './release-gate-report.js';
import { findReadyReleaseGateNodes, findReleaseGatesBlockedByFailedDeps, pickReadyLaunchableReleaseGates } from './release-gate-scheduler.js';
import { readReleaseGateCacheRecord, writeReleaseGateCacheHit } from './release-gate-cache-v2.js';
import { RELEASE_GATE_NODE_SCHEMA, validateReleaseGateManifest } from './release-gate-node.js';
import { countReleaseGateResources, defaultReleaseGateBudget, summarizeReleaseGateBudget } from './release-gate-resource-governor.js';
import { selectAffectedReleaseGates } from './release-gate-affected-selector.js';
import { guardedProcessKill, guardContextForRoute } from '../safety/mutation-guard.js';
import { createRequestedScopeContract } from '../safety/requested-scope-contract.js';
import { rmrf } from '../fsx.js';
export function loadReleaseGateManifest(root, file = 'release-gates.v2.json') {
    const manifestPath = path.join(root, file);
    const parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
    const validation = validateReleaseGateManifest(parsed);
    if (!validation.ok || !validation.manifest) {
        throw new Error(`invalid ${file}: ${validation.errors.join(', ')}`);
    }
    return validation.manifest;
}
export async function runReleaseGateDag(input) {
    const root = path.resolve(input.root);
    const preset = input.preset || 'release';
    const manifest = loadReleaseGateManifest(root);
    const presetGates = selectReleaseGatePreset(manifest, preset);
    const affected = (preset === 'affected' || preset === 'fast') && input.full !== true
        ? selectAffectedReleaseGates(root, manifest, presetGates, { changedSince: input.changedSince || 'auto', preset })
        : selectAffectedReleaseGates(root, manifest, presetGates, { full: true, preset });
    const selected = affected.gates;
    const selectedIds = new Set(selected.map((gate) => gate.id));
    const affectedExternalSatisfiedDeps = affected.selection.mode === 'affected'
        ? new Set(selected.flatMap((gate) => gate.deps || []).filter((dep) => !selectedIds.has(dep)))
        : new Set();
    const runId = `rg-${new Date().toISOString().replace(/[:.]/g, '-')}-${process.pid}`;
    const retentionBefore = await pruneOldReleaseGateRunDirs(root);
    const reportDir = path.join(root, '.sneakoscope', 'reports', 'release-gates', runId);
    fs.mkdirSync(reportDir, { recursive: true });
    const timeline = path.join(reportDir, 'timeline.jsonl');
    appendReleaseGateJsonl(timeline, { event: 'retention', phase: 'before_run', ...retentionBefore, at: new Date().toISOString() });
    const started = Date.now();
    const pending = new Map(selected.map((gate) => [gate.id, gate]));
    const running = new Map();
    const completed = new Map();
    const failed = new Map();
    const budget = defaultReleaseGateBudget();
    const peakResources = {};
    let cached = 0;
    const cachedGates = [];
    const executedGates = [];
    let sumGateMs = 0;
    let peakRunning = 0;
    const writeSummarySnapshot = (finished = false) => {
        const wallMs = Date.now() - started;
        const failures = [...failed.values()].map((row) => ({ id: row.id, exit_code: row.exit_code, stderr_tail: row.stderr_tail, timed_out: row.timed_out, signal: row.signal }));
        const snapshot = {
            schema: 'sks.release-gate-dag-run.v1',
            ok: failures.length === 0,
            run_id: runId,
            selected_preset: preset,
            total_gates: manifest.gates.length,
            selected_gates: selected.length,
            selected_gate_ids: selected.map((gate) => gate.id),
            skipped_by_affected: affected.selection.mode === 'affected' ? affected.selection.skipped_gate_ids : [],
            affected_selection: affected.selection,
            completed: completed.size,
            failed: failed.size,
            cached,
            wall_ms: wallMs,
            sum_gate_ms: sumGateMs,
            cpu_time_saved_ms: Math.max(0, sumGateMs - wallMs),
            parallelism_gain: wallMs > 0 ? Number((sumGateMs / wallMs).toFixed(2)) : 1,
            critical_path_ms: estimateCriticalPath(selected, completed),
            peak_running: peakRunning,
            peak_resources: peakResources,
            cached_gates: cachedGates,
            executed_gates: executedGates,
            slowest_gates: [...completed.values(), ...failed.values()]
                .sort((a, b) => b.duration_ms - a.duration_ms)
                .slice(0, 10)
                .map((row) => ({ id: row.id, duration_ms: row.duration_ms, cached: row.cached })),
            budget_snapshot: budget,
            budget_summary: summarizeReleaseGateBudget(budget),
            report_dir: reportDir,
            failures
        };
        if (!finished) {
            snapshot.in_progress = true;
            snapshot.pending = pending.size;
            snapshot.running = running.size;
        }
        writeReleaseGateJson(path.join(reportDir, 'summary.json'), snapshot);
        return snapshot;
    };
    if (input.explain) {
        writeReleaseGateJson(path.join(reportDir, 'explain.json'), { schema: RELEASE_GATE_NODE_SCHEMA, preset, budget, gates: selected.map((gate) => ({ id: gate.id, deps: gate.deps, resource: gate.resource, command: gate.command })) });
    }
    while (pending.size || running.size) {
        const ready = findReadyReleaseGateNodes({ pending, completed, failed, satisfiedDeps: affectedExternalSatisfiedDeps });
        const launchable = pickReadyLaunchableReleaseGates({ ready, running: [...running.values()].map((row) => row.gate) });
        let progressed = false;
        for (const gate of launchable) {
            pending.delete(gate.id);
            const cacheHit = !input.noCache && gate.cache.enabled ? readReleaseGateCacheRecord(root, gate) : null;
            if (cacheHit) {
                const result = { id: gate.id, ok: true, exit_code: 0, signal: null, timed_out: false, duration_ms: cacheHit.duration_ms, cached: true, stderr_tail: '' };
                completed.set(gate.id, result);
                cached += 1;
                cachedGates.push(gate.id);
                sumGateMs += result.duration_ms;
                progressed = true;
                appendReleaseGateJsonl(timeline, { event: 'cache_hit', gate_id: gate.id, duration_ms: result.duration_ms, at: new Date().toISOString() });
                writeSummarySnapshot(false);
                continue;
            }
            appendReleaseGateJsonl(timeline, { event: 'start', gate_id: gate.id, resource: gate.resource, at: new Date().toISOString() });
            executedGates.push(gate.id);
            running.set(gate.id, { gate, promise: runGate(root, runId, reportDir, gate) });
            peakRunning = Math.max(peakRunning, running.size);
            const used = countReleaseGateResources([...running.values()].map((row) => row.gate));
            for (const [resource, count] of Object.entries(used)) {
                peakResources[resource] = Math.max(peakResources[resource] || 0, Number(count) || 0);
            }
            progressed = true;
        }
        if (!running.size) {
            const blockedByFailedDeps = findReleaseGatesBlockedByFailedDeps({ pending, failed });
            if (blockedByFailedDeps.length) {
                for (const gate of blockedByFailedDeps) {
                    pending.delete(gate.id);
                    const result = {
                        id: gate.id,
                        ok: false,
                        exit_code: null,
                        signal: null,
                        timed_out: false,
                        duration_ms: 0,
                        cached: false,
                        stderr_tail: `blocked by failed dependency: ${gate.deps.filter((dep) => failed.has(dep)).join(', ')}`
                    };
                    failed.set(gate.id, result);
                    appendReleaseGateJsonl(timeline, { event: 'blocked_by_failed_dependency', gate_id: gate.id, deps: gate.deps.filter((dep) => failed.has(dep)), at: new Date().toISOString() });
                }
                continue;
            }
            if (progressed)
                continue;
            const blocked = [...pending.keys()];
            throw new Error(`release gate DAG stalled: ${blocked.join(', ')}`);
        }
        const result = await Promise.race([...running.values()].map((row) => row.promise));
        running.delete(result.id);
        sumGateMs += result.duration_ms;
        if (result.ok) {
            completed.set(result.id, result);
            const gate = selected.find((row) => row.id === result.id);
            if (gate?.cache.enabled && !input.noCache)
                writeReleaseGateCacheHit(root, gate, result.duration_ms);
        }
        else {
            failed.set(result.id, result);
            if (input.failFast) {
                for (const id of [...pending.keys()])
                    pending.delete(id);
            }
        }
        appendReleaseGateJsonl(timeline, { event: result.ok ? 'pass' : 'fail', gate_id: result.id, duration_ms: result.duration_ms, at: new Date().toISOString() });
        writeSummarySnapshot(false);
    }
    const result = writeSummarySnapshot(true);
    const retentionAfter = await pruneOldReleaseGateRunDirs(root, { preserveRunId: runId });
    const finalResult = { ...result, retention: mergeReleaseGateRetention(retentionBefore, retentionAfter) };
    appendReleaseGateJsonl(timeline, { event: 'retention', phase: 'after_run', ...retentionAfter, at: new Date().toISOString() });
    writeReleaseGateJson(path.join(reportDir, 'summary.json'), finalResult);
    return finalResult;
}
export function selectReleaseGatePreset(manifest, preset) {
    const effectivePreset = preset === 'affected' || preset === 'fast' ? 'release' : preset;
    return manifest.gates.filter((gate) => gate.preset.includes(effectivePreset));
}
function runGate(root, runId, reportRoot, gate) {
    const started = Date.now();
    const hermetic = createReleaseGateHermeticEnv({ root, runId, gate, reportRoot });
    const stdoutFile = path.join(hermetic.report_dir, 'stdout.log');
    const stderrFile = path.join(hermetic.report_dir, 'stderr.log');
    const out = fs.createWriteStream(stdoutFile);
    const err = fs.createWriteStream(stderrFile);
    return new Promise((resolve) => {
        const child = spawn(gate.command, { cwd: root, shell: true, env: hermetic.env, stdio: ['ignore', 'pipe', 'pipe'], detached: process.platform !== 'win32' });
        let timedOut = false;
        let timeoutCleanup = null;
        const timer = setTimeout(() => {
            timedOut = true;
            timeoutCleanup = cleanupTimedOutGateProcessTree(root, child);
        }, gate.timeout_ms);
        timer.unref?.();
        child.stdout.pipe(out);
        child.stderr.pipe(err);
        child.on('close', (code, signal) => {
            void (async () => {
                clearTimeout(timer);
                if (timeoutCleanup)
                    await timeoutCleanup;
                out.end();
                err.end();
                const durationMs = Date.now() - started;
                const stderrText = fs.existsSync(stderrFile) ? fs.readFileSync(stderrFile, 'utf8') : '';
                const timeoutTail = timedOut ? `release_gate_timeout:${gate.id}:${gate.timeout_ms}ms` : '';
                const signalTail = !timedOut && signal ? `release_gate_signal:${gate.id}:${signal}` : '';
                const stderrTail = tail([stderrText, timeoutTail, signalTail].filter(Boolean).join('\n'));
                const exitCode = timedOut ? 124 : code;
                const result = { id: gate.id, ok: exitCode === 0, exit_code: exitCode, signal, timed_out: timedOut, duration_ms: durationMs, cached: false, stderr_tail: stderrTail };
                writeReleaseGateJson(path.join(hermetic.report_dir, 'result.json'), { schema: 'sks.release-gate-result.v1', ...result, stdout_log: stdoutFile, stderr_log: stderrFile });
                resolve(result);
            })();
        });
    });
}
async function cleanupTimedOutGateProcessTree(root, child) {
    await killGateProcessTree(root, child, 'SIGTERM');
    await sleep(1500);
    await killGateProcessTree(root, child, 'SIGKILL');
    await sleep(100);
}
async function killGateProcessTree(root, child, signal) {
    if (!child.pid)
        return;
    const pid = process.platform !== 'win32' ? -child.pid : child.pid;
    const contract = createRequestedScopeContract({
        route: 'release:gate-runner',
        userRequest: 'Terminate only the release gate child process tree after its configured timeout.',
        projectRoot: root,
        overrides: { codex_app_process: true }
    });
    try {
        await guardedProcessKill(guardContextForRoute(root, contract, 'release gate timeout cleanup'), pid, { signal, confirmed: true });
    }
    catch {
        try {
            child.kill(signal);
        }
        catch { }
    }
}
function sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
}
function estimateCriticalPath(gates, completed) {
    const byId = new Map(gates.map((gate) => [gate.id, gate]));
    const memo = new Map();
    const visit = (id) => {
        if (memo.has(id))
            return memo.get(id);
        const gate = byId.get(id);
        if (!gate)
            return 0;
        const own = completed.get(id)?.duration_ms || 0;
        const dep = Math.max(0, ...gate.deps.map(visit));
        memo.set(id, own + dep);
        return own + dep;
    };
    return Math.max(0, ...gates.map((gate) => visit(gate.id)));
}
function tail(value, limit = 1200) {
    return value.length > limit ? value.slice(-limit) : value;
}
export async function pruneOldReleaseGateRunDirs(root, opts = {}) {
    const keep = Math.max(1, Math.floor(Number(opts.keep ?? process.env.SKS_RELEASE_GATE_RUN_RETENTION ?? 20) || 20));
    const preserveRunId = opts.preserveRunId || null;
    const base = path.join(root, '.sneakoscope', 'reports', 'release-gates');
    const report = {
        schema: 'sks.release-gate-run-retention.v1',
        keep,
        scanned: 0,
        kept: 0,
        removed: 0,
        preserve_run_id: preserveRunId,
        removed_run_ids: []
    };
    if (!fs.existsSync(base))
        return report;
    const runs = fs.readdirSync(base, { withFileTypes: true })
        .filter((entry) => entry.isDirectory() && /^rg-\d{4}-/.test(entry.name))
        .map((entry) => {
        const dir = path.join(base, entry.name);
        const summary = path.join(dir, 'summary.json');
        const stat = fs.statSync(fs.existsSync(summary) ? summary : dir);
        return { id: entry.name, dir, mtimeMs: stat.mtimeMs };
    })
        .sort((a, b) => b.mtimeMs - a.mtimeMs);
    report.scanned = runs.length;
    const keepIds = new Set(runs.slice(0, keep).map((run) => run.id));
    if (preserveRunId)
        keepIds.add(preserveRunId);
    for (const run of runs) {
        if (keepIds.has(run.id)) {
            report.kept += 1;
            continue;
        }
        await rmrf(run.dir);
        report.removed += 1;
        report.removed_run_ids.push(run.id);
    }
    return report;
}
function mergeReleaseGateRetention(before, after) {
    return {
        ...after,
        scanned: Math.max(before.scanned, after.scanned),
        kept: after.kept,
        removed: before.removed + after.removed,
        removed_run_ids: [...before.removed_run_ids, ...after.removed_run_ids]
    };
}
//# sourceMappingURL=release-gate-dag.js.map