/**
 * `olam services` — umbrella verb for managing Olam service containers.
 *
 * Subcommands:
 *   olam services up              — start all service containers (idempotent)
 *   olam services down            — stop all service containers
 *   olam services status          — show state of all service containers
 *   olam services restart <name>  — restart a named service
 *
 * Substrate-aware (D1 — Phase 2 Phase D):
 *   compose    → docker-based lifecycle (AuthContainerController / McpAuth / KgService)
 *   kubernetes → kubectl scale / rollout restart / get deployment
 *
 * `olam auth up/down/status` delegates here with a deprecation warning.
 */
import { execFileSync, spawnSync } from 'node:child_process';
import pc from 'picocolors';
import { AuthContainerController } from '@olam/core/src/auth/index.js';
import { printError, printHeader, printInfo, printSuccess, printWarning } from '../output.js';
import { KG_SERVICE_HEALTH_TIMEOUT_MS, KG_SERVICE_PORT, KgServiceContainerController, } from './kg-service-container.js';
import { MEMORY_SERVICE_HEALTH_TIMEOUT_MS, MEMORY_SERVICE_PORT, MemoryServiceContainerController, } from './memory-service-container.js';
import { readConfig } from '../lib/config.js';
import { kubectlWrap } from '../lib/kubectl-wrap.js';
import { PERIPHERALS } from '../lib/peripheral-registry.js';
import { resolveKubectlContext } from '../lib/kubectl-context.js';
// ── McpAuthContainerController ────────────────────────────────────
//
// Mirrors AuthContainerController (packages/core/src/auth/container.ts).
// mcp-auth-service is not a @olam/core package, so the controller lives
// here in the CLI layer.
const MCP_AUTH_PORT = 9998;
const MCP_AUTH_VOLUME = 'olam-mcp-auth-data';
const MCP_AUTH_CONTAINER = 'olam-mcp-auth';
const MCP_AUTH_LOCAL_TAG = 'olam-mcp-auth:local';
const MCP_AUTH_PUBLISHED_TAG = 'ghcr.io/pleri/olam-mcp-auth:latest';
const MCP_AUTH_HEALTH_URL = `http://127.0.0.1:${MCP_AUTH_PORT}/health`;
// Cold-bootstrap CI runners (no Docker layer cache, slower disk I/O)
// need more headroom than 15s. The previous default tripped on every
// D1/D2/D3 CI run with a silent "stderr: " (empty) — bumped to 60s
// after run 25591650126 showed /health taking ~16s on a fresh
// bootstrap. Operator-facing local boots are still ~3-5s; the larger
// cap only matters when something is genuinely wrong, in which case
// the timeout-then-dump-logs branch (added below) surfaces the cause.
const MCP_AUTH_HEALTH_TIMEOUT_MS = 60_000;
export class McpAuthContainerController {
    imageTag = MCP_AUTH_LOCAL_TAG;
    status() {
        const r = spawnSync('docker', ['inspect', '--format', '{{.State.Status}}|{{.Id}}', MCP_AUTH_CONTAINER], { encoding: 'utf-8' });
        if (r.status === 0) {
            const [stateRaw, id] = r.stdout.trim().split('|');
            return {
                state: stateRaw === 'running' ? 'running' : 'stopped',
                port: MCP_AUTH_PORT,
                containerId: id,
            };
        }
        return { state: 'missing', port: MCP_AUTH_PORT };
    }
    imageExists(tag = this.imageTag) {
        return spawnSync('docker', ['image', 'inspect', tag], { encoding: 'utf-8' }).status === 0;
    }
    start() {
        const current = this.status();
        if (current.state === 'running')
            return;
        if (current.state === 'stopped') {
            execFileSync('docker', ['start', MCP_AUTH_CONTAINER], { stdio: 'pipe' });
            return;
        }
        if (!this.imageExists()) {
            if (this.imageTag !== MCP_AUTH_PUBLISHED_TAG && this.imageExists(MCP_AUTH_PUBLISHED_TAG)) {
                this.imageTag = MCP_AUTH_PUBLISHED_TAG;
            }
            else {
                throw new Error(`mcp-auth image '${this.imageTag}' not found locally. ` +
                    'Run `olam bootstrap` to pull the published image.');
            }
        }
        execFileSync('docker', [
            'run',
            '--detach',
            '--name', MCP_AUTH_CONTAINER,
            '--restart', 'unless-stopped',
            '--publish', `${MCP_AUTH_PORT}:${MCP_AUTH_PORT}`,
            '--volume', `${MCP_AUTH_VOLUME}:/mcp-auth-data`,
            this.imageTag,
        ], { stdio: 'pipe' });
    }
    stop() {
        const current = this.status();
        if (current.state !== 'running')
            return;
        execFileSync('docker', ['stop', MCP_AUTH_CONTAINER], { stdio: 'pipe' });
    }
    remove() {
        spawnSync('docker', ['rm', '-f', MCP_AUTH_CONTAINER], { stdio: 'pipe' });
    }
    async waitForReady(timeoutMs = MCP_AUTH_HEALTH_TIMEOUT_MS) {
        const deadline = Date.now() + timeoutMs;
        while (Date.now() < deadline) {
            try {
                const res = await fetch(MCP_AUTH_HEALTH_URL, { signal: AbortSignal.timeout(1_500) });
                if (res.ok)
                    return true;
            }
            catch {
                // not ready yet
            }
            await sleep(500);
        }
        return false;
    }
}
function sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
}
// ── Kubernetes context resolution (D1 — SEC-NEW-003) ─────────────
const K8S_NAMESPACE = 'olam';
/** Emits actionable error and returns null when context is missing (SEC-NEW-003).
 *  Uses the shared resolver in `kubectl-context.ts` so the policy lives in one place. */
