/**
 * `olam rekey <world-id>` — rotate the per-world postgres password.
 *
 * Phase A9 of `olam-hybrid-shared-postgres`. Phase A4 (commit 7b4334ff)
 * landed `applyPostgresWorldRole` which already supports rotation via
 * `ALTER ROLE` when the role exists. A9 exposes that rotation as a
 * standalone operator-facing command, useful when:
 *
 *   - The operator wants to force-rotate credentials (suspected leak).
 *   - A world container lost its env-injected password (e.g. crashed
 *     before the app read it).
 *   - A future tmpfs cred-mount flow needs a re-inject path.
 *
 * Flow:
 *   1. Load context + WorldManager.getWorld(worldId).
 *   2. Hybrid + DB gates (exit 3 if non-hybrid or no per-world DBs).
 *   3. `applyPostgresWorldRole(id, worldDbNames)` → ALTER ROLE rotates
 *      the password atomically. The old password is gone.
 *   4. Write new password to `~/.olam/worlds/<id>/credentials.json`
 *      (chmod 600).
 *   5. `docker restart olam-<id>-devbox` so the container picks up the
 *      new password from env on next process start.
 *   6. Audit-log line to `~/.olam/usage.log` (SEC-002): timestamp +
 *      world-id + "rekey" — NEVER the password.
 *
 * Exit codes:
 *   0  success
 *   1  unexpected error
 *   2  world not found
 *   3  precondition failed (non-hybrid, no DBs, etc.)
 */
import { spawnSync as defaultSpawnSync } from 'node:child_process';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { applyPostgresWorldRole as defaultApplyPostgresWorldRole, } from '@olam/core/src/world/manager.js';
import { loadContext } from '../context.js';
import { printError, printSuccess, printInfo, printHeader } from '../output.js';
/** Container name convention — mirrors enter.ts:286, refresh.ts:90, ps.ts:157. */
function devboxContainerName(worldId) {
    return `olam-${worldId}-devbox`;
}
/** Resolve `~/.olam` honoring OLAM_HOME (mirrors host-cp.ts:55). */
function olamHomeDir() {
    return process.env['OLAM_HOME'] ?? path.join(os.homedir(), '.olam');
}
function defaultRestartContainer(name, spawn = defaultSpawnSync) {
    // 30s graceful-stop window matches refresh.ts:41 (RESTART_TIMEOUT_S).
    const r = spawn('docker', ['restart', '--time', '30', name], {
        encoding: 'utf-8',
        stdio: ['ignore', 'pipe', 'pipe'],
    });
    return {
        ok: r.status === 0 && r.error === undefined,
        stderr: (r.stderr || '').trim() || (r.stdout || '').trim(),
    };
}
function defaultAppendAuditLog(homeDir, line) {
    fs.mkdirSync(homeDir, { recursive: true });
    fs.appendFileSync(path.join(homeDir, 'usage.log'), line.endsWith('\n') ? line : line + '\n', {
        encoding: 'utf-8',
    });
}
function buildDefaultDeps() {
    return {
        // Replaced inside `registerRekey` once the context is loaded; the
        // default here keeps the type contract self-consistent.
        getWorld: () => undefined,
        applyPostgresWorldRole: (id, dbs) => defaultApplyPostgresWorldRole(id, dbs),
        restartContainer: (name) => defaultRestartContainer(name),
        olamHomeDir,
        now: () => new Date(),
        stdout: { write: (s) => process.stdout.write(s) },
        appendAuditLog: (line) => defaultAppendAuditLog(olamHomeDir(), line),
    };
}
/** Domain-specific error carrying the documented exit code. */
export class RekeyError extends Error {
    exitCode;
    constructor(message, exitCode) {
        super(message);
        this.name = 'RekeyError';
        this.exitCode = exitCode;
    }
}
/**
 * Core rekey logic. Returns the rotation result on success, throws
 * {@link RekeyError} with the documented exit code on failure.
 *
 * The password is held in this function's local binding and the
 * returned object only; it is written to disk with mode 0o600 and
 * NEVER logged or written to the audit trail (SEC-002).
 */
