/**
 * olam refresh <world> — Hot-refresh per-world CP from the local checkout.
 *
 * Copies the latest packages/control-plane/standalone/ source into a running
 * devbox container, then restarts the CP via `docker restart`.
 *
 * Restart mechanism: the devbox CMD is `node /opt/olam/control-plane/server.mjs`
 * (PID 1 via entrypoint.sh's `exec "$@"`). No supervisor wrapper exists — killing
 * PID 1 stops the container. `docker restart` is therefore the correct mechanism:
 * it stops the container, re-runs entrypoint.sh + CMD, which reinstalls hooks
 * and starts the CP cleanly.
 *
 * Side-effect: any running tmux sessions (including active claude sessions) are
 * killed on restart. Operators should re-dispatch if needed afterward.
 * This is the accepted tradeoff for a maintenance operation.
 *
 * Source parity: the file list mirrors build-devbox.sh:38-77 exactly. Any
 * drift between this file and that script will produce a broken world.
 */
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { spawnSync } from 'node:child_process';
import ora from 'ora';
import { loadContext } from '../context.js';
import { printError, printInfo, printHeader, printWarning } from '../output.js';
import { collectCpSourceFiles, validateServerMjsPresent, } from './refresh-helpers.js';
// Re-export for consumers who want the pure helpers without the full command.
export { collectCpSourceFiles, validateServerMjsPresent } from './refresh-helpers.js';
// Container port 8080 maps to host port 19080 + portOffset (container.ts)
const HOST_CP_BASE_PORT = 19080;
// docker restart --time: seconds to wait for graceful stop before SIGKILL
const RESTART_TIMEOUT_S = 30;
// Health poll interval / total timeout
const HEALTH_POLL_MS = 500;
const HEALTH_TIMEOUT_MS = 30_000;
// ── Docker helpers ──────────────────────────────────────────────────────────
function docker(args) {
    const result = spawnSync('docker', args, {
        encoding: 'utf-8',
        stdio: ['ignore', 'pipe', 'pipe'],
    });
    return {
        ok: result.status === 0 && result.error === undefined,
        stdout: result.stdout ?? '',
        stderr: result.stderr ?? '',
    };
}
async function waitForCpHealth(port, timeoutMs = HEALTH_TIMEOUT_MS) {
    const deadline = Date.now() + timeoutMs;
    while (Date.now() < deadline) {
        try {
            const res = await fetch(`http://127.0.0.1:${port}/health`, {
                signal: AbortSignal.timeout(2000),
            });
            if (res.ok)
                return true;
        }
        catch {
            // not up yet — keep polling
        }
        await new Promise(r => setTimeout(r, HEALTH_POLL_MS));
    }
    return false;
}
async function refreshWorld(worldId, portOffset, standaloneDir, opts) {
    const containerName = `olam-${worldId}-devbox`;
    // 1. Collect + validate source files
    let entries;
    try {
        entries = collectCpSourceFiles(standaloneDir);
        if (!opts.skipBuild) {
            validateServerMjsPresent(entries);
        }
    }
    catch (err) {
        return {
            worldId,
            ok: false,
            error: err instanceof Error ? err.message : String(err),
        };
    }
    // 2. Stage into a temp dir (mirrors build-devbox.sh's BUILD_DIR staging)
    const stagingDir = fs.mkdtempSync(path.join(os.tmpdir(), `olam-refresh-${worldId}-`));
    try {
        const hasLib = entries.some(e => e.destRelPath.startsWith('lib/'));
        if (hasLib) {
            fs.mkdirSync(path.join(stagingDir, 'lib'), { recursive: true });
        }
        for (const { srcPath, destRelPath } of entries) {
            fs.copyFileSync(srcPath, path.join(stagingDir, destRelPath));
        }
        // 3. docker cp staging contents → container
        // Trailing /. on the source copies contents, not the dir itself.
        const cpResult = docker([
            'cp',
            `${stagingDir}/.`,
            `${containerName}:/opt/olam/control-plane/`,
        ]);
        if (!cpResult.ok) {
            return {
                worldId,
                ok: false,
                error: `docker cp failed: ${cpResult.stderr.trim() || cpResult.stdout.trim()}`,
            };
        }
    }
    finally {
        fs.rmSync(stagingDir, { recursive: true, force: true });
    }
    // 4. Restart if requested
    if (opts.restart) {
        const restartResult = docker([
            'restart', '--time', String(RESTART_TIMEOUT_S), containerName,
        ]);
        if (!restartResult.ok) {
            return {
                worldId,
                ok: false,
                error: `docker restart failed: ${restartResult.stderr.trim() || restartResult.stdout.trim()}`,
            };
        }
        // 5. Poll health endpoint
        const cpPort = HOST_CP_BASE_PORT + portOffset;
        const healthy = await waitForCpHealth(cpPort);
        if (!healthy) {
            // Non-fatal: files are copied and container restarted, just slow to respond
            return { worldId, ok: true, error: `healthy but /health at :${cpPort} didn't respond within ${HEALTH_TIMEOUT_MS / 1000}s` };
        }
    }
    return { worldId, ok: true };
}
// ── Command registration ────────────────────────────────────────────────────
export function registerRefresh(program) {
    program
        .command('refresh')
        .description("Hot-refresh a running world's per-world CP from the local checkout")
        .argument('[world]', 'World ID')
        .option('--all', 'Refresh every running world sequentially (overrides positional arg)')
        .option('--no-restart', 'Copy files but skip CP restart (for debugging)')
        .option('--skip-build', 'Skip the prerequisite server.mjs presence check')
        .action(async (worldId, opts) => {
        // Validate: need world ID or --all
        if (!opts.all && !worldId) {
            printError('Specify a world ID or use --all to refresh all running worlds.');
            process.exitCode = 1;
            return;
        }
        const { ctx, error } = await loadContext();
        if (!ctx) {
            printError(error?.message ?? 'Olam is not configured. Run `olam init` first.');
            process.exitCode = 1;
            return;
        }
        // Locate standalone source dir (must run from olam repo root)
        const standaloneDir = path.join(process.cwd(), 'packages/control-plane/standalone');
        if (!fs.existsSync(standaloneDir)) {
            printError(`CP standalone source not found at ${standaloneDir}.\n` +
                'Run `olam refresh` from the olam repo root.');
            process.exitCode = 1;
            return;
        }
        const restart = opts.restart !== false;
        const skipBuild = opts.skipBuild === true;
        // Collect worlds to refresh
        const worldsToRefresh = opts.all
            ? ctx.worldManager.listWorlds({ status: 'running' })
            : (() => {
                const w = ctx.worldManager.getWorld(worldId);
                if (!w) {
                    printError(`World "${worldId}" not found in registry. Run \`olam list\` to see worlds.`);
                    process.exitCode = 1;
                    return null;
                }
                if (w.status !== 'running') {
                    printError(`World "${worldId}" is ${w.status}. Only running worlds can be refreshed.`);
                    process.exitCode = 1;
                    return null;
                }
                return [w];
            })();
        if (!worldsToRefresh)
            return; // error already printed above
        if (worldsToRefresh.length === 0) {
            printInfo('No worlds', 'No running worlds found to refresh.');
            return;
        }
        printHeader('olam refresh');
        printInfo('Source', standaloneDir);
        printInfo('Worlds', worldsToRefresh.map(w => `${w.name} (${w.id})`).join(', '));
        printInfo('Restart', restart ? 'yes (docker restart)' : 'no (--no-restart)');
        process.stdout.write('\n');
        const results = [];
        for (const world of worldsToRefresh) {
            const spinner = ora(`${world.name}: copying CP source...`).start();
            try {
                const result = await refreshWorld(world.id, world.portOffset, standaloneDir, { restart, skipBuild });
                results.push(result);
                if (!result.ok) {
                    spinner.fail(`${world.name}: ${result.error}`);
                    continue;
                }
                // ok=true but with a warning (health timeout)
                if (result.error) {
                    spinner.warn(`${world.name}: refreshed — ${result.error}`);
                }
                else if (restart) {
                    spinner.succeed(`${world.name}: refreshed and healthy`);
                }
                else {
                    spinner.succeed(`${world.name}: files copied (restart skipped)`);
                }
            }
            catch (err) {
                const msg = err instanceof Error ? err.message : String(err);
                results.push({ worldId: world.id, ok: false, error: msg });
                spinner.fail(`${world.name}: ${msg}`);
            }
        }
        // Summary for --all
        if (opts.all) {
            const okCount = results.filter(r => r.ok).length;
            const errCount = results.filter(r => !r.ok).length;
            process.stdout.write('\n');
            if (errCount > 0) {
                printWarning(`refreshed: ${okCount}, errored: ${errCount}`);
            }
            else {
                printInfo('Summary', `refreshed: ${okCount}`);
            }
        }
        if (results.some(r => !r.ok)) {
            process.exitCode = 1;
        }
    });
}
//# sourceMappingURL=refresh.js.map