function assertK8sContext(configPath) {
    const resolved = resolveKubectlContext(configPath);
    if (resolved.error !== undefined || resolved.context === undefined) {
        printError((resolved.error ?? 'kubectl context missing') +
            '\n  See docs/architecture/peripheral-services-on-k3s.md for setup instructions.');
        return null;
    }
    if (resolved.deprecationWarning !== undefined) {
        printWarning(resolved.deprecationWarning);
    }
    return { context: resolved.context };
}
/**
 * `olam services up` on kubernetes substrate.
 * Scales each peripheral deployment to replicas=1.
 * Applies the manifest if the Deployment is missing (not found).
 */
export async function servicesUpKubernetes(deps = {}) {
    const ctx = assertK8sContext(deps.configPath);
    if (!ctx)
        return { exitCode: 1 };
    const wrap = deps.kubectlWrapImpl ?? kubectlWrap;
    const { context } = ctx;
    printHeader('Services up (kubernetes)');
    let exitCode = 0;
    for (const peripheral of PERIPHERALS) {
        const deploymentName = normalizeK8sDeploymentName(peripheral.name);
        const result = await wrap([
            '--context', context,
            'scale', `deployment/${deploymentName}`,
            '--replicas=1',
            '-n', K8S_NAMESPACE,
        ], { timeout: 30_000 });
        if (!result.ok) {
            printWarning(`${deploymentName}: scale failed — ${result.stderr.split('\n')[0] ?? ''} (try olam upgrade to apply manifests)`);
            exitCode = 1;
        }
        else {
            printSuccess(`${deploymentName}: scaled to 1 replica`);
        }
    }
    return { exitCode };
}
/**
 * `olam services down` on kubernetes substrate.
 * Scales each peripheral deployment to replicas=0.
 */