export async function doRekey(worldId, deps) {
    const world = deps.getWorld(worldId);
    if (!world) {
        throw new RekeyError(`world not found: ${worldId}`, 2);
    }
    if (world.worldRoleName === undefined) {
        throw new RekeyError(`world ${worldId} is not hybrid-mode (no per-world role). ` +
            '`olam rekey` only applies to hybrid-mode worlds; use the bridge ' +
            "postgres's `development` user.", 3);
    }
    if (world.worldDbNames === undefined || world.worldDbNames.length === 0) {
        throw new RekeyError('world has no per-world DBs to rotate role on', 3);
    }
    // 1. Rotate. ALTER ROLE inside applyPostgresWorldRole is idempotent —
    //    the same function handles both initial create and rotation.
    const { worldRoleName, password } = deps.applyPostgresWorldRole(worldId, world.worldDbNames);
    // 2. Persist credentials BEFORE restart so the container always has a
    //    valid file to consult if env-injection ever moves to file-mount.
    const rotatedAt = deps.now().toISOString();
    const homeDir = deps.olamHomeDir();
    const worldDir = path.join(homeDir, 'worlds', worldId);
    fs.mkdirSync(worldDir, { recursive: true });
    const credentialsPath = path.join(worldDir, 'credentials.json');
    const payload = {
        worldRoleName,
        password,
        rotatedAt,
    };
    // chmod 600 — owner read/write only. Order matters: create empty file
    // first, fchmod, THEN write. `writeFileSync({ mode })` only applies on
    // initial create, so re-rotations on an existing file keep the old
    // mode if we skip the explicit chmod.
    fs.writeFileSync(credentialsPath, JSON.stringify(payload, null, 2) + '\n', {
        encoding: 'utf-8',
        mode: 0o600,
    });
    fs.chmodSync(credentialsPath, 0o600);
    // 3. Restart the world devbox container so the (env-injected)
    //    password reload happens. Failure here is reported but the
    //    rotation itself is already durable in postgres.
    const restart = deps.restartContainer(devboxContainerName(worldId));
    // Container restart is best-effort; rotation already landed in pg.
    // 4. Audit-log line — NO password. SEC-002 closure.
    deps.appendAuditLog(`${rotatedAt} ${worldId} rekey`);
    if (!restart.ok) {
        // Surface the restart failure in stderr; caller can re-run
        // `docker restart olam-<id>-devbox` manually. Exit code stays 0
        // because the credential rotation succeeded.
        process.stderr.write(`warn: docker restart ${devboxContainerName(worldId)} failed (` +
            `${restart.stderr || 'unknown'}). Run it manually to pick up the new ` +
            'password.\n');
    }
    return {
        worldId,
        worldRoleName,
        password,
        credentialsPath,
        rotatedAt,
        containerRestarted: restart.ok,
    };
}
function emitJson(stdout, result) {
    stdout.write(JSON.stringify(result, null, 2) + '\n');
}
function emitHuman(result, opts) {
    printHeader(`Rotated credentials for ${result.worldId}`);
    printInfo('role', result.worldRoleName);
    printInfo('credentials', result.credentialsPath);
    printInfo('rotated', result.rotatedAt);
    printInfo('container', result.containerRestarted ? 'restarted' : 'restart failed (see stderr)');
    if (opts.print) {
        // Plain stdout (no `printInfo` decorator) so the password can be
        // piped: `olam rekey <id> --print | tail -n1`.
        process.stdout.write(result.password + '\n');
    }
    printSuccess(`Run \`olam enter ${result.worldId}\` to verify the new password works`);
}
export function registerRekey(program) {
    program
        .command('rekey')
        .description('Rotate the per-world postgres password for a hybrid-mode world')
        .argument('<world-id>', 'World ID (e.g. iron-bay-3122)')
        .option('--print', 'Also print the new password to stdout (default: write to file only)')
        .option('--json', 'Emit the rotation result as JSON')
        .action(async (worldId, opts) => {
        const { ctx, error } = await loadContext();
        if (!ctx) {
            printError(error?.message ?? 'Olam is not configured. Run `olam init` first.');
            process.exitCode = 1;
            return;
        }
        const deps = {
            ...buildDefaultDeps(),
            getWorld: (id) => ctx.worldManager.getWorld(id),
        };
        try {
            const result = await doRekey(worldId, deps);
            if (opts.json) {
                emitJson(deps.stdout, result);
            }
            else {
                emitHuman(result, opts);
            }
        }
        catch (err) {
            if (err instanceof RekeyError) {
                printError(err.message);
                process.exitCode = err.exitCode;
                return;
            }
            printError(err instanceof Error ? err.message : String(err));
            process.exitCode = 1;
        }
    });
}
//# sourceMappingURL=rekey.js.map