/**
 * olam runbooks — manage runbooks in the global config (~/.olam/config.json).
 *
 *   olam runbooks list
 *   olam runbooks show <name>
 *   olam runbooks add <name> --repos <r1> <r2>... [--port <repo.svc=N>...]
 *   olam runbooks remove <name>
 *   olam runbooks apply <name> [--name <world>] [--task <t>]
 *   olam runbooks check-ports <name>
 */
import pc from 'picocolors';
import { listRunbooks, getRunbook, addRunbook, removeRunbook, validateRunbookPorts, } from '@olam/core/src/global-config/index.js';
import { printError, printSuccess } from '../output.js';
function asMessage(err) {
    return err instanceof Error ? err.message : String(err);
}
/**
 * Pure renderer for `olam runbooks list`.
 *
 * Extracted so the `--json` contract is unit-testable without the global
 * config store or a TTY (Rule 9). The JSON shape is a stable, color-free
 * array of `{ name, repos, description, portCount, updatedAt }` so agents
 * and scripts can enumerate runbook inventory without screen-scraping the
 * colorized human table.
 */
export function renderRunbookList(runbooks, opts) {
    if (opts.json) {
        return JSON.stringify(runbooks.map((rb) => ({
            name: rb.name,
            repos: rb.repos,
            description: rb.description ?? null,
            portCount: rb.portMap
                ? Object.values(rb.portMap).reduce((n, svc) => n + Object.keys(svc).length, 0)
                : 0,
            updatedAt: rb.updatedAt,
        })), null, 2);
    }
    if (runbooks.length === 0) {
        return pc.dim('0 runbook(s). Add one with: olam runbooks add <name> --repos <repo>');
    }
    const lines = [pc.bold(`${runbooks.length} runbook(s)`)];
    for (const rb of runbooks) {
        const when = new Date(rb.updatedAt).toISOString().slice(0, 10);
        const repoList = rb.repos.join(', ');
        lines.push(`  ${pc.bold(rb.name.padEnd(24))} ${repoList.padEnd(40)} ${pc.dim(when)}`);
    }
    return lines.join('\n');
}
/**
 * Build the structured port-status rows for a runbook, pairing each declared
 * port against the resolved conflict set. Pure + deterministic so the
 * `check-ports --json` contract is testable without binding real TCP ports
 * (Rule 9 — the WHY: scripts gating world creation must read conflict state
 * machine-readably, not parse the ✓/✗ glyph table).
 */