export async function servicesDownKubernetes(deps = {}) {
    const ctx = assertK8sContext(deps.configPath);
    if (!ctx)
        return { exitCode: 1 };
    const wrap = deps.kubectlWrapImpl ?? kubectlWrap;
    const { context } = ctx;
    let exitCode = 0;
    for (const peripheral of PERIPHERALS) {
        const deploymentName = normalizeK8sDeploymentName(peripheral.name);
        const result = await wrap([
            '--context', context,
            'scale', `deployment/${deploymentName}`,
            '--replicas=0',
            '-n', K8S_NAMESPACE,
        ], { timeout: 30_000 });
        if (!result.ok) {
            printError(`${deploymentName}: scale down failed — ${result.stderr.split('\n')[0] ?? ''}`);
            exitCode = 1;
        }
        else {
            printSuccess(`${deploymentName}: scaled to 0 replicas`);
        }
    }
    return { exitCode };
}
/**
 * `olam services status` on kubernetes substrate.
 * Fetches `kubectl get deployment -n olam -o wide` and formats it to match
 * the compose table shape: NAME | STATUS | IMAGE | AGE | RESTARTS.
 */
export async function servicesStatusKubernetes(deps = {}) {
    const ctx = assertK8sContext(deps.configPath);
    if (!ctx)
        return;
    const wrap = deps.kubectlWrapImpl ?? kubectlWrap;
    const { context } = ctx;
    printHeader('Services status (kubernetes)');
    const result = await wrap([
        '--context', context,
        'get', 'deployment',
        '-n', K8S_NAMESPACE,
        '-o', 'wide',
    ], { timeout: 30_000 });
    if (!result.ok) {
        printError(`kubectl get deployment failed: ${result.stderr.split('\n')[0] ?? ''}`);
        return;
    }
    // Parse kubectl output and reformat to match compose table shape.
    // kubectl -o wide columns: NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR
    // We emit: NAME | STATUS | IMAGE | AGE | RESTARTS
    const lines = result.stdout.trim().split('\n');
    const header = `  ${'NAME'.padEnd(24)}  ${'STATUS'.padEnd(10)}  ${'IMAGE'.padEnd(44)}  ${'AGE'.padEnd(8)}  RESTARTS`;
    const divider = '  ' + '─'.repeat(104);
    printInfo('', header);
    printInfo('', divider);
    for (const line of lines.slice(1)) {
        const cols = line.trim().split(/\s+/);
        const name = cols[0] ?? '';
        const ready = cols[1] ?? '0/0'; // e.g. "1/1"
        const available = cols[3] ?? '0';
        const age = cols[4] ?? '?';
        const image = cols[6] ?? '(unknown)';
        // Short image tag: strip registry prefix for readability.
        const shortImage = image.split('/').pop() ?? image;
        const status = available === ready.split('/')[1] ? 'running' : 'degraded';
        const statusStr = status === 'running' ? pc.green(status) : pc.yellow(status);
        printInfo('', `  ${name.padEnd(24)}  ${statusStr.padEnd(10 + (status === 'running' ? pc.green('').length : pc.yellow('').length))}  ${shortImage.padEnd(44)}  ${age.padEnd(8)}  —`);
    }
}
/**
 * Normalize a user-supplied service name to the k8s Deployment name form.
 *
 * All olam peripheral Deployments are named `olam-<short-name>` in the k8s
 * manifests (e.g. `olam-kg-service`, `olam-auth-service`). When the operator
 * types `olam services restart kg-service` the CLI must prepend `olam-` before
 * calling `kubectl rollout restart deployment/olam-kg-service`.
 *
 * Idempotent: if the user already types the full name (`olam-kg-service`), the
 * prefix is NOT doubled → `olam-kg-service` (NOT `olam-olam-kg-service`).
 *
 * Exported for unit testing (C1 — R3-D).
 */
export function normalizeK8sDeploymentName(name) {
    if (name.startsWith('olam-'))
        return name;
    return `olam-${name}`;
}
/**
 * `olam services restart <name>` on kubernetes substrate.
 * Uses `kubectl rollout restart deployment/olam-<name>`.
 * Prepends `olam-` to the user-supplied name (idempotent — no double-prefix).
 */
