/**
 * `olam auth upgrade` — rebuild the olam-auth image and force-recreate the container.
 *
 * Steps:
 *   bash build-auth.sh (rebuilds olam-auth:local with latest source + git SHA)
 *   docker stop olam-auth + docker rm olam-auth
 *   olam auth up logic (docker run with correct env vars via AuthContainerController)
 *   wait for /health
 *   smoke test: POST /credentials/add provider=codex must NOT return a Claude URL
 */
import * as fs from 'node:fs';
import * as path from 'node:path';
import { spawnSync } from 'node:child_process';
import ora from 'ora';
import pc from 'picocolors';
import { printError, printSuccess, printInfo, printWarning, printHeader } from '../output.js';
import { readAuthSecret } from './host-cp.js';
import { AuthContainerController } from '@olam/core/src/auth/index.js';
import { loadImageDigests, pullImageWithRetry, realDocker, } from './bootstrap.js';
import { EXIT_BOOTSTRAP_PULL_FAILED, EXIT_GENERIC_ERROR, EXIT_PROTOCOL_MISMATCH, } from '../exit-codes.js';
import { checkProtocolOverlap, parseProtocolVersionsLabel } from '../protocol-version.js';
import { isDevMode, MissingBuildScriptError } from '../install-root.js';
const AUTH_PORT = 9999;
const AUTH_HEALTH_URL = `http://127.0.0.1:${AUTH_PORT}/health`;
const AUTH_ADD_URL = `http://127.0.0.1:${AUTH_PORT}/credentials/add`;
/** Normalise raw Commander option object into typed opts. */
export function parseAuthUpgradeOpts(raw) {
    return {
        yes: raw.yes === true,
        skipRecreate: raw.skipRecreate === true,
        fromSource: raw.fromSource === true,
    };
}
/**
 * Check cwd looks like the olam repo root for auth-service.
 * Verifies packages/auth-service/Dockerfile exists.
 */
export function validateAuthRepoRoot(cwd) {
    const marker = path.join(cwd, 'packages/auth-service/Dockerfile');
    if (!fs.existsSync(marker)) {
        return {
            ok: false,
            error: `Not an olam repo root (expected ${marker}).\n` +
                'Run `olam auth upgrade` from the root of your olam checkout.',
        };
    }
    return { ok: true };
}
/** Block until auth service /health responds OK or timeout expires. */
export async function waitForAuthHealth(timeoutMs = 10_000) {
    const deadline = Date.now() + timeoutMs;
    while (Date.now() < deadline) {
        try {
            const res = await fetch(AUTH_HEALTH_URL, {
                signal: AbortSignal.timeout(2000),
            });
            if (res.ok)
                return true;
        }
        catch {
            // not up yet
        }
        await new Promise((r) => setTimeout(r, 500));
    }
    return false;
}
/**
 * Smoke test: POST /credentials/add with provider=codex.
 *
 * Passes when the response does NOT contain a claude.com/claude.ai loginUrl.
 * A stale (pre-codex) image falls through to the Claude PKCE branch and
 * returns a claude.com URL — this gate catches that.
 */
