/**
 * Run the continuous DAG dispatcher until the team drains, the caller
 * returns false from onWave, or SIGINT/SIGTERM arrives.
 */
export async function runSupervisor(mgr, opts) {
    const intervalMs = opts.intervalMs ?? 8000;
    const maxWaves = opts.maxWaves ?? 1000;
    const team = opts.team;
    const startedAt = Date.now();
    let stopSignal = false;
    const onSig = () => { stopSignal = true; };
    process.once('SIGINT', onSig);
    process.once('SIGTERM', onSig);
    try {
        for (let wave = 1; wave <= maxWaves; wave++) {
            // Pick up teammates added by other processes (e.g. the Planner's
            // `agents teams add` calls). Without this the supervisor only ever
            // sees teammates it created itself.
            await mgr.rescanFromDisk();
            // Distributed teammates: one ssh-per-host liveness/exit pre-pass BEFORE any
            // polling this wave, so every subsequent per-teammate status read
            // (startReady's roster scan + listByTask below) consumes a cached snapshot
            // instead of each opening its own SSH handshake — no N-round-trips-per-wave
            // blowup at 10+ remote teammates. No-op for all-local teams. Reads the
            // in-memory roster directly, so it doesn't itself trigger a poll.
            await mgr.prefetchRemoteStatus(team);
            const launched = await mgr.startReady(team);
            const all = await mgr.listByTask(team);
            let pending = 0, running = 0, completed = 0, failed = 0;
            for (const a of all) {
                if (a.status === 'pending')
                    pending++;
                else if (a.status === 'running')
                    running++;
                else if (a.status === 'completed')
                    completed++;
                else if (a.status === 'failed')
                    failed++;
            }
            const summary = {
                wave,
                timestamp: new Date().toISOString(),
                team,
                launched,
                pending,
                running,
                completed,
                failed,
                drained: pending === 0 && running === 0,
            };
            const keepGoing = await opts.onWave(summary);
            if (keepGoing === false) {
                return { waves: wave, stoppedBy: 'callback', elapsed_ms: Date.now() - startedAt };
            }
            // Live budget kill (issue #399). Poll AFTER the wave's callback so the
            // most recent teammate stdout is already flushed to disk. On breach we
            // stop every RUNNING teammate — the DAG effectively drains this wave.
            if (opts.budgetWatcher) {
                await opts.budgetWatcher.poll();
                if (opts.budgetWatcher.breached()) {
                    const breach = opts.budgetWatcher.breach();
                    await mgr.stopByTask(team);
                    if (breach && opts.onBudgetBreach)
                        opts.onBudgetBreach(breach);
                    opts.budgetWatcher.dispose();
                    return {
                        waves: wave,
                        stoppedBy: 'budget',
                        elapsed_ms: Date.now() - startedAt,
                        budgetBreach: breach ?? undefined,
                    };
                }
            }
            // Re-check drain AFTER the callback. The callback may have added new
            // teammates mid-flight (that's the whole point of the dynamic DAG), so
            // trusting the pre-callback snapshot would drain prematurely. Rescan
            // too, because the callback could have triggered a sibling process
            // that wrote a fresh meta.json.
            await mgr.rescanFromDisk();
            const afterCallback = await mgr.listByTask(team);
            const stillLive = afterCallback.some((a) => a.status === 'pending' || a.status === 'running');
            if (!stillLive) {
                const failed = afterCallback.filter((a) => a.status === 'failed').length;
                return { waves: wave, stoppedBy: 'drained', failed, elapsed_ms: Date.now() - startedAt };
            }
            if (stopSignal) {
                return { waves: wave, stoppedBy: 'signal', elapsed_ms: Date.now() - startedAt };
            }
            await new Promise((r) => setTimeout(r, intervalMs));
            if (stopSignal) {
                return { waves: wave, stoppedBy: 'signal', elapsed_ms: Date.now() - startedAt };
            }
        }
        return { waves: maxWaves, stoppedBy: 'max-waves', elapsed_ms: Date.now() - startedAt };
    }
    finally {
        process.off('SIGINT', onSig);
        process.off('SIGTERM', onSig);
    }
}