export function buildPortStatusRows(portMap, conflicts) {
    const rows = [];
    for (const [repoName, svcMap] of Object.entries(portMap ?? {})) {
        for (const [serviceName, port] of Object.entries(svcMap)) {
            const conflict = conflicts.find((c) => c.port === port);
            rows.push(conflict
                ? { repoName, serviceName, port, free: false, occupant: conflict.occupant }
                : { repoName, serviceName, port, free: true });
        }
    }
    return rows;
}
/** Pure JSON renderer for `olam runbooks check-ports --json`. */
export function renderCheckPortsJson(name, rows, conflictCount) {
    return JSON.stringify({ name, conflictCount, ports: rows }, null, 2);
}
/** Parse "--port repo.svc=N" args into a nested portMap. */
function parsePortFlags(ports) {
    const portMap = {};
    for (const p of ports) {
        const eqIdx = p.lastIndexOf('=');
        if (eqIdx === -1) {
            throw new Error(`invalid --port value "${p}": expected format "repo.service=PORT"`);
        }
        const key = p.slice(0, eqIdx);
        const portStr = p.slice(eqIdx + 1);
        const dotIdx = key.indexOf('.');
        if (dotIdx === -1) {
            throw new Error(`invalid --port key "${key}": expected format "repo.service"`);
        }
        const repo = key.slice(0, dotIdx);
        const svc = key.slice(dotIdx + 1);
        const portNum = parseInt(portStr, 10);
        if (Number.isNaN(portNum)) {
            throw new Error(`invalid port number "${portStr}" in "${p}"`);
        }
        if (!portMap[repo])
            portMap[repo] = {};
        portMap[repo][svc] = portNum;
    }
    return portMap;
}
function formatConflict(c) {
    const occupantStr = c.occupant.type === 'olam-world'
        ? `in use by world "${c.occupant.worldId}" (olam)`
        : `in use by PID ${c.occupant.pid ?? 'unknown'} (non-olam process)`;
    return `  ${pc.red('✗')} ${c.repoName}.${c.serviceName}:${c.port} — ${occupantStr}`;
}
export function registerRunbooks(program) {
    const runbooks = program
        .command('runbooks')
        .description('Manage runbooks in the global config');
    runbooks
        .command('list')
        .description('List all runbooks')
        .option('--json', 'Emit machine-readable JSON instead of the text table', false)
        .action((opts) => {
        const all = listRunbooks();
        console.log(renderRunbookList(all, { json: opts.json === true }));
    });
    runbooks
        .command('show')
        .description('Show a runbook in detail')
        .argument('<name>', 'Runbook name')
        .action((name) => {
        try {
            const rb = getRunbook(name);
            console.log(JSON.stringify(rb, null, 2));
        }
        catch (err) {
            printError(asMessage(err));
            process.exitCode = 1;
        }
    });
    runbooks
        .command('add')
        .description('Create a new runbook')
        .argument('<name>', 'Runbook name (lowercase, digits, dash)')
        .requiredOption('--repos <repos...>', 'Repo names to include (space-separated)')
        .option('--description <desc>', 'Optional human-readable description')
        .option('--port <port...>', 'Port mapping: repo.service=PORT (repeatable)')
        .action((name, opts) => {
        try {
            const portMap = opts.port && opts.port.length > 0 ? parsePortFlags(opts.port) : undefined;
            const rb = addRunbook({
                name,
                repos: opts.repos,
                description: opts.description,
                portMap,
            });
            printSuccess(`created runbook "${rb.name}"`);
        }
        catch (err) {
            printError(asMessage(err));
            process.exitCode = 1;
        }
    });
    runbooks
        .command('remove')
        .description('Remove a runbook')
        .argument('<name>', 'Runbook name to remove')
        .action((name) => {
        try {
            removeRunbook(name);
            printSuccess(`removed runbook "${name}"`);
        }
        catch (err) {
            printError(asMessage(err));
            process.exitCode = 1;
        }
    });
    runbooks
        .command('apply')
        .description('Create a world from a runbook (delegates to olam create)')
        .argument('<name>', 'Runbook name')
        .option('--name <world>', 'World name override')
        .option('--task <task>', 'Initial task to dispatch')
        .action((name, opts) => {
        try {
            const rb = getRunbook(name);
            // Port-validate before attempting world creation
            const { conflicts } = validateRunbookPorts(rb);
            if (conflicts.length > 0) {
                printError(`port conflicts detected for runbook "${name}":`);
                for (const c of conflicts) {
                    console.error(formatConflict(c));
                }
                console.error(`\n${conflicts.length} port conflict(s). Stop the conflicting processes or update portMap in runbook "${name}".`);
                process.exitCode = 1;
                return;
            }
            // Delegate to the world creation pathway
            // (Phase D wires this to olam create --runbook; for now print a hint)
            console.log(pc.dim(`olam runbooks apply: port validation passed for "${name}" (repos: ${rb.repos.join(', ')}).`));
            console.log(pc.dim(`Hint: use "olam create --repos ${rb.repos.join(' ')}${opts.task ? ` --task "${opts.task}"` : ''}${opts.name ? ` --name ${opts.name}` : ''}" to create the world.`));
            console.log(pc.yellow('Phase D will wire --runbook directly. For now, use "olam create --repos ..." above.'));
        }
        catch (err) {
            printError(asMessage(err));
            process.exitCode = 1;
        }
    });
    runbooks
        .command('check-ports')
        .description('Check if runbook ports are available')
        .argument('<name>', 'Runbook name')
        .option('--json', 'Emit machine-readable JSON instead of the text table', false)
        .action((name, opts) => {
        try {
            const rb = getRunbook(name);
            const { conflicts } = validateRunbookPorts(rb);
            const rows = buildPortStatusRows(rb.portMap, conflicts);
            if (opts.json === true) {
                // --json always emits the full structured payload (including the
                // empty-ports case as `ports: []`), so consumers get one stable
                // shape regardless of whether the runbook declares ports.
                console.log(renderCheckPortsJson(name, rows, conflicts.length));
                if (conflicts.length > 0)
                    process.exitCode = 1;
                return;
            }
            if (rows.length === 0) {
                console.log(pc.dim(`runbook "${name}" declares no ports.`));
                return;
            }
            // Print all declared ports
            for (const row of rows) {
                if (!row.free) {
                    const conflict = conflicts.find((c) => c.port === row.port);
                    if (conflict)
                        console.log(formatConflict(conflict));
                }
                else {
                    console.log(`  ${pc.green('✓')} ${row.repoName}.${row.serviceName}:${row.port} — free`);
                }
            }
            if (conflicts.length > 0) {
                console.error(`\n${conflicts.length} port conflict(s). Stop the conflicting processes or update portMap in runbook "${name}".`);
                process.exitCode = 1;
            }
            else {
                console.log(pc.green('\n✓ all ports free'));
            }
        }
        catch (err) {
            printError(asMessage(err));
            process.exitCode = 1;
        }
    });
}
//# sourceMappingURL=runbooks.js.map