/**
 * Runtime wiring for declarative dynamic fan-out (issue #343).
 *
 * The declarative shape (`for_each:` in WORKFLOW.md / routine `run:` blocks) is
 * parsed and expanded by `src/lib/workflows.ts` (`parseForEachBlock`,
 * `expandForEach`). This module is the thin bridge that stages the expanded
 * teammate descriptors into the EXISTING teams substrate — one
 * `AgentManager.spawn` per descriptor, which the DAG supervisor then picks up
 * mid-flight via `rescanFromDisk` / `startReady`
 * (`src/lib/teams/supervisor.ts:98-103`).
 *
 * It deliberately adds NO new orchestration engine: `spawn` with an `--after`
 * chain is the whole mechanism, so cycle detection, wave scheduling, and
 * cross-vendor/rotation dispatch all carry over unchanged.
 */
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import { posixShellPath } from '../platform/exec.js';
import { expandForEach } from '../workflows.js';
const execFileAsync = promisify(execFile);
/**
 * Parse a producer's stdout into the item list `expandForEach` fans out over.
 *
 * Two shapes are accepted (issue #343), tried in order:
 *   1. A JSON array — `["a","b","c"]` (each element coerced to a trimmed string).
 *   2. Newline-delimited — one item per line.
 *
 * Empty lines / entries are dropped so a trailing newline or a `[]` never
 * fabricates a phantom teammate. Pure and deterministic — no I/O.
 */
export function parseProducedItems(stdout) {
    const trimmed = stdout.trim();
    if (trimmed === '')
        return [];
    // JSON array form first — only when it actually looks like one, so a plain
    // line that happens to start with '[' doesn't get swallowed by a parse error.
    if (trimmed.startsWith('[')) {
        try {
            const parsed = JSON.parse(trimmed);
            if (Array.isArray(parsed)) {
                return parsed
                    .map((x) => (x == null ? '' : typeof x === 'string' ? x : String(x)))
                    .map((s) => s.trim())
                    .filter((s) => s.length > 0);
            }
        }
        catch {
            // Not valid JSON — fall through to newline parsing.
        }
    }
    // Newline-delimited form.
    return trimmed
        .split(/\r?\n/)
        .map((s) => s.trim())
        .filter((s) => s.length > 0);
}
/**
 * Resolve a `for_each` spec's item list at runtime (issue #343): either by
 * running its `produce:` shell command and parsing stdout, or by resolving an
 * `itemsRef` to a prior step's list. The resulting items feed `expandForEach` /
 * `runForEach`.
 *
 * `itemsRef` wins when a resolver is supplied and returns a list; otherwise the
 * `produce` command runs. A spec with neither a resolvable ref nor a produce
 * command is a hard error — there is nothing to fan out over.
 */
export async function produceItems(spec, opts = {}) {
    if (spec.itemsRef && opts.resolveItemsRef) {
        const resolved = opts.resolveItemsRef(spec.itemsRef);
        if (resolved)
            return resolved;
    }
    if (!spec.produce) {
        if (spec.itemsRef) {
            throw new Error(`for_each itemsRef '${spec.itemsRef}' could not be resolved and no produce command is set`);
        }
        throw new Error('for_each has neither a produce command nor a resolvable itemsRef');
    }
    // The producer is a shell command string (e.g. "rg -l 'x' src/"), so it runs
    // under `sh -c` exactly like a teammate command — resolved portably, because
    // /bin/sh does not exist on Windows (there it's Git's sh.exe from PATH).
    // maxBuffer is raised well above the default 1MB so a producer emitting a
    // large list isn't truncated silently mid-stream.
    const { stdout } = await execFileAsync(posixShellPath(), ['-c', spec.produce], {
        cwd: opts.cwd ?? undefined,
        env: opts.env ?? process.env,
        timeout: opts.timeoutMs ?? 120_000,
        maxBuffer: 64 * 1024 * 1024,
        encoding: 'utf-8',
    });
    return parseProducedItems(stdout);
}
/**
 * Evaluate a verify panel's `keep_if` gate against its boolean votes (issue
 * #343). A `true` vote is a skeptic confirming the finding should be kept.
 *
 *   - `all`      — every skeptic must vote keep.
 *   - `any`      — at least one skeptic votes keep.
 *   - `majority` — strictly more than half vote keep (a tie does NOT pass).
 *
 * An empty panel returns false: with no votes there is nothing affirming the
 * item, so the conservative gate drops it.
 */
export function evaluateKeepIf(votes, keepIf) {
    const total = votes.length;
    if (total === 0)
        return false;
    const yes = votes.reduce((n, v) => (v ? n + 1 : n), 0);
    switch (keepIf) {
        case 'all':
            return yes === total;
        case 'any':
            return yes >= 1;
        case 'majority':
            return yes * 2 > total;
    }
}
/**
 * Tally the verify panels of an expanded `for_each` and gate each item (issue
 * #343). Groups verify teammates by the stage teammate they depend on, reads a
 * boolean vote per verify teammate via `readVote`, and applies `keep_if`.
 *
 * An item with no verify panel is kept unconditionally (there is no gate).
 * Pure and deterministic: `readVote` is the only place runtime state enters, so
 * this is unit-testable with a synthetic vote reader.
 */
export function tallyForEach(teammates, readVote) {
    const stages = teammates.filter((t) => t.role === 'stage');
    const verifiersByStage = new Map();
    for (const t of teammates) {
        if (t.role !== 'verify')
            continue;
        const stageName = t.after[0];
        if (!stageName)
            continue;
        const list = verifiersByStage.get(stageName) ?? [];
        list.push(t);
        verifiersByStage.set(stageName, list);
    }
    return stages.map((stage) => {
        const panel = verifiersByStage.get(stage.name) ?? [];
        if (panel.length === 0) {
            return {
                item: stage.item,
                itemIndex: stage.itemIndex,
                stageName: stage.name,
                kept: true,
                votes: [],
            };
        }
        const keepIf = panel[0].keep_if ?? 'majority';
        const votes = panel.map((v) => readVote(v));
        return {
            item: stage.item,
            itemIndex: stage.itemIndex,
            stageName: stage.name,
            kept: evaluateKeepIf(votes, keepIf),
            votes,
            keepIf,
        };
    });
}
/**
 * Expand a `for_each` spec against a producer's output and stage every
 * resulting teammate into the given team via the dynamic-add path.
 *
 * Returns the descriptors and the spawned handles so a caller can drive the
 * supervisor and later gather results (e.g. to evaluate a `verify` panel's
 * `keep_if` gate — that vote-counting lives downstream, not here).
 */
export async function runForEach(mgr, teamName, spec, items, opts = {}) {
    const { teammates, producedCount, usedCount, truncated } = expandForEach(spec, items, {
        producerName: opts.producerName,
    });
    const effort = opts.effort ?? 'medium';
    const spawned = [];
    for (const t of teammates) {
        const agent = await mgr.spawn(teamName, t.agentType, t.prompt, opts.cwd ?? null, null, // mode: inherit team default
        effort, null, // parentSessionId
        null, // workspaceDir
        null, // version
        t.name, t.after);
        spawned.push(agent);
    }
    return { teammates, spawned, producedCount, usedCount, truncated };
}