export async function servicesRestartKubernetes(name, deps = {}) {
    const ctx = assertK8sContext(deps.configPath);
    if (!ctx)
        return { exitCode: 1 };
    const wrap = deps.kubectlWrapImpl ?? kubectlWrap;
    const { context } = ctx;
    const deploymentName = normalizeK8sDeploymentName(name);
    const result = await wrap([
        '--context', context,
        'rollout', 'restart',
        `deployment/${deploymentName}`,
        '-n', K8S_NAMESPACE,
    ], { timeout: 30_000 });
    if (!result.ok) {
        printError(`rollout restart ${deploymentName} failed: ${result.stderr.split('\n')[0] ?? ''}`);
        return { exitCode: 1 };
    }
    printSuccess(`${deploymentName}: rollout restart triggered`);
    return { exitCode: 0 };
}
// ── Services orchestration ────────────────────────────────────────
/** Start both auth containers, idempotent. */
/**
 * Dump the last N lines of `docker logs <container>` to stderr so the
 * operator can see why /health didn't respond. Pre-fix, the timeout
 * branch printed only "started but /health did not respond within Ns"
 * and the operator had no way to know whether the container had
 * crashed, was stuck on an env var, or was just genuinely slow.
 */
function dumpContainerLogs(container, tail = 40) {
    try {
        const result = spawnSync('docker', ['logs', '--tail', String(tail), container], {
            encoding: 'utf-8',
            stdio: ['ignore', 'pipe', 'pipe'],
        });
        const out = (result.stdout ?? '') + (result.stderr ?? '');
        if (out.trim().length > 0) {
            process.stderr.write(`\n  --- last ${tail} lines from \`docker logs ${container}\` ---\n`);
            for (const line of out.split('\n').slice(-tail)) {
                process.stderr.write(`  ${line}\n`);
            }
        }
    }
    catch { /* best effort */ }
}
export async function servicesUp() {
    const auth = new AuthContainerController();
    const mcpAuth = new McpAuthContainerController();
    const kgService = new KgServiceContainerController();
    const memoryService = new MemoryServiceContainerController();
    // Start olam-auth
    const authStatus = auth.status();
    if (authStatus.state === 'running') {
        printSuccess(`olam-auth already running on :${authStatus.port}`);
    }
    else {
        try {
            auth.start();
        }
        catch (err) {
            printError(`olam-auth failed to start: ${err instanceof Error ? err.message : String(err)}`);
            return { exitCode: 1 };
        }
        const ready = await auth.waitForReady(MCP_AUTH_HEALTH_TIMEOUT_MS);
        if (!ready) {
            printError(`olam-auth started but /health did not respond within ${MCP_AUTH_HEALTH_TIMEOUT_MS / 1000}s.`);
            dumpContainerLogs('olam-auth');
            return { exitCode: 1 };
        }
        printSuccess('olam-auth started');
    }
    // Start olam-mcp-auth
    const mcpStatus = mcpAuth.status();
    if (mcpStatus.state === 'running') {
        printSuccess(`olam-mcp-auth already running on :${mcpStatus.port}`);
    }
    else {
        try {
            mcpAuth.start();
        }
        catch (err) {
            printError(`olam-mcp-auth failed to start: ${err instanceof Error ? err.message : String(err)}`);
            return { exitCode: 1 };
        }
        const ready = await mcpAuth.waitForReady();
        if (!ready) {
            printError(`olam-mcp-auth started but /health did not respond within ${MCP_AUTH_HEALTH_TIMEOUT_MS / 1000}s.`);
            dumpContainerLogs('olam-mcp-auth');
            return { exitCode: 1 };
        }
        printSuccess('olam-mcp-auth started');
    }
    // Start olam-kg-service
    const kgStatus = kgService.status();
    if (kgStatus.state === 'running') {
        printSuccess(`olam-kg-service already running on :${kgStatus.port}`);
    }
    else if (kgService.imageMissingEverywhere()) {
        // Operator opted out via `olam bootstrap --skip-kg-service`, OR the
        // release predates kg-service. Either way: silent skip with an info
        // hint rather than a warning. Repeating a warning on every services-up
        // for an intentional opt-out is noise.
        printInfo('olam-kg-service', 'skipped (no image; run `olam bootstrap` to pull)');
        printHeader('Services up');
        printInfo('olam-auth', ':9999');
        printInfo('olam-mcp-auth', ':9998');
        return { exitCode: 0 };
    }
    else {
        try {
            kgService.start();
        }
        catch (err) {
            // kg-service is non-fatal — image exists but start failed (port
            // conflict, mount permission, etc.). Surface the docker stderr so
            // operator can diagnose.
            const errMsg = err instanceof Error ? err.message : String(err);
            const stderr = err instanceof Error && 'stderr' in err
                ? String(err.stderr ?? '').trim()
                : '';
            printWarning(`olam-kg-service not started: ${errMsg}${stderr ? `\n  stderr: ${stderr}` : ''}`);
            printHeader('Services up');
            printInfo('olam-auth', ':9999');
            printInfo('olam-mcp-auth', ':9998');
            return { exitCode: 0 };
        }
        const ready = await kgService.waitForReady();
        if (!ready) {
            printError(`olam-kg-service started but /health did not respond within ${KG_SERVICE_HEALTH_TIMEOUT_MS / 1000}s.`);
            dumpContainerLogs('olam-kg-service');
            return { exitCode: 1 };
        }
        // Pre-warm — eliminates first-call cold cliff per Phase A latency bench.
        await kgService.prewarm();
        printSuccess('olam-kg-service started + pre-warmed');
    }
    // Start olam-memory-service (memory-service-as-docker-peripheral Phase A4).
    // Same opted-out vs broken pattern as kg-service.
    const memoryStatus = memoryService.status();
    if (memoryStatus.state === 'running') {
        printSuccess(`olam-memory-service already running on :${memoryStatus.port}`);
    }
    else if (memoryService.imageMissingEverywhere()) {
        printInfo('olam-memory-service', 'skipped (no image; run `olam bootstrap` to pull)');
    }
    else {
        try {
            memoryService.start();
        }
        catch (err) {
            const errMsg = err instanceof Error ? err.message : String(err);
            const stderr = err instanceof Error && 'stderr' in err
                ? String(err.stderr ?? '').trim()
                : '';
            printWarning(`olam-memory-service not started: ${errMsg}${stderr ? `\n  stderr: ${stderr}` : ''}`);
            // Non-fatal — memory-service is optional during Phase A coexistence with
            // the legacy host-process model. Operators on the host-process path
            // continue working.
            printHeader('Services up');
            printInfo('olam-auth', ':9999');
            printInfo('olam-mcp-auth', ':9998');
            printInfo('olam-kg-service', `:${KG_SERVICE_PORT}`);
            return { exitCode: 0 };
        }
        const ready = await memoryService.waitForReady();
        if (!ready) {
            printError(`olam-memory-service started but /agentmemory/livez did not respond within ${MEMORY_SERVICE_HEALTH_TIMEOUT_MS / 1000}s.`);
            dumpContainerLogs('olam-memory-service');
            return { exitCode: 1 };
        }
        printSuccess('olam-memory-service started');
    }
    printHeader('Services up');
    printInfo('olam-auth', ':9999');
    printInfo('olam-mcp-auth', ':9998');
    printInfo('olam-kg-service', `:${KG_SERVICE_PORT}`);
    printInfo('olam-memory-service', `:${MEMORY_SERVICE_PORT}`);
    return { exitCode: 0 };
}
/** Stop all service containers. */
export function servicesDown() {
    const auth = new AuthContainerController();
    const mcpAuth = new McpAuthContainerController();
    const kgService = new KgServiceContainerController();
    const memoryService = new MemoryServiceContainerController();
    let exitCode = 0;
    try {
        auth.stop();
        printSuccess('olam-auth stopped.');
    }
    catch (err) {
        printError(`olam-auth: ${err instanceof Error ? err.message : String(err)}`);
        exitCode = 1;
    }
    try {
        mcpAuth.stop();
        printSuccess('olam-mcp-auth stopped.');
    }
    catch (err) {
        printError(`olam-mcp-auth: ${err instanceof Error ? err.message : String(err)}`);
        exitCode = 1;
    }
    try {
        kgService.stop();
        printSuccess('olam-kg-service stopped.');
    }
    catch (err) {
        printError(`olam-kg-service: ${err instanceof Error ? err.message : String(err)}`);
        exitCode = 1;
    }
    try {
        memoryService.stop();
        printSuccess('olam-memory-service stopped.');
    }
    catch (err) {
        printError(`olam-memory-service: ${err instanceof Error ? err.message : String(err)}`);
        exitCode = 1;
    }
    return { exitCode };
}
/** Show container state for all services. */
export function servicesStatus() {
    const auth = new AuthContainerController();
    const mcpAuth = new McpAuthContainerController();
    const kgService = new KgServiceContainerController();
    const memoryService = new MemoryServiceContainerController();
    printHeader('Services status');
    const authState = auth.status();
    const authStateStr = authState.state === 'running'
        ? pc.green('running')
        : authState.state === 'stopped'
            ? pc.yellow('stopped')
            : pc.red('missing');
    printInfo('olam-auth', `${authStateStr}  :${authState.port}`);
    const mcpState = mcpAuth.status();
    const mcpStateStr = mcpState.state === 'running'
        ? pc.green('running')
        : mcpState.state === 'stopped'
            ? pc.yellow('stopped')
            : pc.red('missing');
    printInfo('olam-mcp-auth', `${mcpStateStr}  :${mcpState.port}`);
    const kgState = kgService.status();
    const kgStateStr = kgState.state === 'running'
        ? pc.green('running')
        : kgState.state === 'stopped'
            ? pc.yellow('stopped')
            : pc.red('missing');
    printInfo('olam-kg-service', `${kgStateStr}  :${kgState.port}`);
    const memState = memoryService.status();
    const memStateStr = memState.state === 'running'
        ? pc.green('running')
        : memState.state === 'stopped'
            ? pc.yellow('stopped')
            : pc.red('missing');
    printInfo('olam-memory-service', `${memStateStr}  :${memState.port}`);
}
// ── Commander registration ────────────────────────────────────────
export function registerServices(program) {
    const services = program
        .command('services')
        .description('Manage Olam service containers. Substrate-aware: compose uses docker; kubernetes uses kubectl.');
    services
        .command('up')
        .description('Start all service containers (idempotent)')
        .action(async () => {
        const cfg = readConfig();
        if (cfg.host.substrate === 'kubernetes') {
            const result = await servicesUpKubernetes();
            if (result.exitCode !== 0)
                process.exitCode = result.exitCode;
        }
        else {
            const result = await servicesUp();
            if (result.exitCode !== 0)
                process.exitCode = result.exitCode;
        }
    });
    services
        .command('down')
        .description('Stop all service containers')
        .action(async () => {
        const cfg = readConfig();
        if (cfg.host.substrate === 'kubernetes') {
            const result = await servicesDownKubernetes();
            if (result.exitCode !== 0)
                process.exitCode = result.exitCode;
        }
        else {
            const result = servicesDown();
            if (result.exitCode !== 0)
                process.exitCode = result.exitCode;
        }
    });
    services
        .command('status')
        .description('Show state of all service containers')
        .action(async () => {
        const cfg = readConfig();
        if (cfg.host.substrate === 'kubernetes') {
            await servicesStatusKubernetes();
        }
        else {
            servicesStatus();
        }
    });
    services
        .command('restart <name>')
        .description('Restart a named service (kubernetes: kubectl rollout restart; compose: docker compose restart)')
        .action(async (name) => {
        const cfg = readConfig();
        if (cfg.host.substrate === 'kubernetes') {
            const result = await servicesRestartKubernetes(name);
            if (result.exitCode !== 0)
                process.exitCode = result.exitCode;
        }
        else {
            // Compose restart — delegate to docker compose restart <name>.
            const r = spawnSync('docker', ['compose', 'restart', name], { stdio: 'inherit' });
            if (r.status !== 0)
                process.exitCode = r.status ?? 1;
        }
    });
}
//# sourceMappingURL=services.js.map