export async function smokeTestCodexProvider(authSecret) {
    const headers = { 'Content-Type': 'application/json' };
    if (authSecret)
        headers['X-Olam-Secret'] = authSecret;
    let res;
    try {
        res = await fetch(AUTH_ADD_URL, {
            method: 'POST',
            headers,
            body: JSON.stringify({ provider: 'codex', label: '__smoke-test__' }),
            signal: AbortSignal.timeout(5000),
        });
    }
    catch (err) {
        const msg = err instanceof Error ? err.message : 'fetch failed';
        return { ok: false, error: `Smoke test request failed: ${msg}` };
    }
    let body;
    try {
        body = await res.json();
    }
    catch {
        return { ok: false, error: `Smoke test: non-JSON response (status ${res.status})` };
    }
    const b = body;
    // A stale image returns a loginUrl pointing to claude.com.
    const loginUrl = typeof b['loginUrl'] === 'string' ? b['loginUrl'] : '';
    if (/claude\.com|claude\.ai/.test(loginUrl)) {
        return {
            ok: false,
            error: `Smoke test FAILED: provider=codex returned a Claude OAuth URL (${loginUrl}). ` +
                'The deployed image is stale — Codex branch is missing from /app/dist.',
        };
    }
    // Could be a 502 (OpenAI unreachable in CI) — that's not a stale-image failure.
    // We only fail the smoke test on a Claude URL.
    return { ok: true };
}
function runStep(label, cmd, args, opts = {}) {
    const start = Date.now();
    process.stdout.write(`  ${pc.dim(label.padEnd(34))}`);
    const result = spawnSync(cmd, [...args], {
        encoding: 'utf-8',
        stdio: ['ignore', 'pipe', 'pipe'],
        cwd: opts.cwd ?? process.cwd(),
        env: { ...process.env, ...(opts.env ?? {}) },
    });
    const durationMs = Date.now() - start;
    const ok = result.status === 0 && result.error === undefined;
    const dur = `${(durationMs / 1000).toFixed(1)}s`;
    process.stdout.write(`${ok ? pc.green('✓') : pc.red('✗')} ${dur}\n`);
    return {
        ok,
        stdout: result.stdout ?? '',
        stderr: result.stderr ?? '',
        durationMs,
    };
}
async function confirm(message) {
    if (!process.stdin.isTTY)
        return true;
    const { createInterface } = await import('node:readline');
    const rl = createInterface({ input: process.stdin, output: process.stdout });
    return new Promise((resolve) => {
        rl.question(`${message} [y/N] `, (answer) => {
            rl.close();
            resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
        });
    });
}
function printTimings(timings) {
    const total = timings.reduce((s, t) => s + t.durationMs, 0);
    process.stdout.write('\n');
    for (const t of timings) {
        printInfo(t.label, `${(t.durationMs / 1000).toFixed(1)}s`);
    }
    printInfo('total', `${(total / 1000).toFixed(1)}s`);
}
export async function runAuthUpgradePullByDigest(deps = {}) {
    const docker = deps.docker ?? realDocker;
    const digests = deps.digests ?? loadImageDigests();
    const registry = deps.registry ?? digests.$registry ?? 'ghcr.io/pleri';
    printHeader('olam auth upgrade (pull-by-digest)');
    // Step 1 — daemon smoke.
    const infoSpinner = ora('Checking docker daemon').start();
    const info = await docker.info();
    if (info.exitCode !== 0) {
        infoSpinner.fail('docker daemon not reachable');
        process.stderr.write(`${pc.red('error')} docker info exited with ${info.exitCode}.\n` +
            '  Ensure Docker is running, then retry.\n');
        return { exitCode: EXIT_GENERIC_ERROR, summary: 'docker daemon not reachable' };
    }
    infoSpinner.succeed('docker daemon reachable');
    // Step 2 — pull olam-auth by digest.
    const ref = `${registry}/olam-auth@${digests.auth}`;
    const pullSpinner = ora(`Pulling ${ref.slice(0, 60)}…`).start();
    const pullResult = await pullImageWithRetry(ref, docker);
    if (pullResult.exitCode !== 0) {
        pullSpinner.fail('Pull failed');
        process.stderr.write(`${pc.red('error')} pull failed (exit ${pullResult.exitCode}):\n` +
            `  ${pullResult.stderr.split('\n')[0] ?? '(empty)'}\n` +
            '\n  Remedy: re-run after resolving network / GHCR availability.\n' +
            '  For monorepo dev, `OLAM_DEV=1 olam auth upgrade --from-source` builds locally.\n');
        return { exitCode: EXIT_BOOTSTRAP_PULL_FAILED, summary: 'pull failed: auth' };
    }
    pullSpinner.succeed('Image pulled');
    // Step 3 — protocol-version handshake.
    const handshakeSpinner = ora('Verifying olam.protocol.versions handshake').start();
    const inspect = await docker.inspectLabel(ref, 'olam.protocol.versions');
    if (inspect.exitCode !== 0) {
        handshakeSpinner.fail('Could not inspect auth image');
        process.stderr.write(`${pc.red('error')} docker inspect ${ref} failed: ${inspect.stderr}\n`);
        return { exitCode: EXIT_GENERIC_ERROR, summary: 'inspect failed: auth' };
    }
    const labelValue = (inspect.stdout || '').trim();
    const versions = parseProtocolVersionsLabel(labelValue === '<no value>' ? '' : labelValue);
    const decision = checkProtocolOverlap(versions);
    if (!decision.compatible) {
        handshakeSpinner.fail('Protocol mismatch on auth image');
        process.stderr.write(`${pc.red('error')} ${decision.remedy}\n`);
        return { exitCode: EXIT_PROTOCOL_MISMATCH, summary: 'protocol mismatch: auth' };
    }
    handshakeSpinner.succeed('Protocol handshake passed');
    // Step 4 — re-tag pulled-by-digest to olam-auth:local.
    const tagSpinner = ora('Tagging digest → olam-auth:local').start();
    const tagger = deps.tagImpl ?? defaultTagAuth;
    const tagResult = tagger(ref, 'olam-auth:local');
    if (!tagResult.ok) {
        tagSpinner.fail('docker tag failed');
        process.stderr.write(`${pc.red('error')} ${tagResult.error ?? 'docker tag failed'}\n`);
        return { exitCode: EXIT_GENERIC_ERROR, summary: 'tag failed: auth' };
    }
    tagSpinner.succeed('Re-tagged → olam-auth:local');
    // Step 5 — recreate auth-service container.
    const authSpinner = ora('Recreating auth-service container').start();
    const recreate = deps.recreateAuth ?? defaultRecreateAuth;
    const recreateResult = await recreate();
    if (!recreateResult.ok) {
        authSpinner.fail('auth-service recreate failed');
        process.stderr.write(`${pc.red('error')} ${recreateResult.error ?? 'unknown failure'}\n`);
        return { exitCode: EXIT_GENERIC_ERROR, summary: 'auth recreate failed' };
    }
    authSpinner.succeed('auth-service running on new image');
    printSuccess('Auth upgrade complete (pull-by-digest)');
    printInfo('digest', digests.auth.slice(0, 32) + '…');
    return { exitCode: 0, summary: 'auth upgraded' };
}
function defaultTagAuth(from, to) {
    try {
        const r = spawnSync('docker', ['tag', from, to], {
            encoding: 'utf-8',
            stdio: ['ignore', 'ignore', 'pipe'],
        });
        if (r.status === 0 && r.error === undefined)
            return { ok: true };
        return {
            ok: false,
            error: (r.stderr ?? '').trim() || r.error?.message || 'docker tag failed',
        };
    }
    catch (err) {
        return {
            ok: false,
            error: err instanceof Error ? `spawnSync threw: ${err.message}` : 'spawnSync threw',
        };
    }
}
async function defaultRecreateAuth() {
    try {
        spawnSync('docker', ['stop', 'olam-auth'], {
            encoding: 'utf-8',
            stdio: ['ignore', 'ignore', 'ignore'],
        });
        spawnSync('docker', ['rm', 'olam-auth'], {
            encoding: 'utf-8',
            stdio: ['ignore', 'ignore', 'ignore'],
        });
        const controller = new AuthContainerController();
        controller.start();
        // 60s mirrors upgrade.ts's AUTH_HEALTH_TIMEOUT_MS bump — auth's
        // post-recreate cold boot can exceed 15s on first sqlite migration.
        const healthy = await waitForAuthHealth(60_000);
        if (!healthy) {
            return { ok: false, error: 'auth-service /health did not respond within 60s' };
        }
        return { ok: true };
    }
    catch (err) {
        return { ok: false, error: err instanceof Error ? err.message : String(err) };
    }
}
// ── Main handler ──────────────────────────────────────────────────
async function handleAuthUpgrade(opts) {
    const cwd = process.cwd();
    // 1. Verify repo root.
    const rootCheck = validateAuthRepoRoot(cwd);
    if (!rootCheck.ok) {
        printError(rootCheck.error);
        process.exitCode = 1;
        return;
    }
    // 2. Print plan.
    printInfo('olam auth upgrade', '');
    const steps = [
        'bash build-auth.sh (docker image)',
        'stop + remove olam-auth container',
        'docker run olam-auth:local (force-recreate)',
        'wait for /health',
        'smoke test: codex provider → OpenAI URL',
    ];
    if (opts.skipRecreate)
        steps.splice(1, 3, '[skipping container recreate]');
    printInfo('Steps', steps.join(', '));
    process.stdout.write('\n');
    // 3. Confirm unless --yes.
    if (!opts.yes) {
        const proceed = await confirm('Proceed?');
        if (!proceed) {
            printInfo('Aborted', 'no changes made');
            return;
        }
    }
    const timings = [];
    // Step a: build the auth image. Phase B6: resolve the build script via
    // installRoot + 2-condition dev-mode check (OLAM_DEV=1 + monorepo
    // siblings). Outside dev mode, surface the named remedy from
    // MissingBuildScriptError and exit cleanly — published-image upgrades
    // (Phase C's C6) take the no-flag default path.
    let buildScript;
    try {
        const { resolveBuildScript } = await import('../install-root.js');
        buildScript = resolveBuildScript({
            scriptRelPath: 'packages/adapters/src/docker/build-auth.sh',
        });
    }
    catch (err) {
        printError(err instanceof Error ? err.message : String(err));
        process.exitCode = 1;
        return;
    }
    const imageResult = runStep('bash build-auth.sh', 'bash', [buildScript], { cwd });
    timings.push({ label: 'docker image build', durationMs: imageResult.durationMs });
    if (!imageResult.ok) {
        printError(`Docker image build failed:\n${imageResult.stderr}`);
        process.exitCode = 1;
        return;
    }
    if (opts.skipRecreate) {
        process.stdout.write('\n');
        printSuccess('Image rebuilt (--skip-recreate: container not touched)');
        printTimings(timings);
        return;
    }
    // Step b: stop the running container (ignore error if not running).
    const stopStart = Date.now();
    process.stdout.write(`  ${pc.dim('docker stop olam-auth'.padEnd(34))}`);
    spawnSync('docker', ['stop', 'olam-auth'], {
        encoding: 'utf-8',
        stdio: ['ignore', 'pipe', 'pipe'],
    });
    const stopDurationMs = Date.now() - stopStart;
    process.stdout.write(`${pc.green('✓')} ${(stopDurationMs / 1000).toFixed(1)}s\n`);
    timings.push({ label: 'container stop', durationMs: stopDurationMs });
    // Step c: remove the container so force-recreate with new image is clean.
    const rmStart = Date.now();
    process.stdout.write(`  ${pc.dim('docker rm olam-auth'.padEnd(34))}`);
    spawnSync('docker', ['rm', 'olam-auth'], {
        encoding: 'utf-8',
        stdio: ['ignore', 'pipe', 'pipe'],
    });
    const rmDurationMs = Date.now() - rmStart;
    process.stdout.write(`${pc.green('✓')} ${(rmDurationMs / 1000).toFixed(1)}s\n`);
    timings.push({ label: 'container remove', durationMs: rmDurationMs });
    // Step d: start the new container via AuthContainerController (handles secret injection).
    const startStart = Date.now();
    process.stdout.write(`  ${pc.dim('docker run olam-auth:local'.padEnd(34))}`);
    try {
        const controller = new AuthContainerController();
        controller.start();
        const startDurationMs = Date.now() - startStart;
        process.stdout.write(`${pc.green('✓')} ${(startDurationMs / 1000).toFixed(1)}s\n`);
        timings.push({ label: 'container start', durationMs: startDurationMs });
    }
    catch (err) {
        const startDurationMs = Date.now() - startStart;
        process.stdout.write(`${pc.red('✗')} ${(startDurationMs / 1000).toFixed(1)}s\n`);
        timings.push({ label: 'container start', durationMs: startDurationMs });
        printError(`Failed to start auth container: ${err instanceof Error ? err.message : String(err)}`);
        process.exitCode = 1;
        return;
    }
    // Step e: wait for /health.
    process.stdout.write(`  ${pc.dim('waiting for /health'.padEnd(34))}`);
    const healthStart = Date.now();
    const healthy = await waitForAuthHealth(10_000);
    const healthDurationMs = Date.now() - healthStart;
    const healthDur = `${(healthDurationMs / 1000).toFixed(1)}s`;
    process.stdout.write(`${healthy ? pc.green('✓') : pc.yellow('?')} ${healthDur}\n`);
    timings.push({ label: '/health', durationMs: healthDurationMs });
    if (!healthy) {
        printWarning('Auth service started but /health did not respond within 10s. Check: docker logs olam-auth');
        process.exitCode = 1;
        printTimings(timings);
        return;
    }
    // Step f: smoke test — codex provider must not return a Claude URL.
    process.stdout.write(`  ${pc.dim('smoke test: codex provider'.padEnd(34))}`);
    const smokeStart = Date.now();
    const authSecret = readAuthSecret();
    const smoke = await smokeTestCodexProvider(authSecret);
    const smokeDurationMs = Date.now() - smokeStart;
    const smokeDur = `${(smokeDurationMs / 1000).toFixed(1)}s`;
    process.stdout.write(`${smoke.ok ? pc.green('✓') : pc.red('✗')} ${smokeDur}\n`);
    timings.push({ label: 'smoke test', durationMs: smokeDurationMs });
    if (!smoke.ok) {
        printError(smoke.error);
        process.exitCode = 1;
        printTimings(timings);
        return;
    }
    // 4. Summary.
    process.stdout.write('\n');
    printSuccess('Auth upgrade complete');
    printInfo('Next', 'olam auth login --label primary (if needed)');
    printTimings(timings);
}
// ── Register ──────────────────────────────────────────────────────
export function registerAuthUpgrade(auth) {
    auth
        .command('upgrade')
        .description('Upgrade the olam-auth container. Default: pull olam-auth@<digest> from ghcr.io and recreate. ' +
        'Use --from-source to rebuild from monorepo source (requires OLAM_DEV=1).')
        .option('-y, --yes', 'Skip the confirmation prompt')
        .option('--skip-recreate', 'Build the image only; skip container stop/rm/start (only with --from-source)')
        .option('--from-source', 'Build olam-auth from monorepo source (legacy path).\n' +
        '                              Requires OLAM_DEV=1 + a `packages/` sibling at the install root.')
        .action(async (opts) => {
        const parsed = parseAuthUpgradeOpts(opts);
        if (parsed.fromSource) {
            if (!isDevMode()) {
                printError(new MissingBuildScriptError('packages/adapters/src/docker/build-auth.sh').message);
                process.exitCode = 1;
                return;
            }
            await handleAuthUpgrade(parsed);
            return;
        }
        try {
            const result = await runAuthUpgradePullByDigest();
            process.exitCode = result.exitCode;
        }
        catch (err) {
            printError(err instanceof Error ? err.message : String(err));
            process.exitCode = EXIT_GENERIC_ERROR;
        }
    });
}
//# sourceMappingURL=auth-upgrade.js.map