/**
 * olam kg status [<workspace>] — operator-facing report for KG storage.
 *
 * Phase C1 of olam-kg-service (plan ~/.claude/plans/olam-kg-service.md).
 *
 * Two modes:
 *   - `olam kg status` — enumerate every pristine + total overlay count.
 *     Compact table with built_at / node_count / size / world_count.
 *   - `olam kg status <workspace>` — same workspace's pristine plus a
 *     per-world overlay table (world_id / overlay-graphify-out size /
 *     overlay graph.json node_count).
 *
 * Storage layout (per Phase A2 + B1):
 *   - Pristines:  `~/.olam/kg/<workspace>/graphify-out/`  + `freshness.json`
 *   - Overlays:   `~/.olam/worlds/<world-id>/<clone-dir>/graphify-out/`
 *
 * Cumulative-storage policy (per tracker T1):
 *   - >= 1.5GB  → soft warn on stderr (operator decides whether to prune)
 *   - >= 5.0GB  → hard error on stderr + non-zero exit
 *
 * Risk T7 mitigation: workspace name (when passed) regex-validated via
 * @olam/core's `validateWorkspaceName`. All resolved paths asserted to
 * live under `~/.olam/` (via `kgPristinePath` / explicit prefix check).
 *
 * Side effects: read-only (no FS writes; no subprocess spawns).
 */
import fs from 'node:fs';
import { homedir } from 'node:os';
import path from 'node:path';
import { kgPristinePath } from '@olam/core/src/kg/storage-paths.js';
import { validateWorkspaceName } from '@olam/core/src/world/workspace-name.js';
import { SOFT_CAP_BYTES, HARD_CAP_BYTES } from '../lib/kg-caps.js';
import { printError, printWarning } from '../output.js';
function olamHome() {
    return process.env.OLAM_HOME ?? path.join(homedir(), '.olam');
}
function kgRoot() {
    return path.join(olamHome(), 'kg');
}
function worldsRoot() {
    return path.join(olamHome(), 'worlds');
}
/**
 * Recursive directory size in bytes. Symlinks NOT followed (lstat).
 * Missing directory → 0 (callers handle via empty-state messaging).
 *
 * Pure read; no FS mutation. Cheap enough for KG-sized trees (graphify-out
 * is typically ~10-200MB per workspace); not optimised for trees with
 * millions of files.
 */
function dirSizeBytes(dir) {
    if (!fs.existsSync(dir))
        return 0;
    let total = 0;
    const stack = [dir];
    while (stack.length > 0) {
        const cur = stack.pop();
        let entries;
        try {
            entries = fs.readdirSync(cur, { withFileTypes: true });
        }
        catch {
            continue; // permission denied / race; skip
        }
        for (const entry of entries) {
            const full = path.join(cur, entry.name);
            if (entry.isSymbolicLink())
                continue;
            if (entry.isDirectory()) {
                stack.push(full);
                continue;
            }
            try {
                total += fs.statSync(full).size;
            }
            catch {
                // file disappeared between readdir + stat; skip
            }
        }
    }
    return total;
}
function formatBytes(n) {
    if (n < 1024)
        return `${n} B`;
    if (n < 1024 * 1024)
        return `${(n / 1024).toFixed(1)} KB`;
    if (n < 1024 * 1024 * 1024)
        return `${(n / 1024 / 1024).toFixed(1)} MB`;
    return `${(n / 1024 / 1024 / 1024).toFixed(2)} GB`;
}
function readFreshness(workspace) {
    const file = path.join(kgPristinePath(workspace), 'freshness.json');
    if (!fs.existsSync(file))
        return null;
    try {
        const raw = JSON.parse(fs.readFileSync(file, 'utf-8'));
        if (raw && typeof raw === 'object')
            return raw;
        return null;
    }
    catch {
        return null;
    }
}
/**
 * Read node_count directly from an overlay's graph.json. Returns null
 * when graph.json is missing OR malformed — defensive: the report should
 * still render even if one overlay is broken.
 */
function readOverlayNodeCount(graphifyOutDir) {
    const graphPath = path.join(graphifyOutDir, 'graph.json');
    if (!fs.existsSync(graphPath))
        return null;
    try {
        const raw = JSON.parse(fs.readFileSync(graphPath, 'utf-8'));
        if (raw && typeof raw === 'object') {
            const nodes = raw.nodes;
            if (Array.isArray(nodes))
                return nodes.length;
        }
        return null;
    }
    catch {
        return null;
    }
}
/**
 * Enumerate every world's overlay graphify-out directory.
 *
 * Layout: `~/.olam/worlds/<world-id>/<clone-dir>/graphify-out/`. Each
 * world MAY have multiple clones (multi-repo manifests; phase C of
 * olam-3-org-fatbox-runtime). We surface ALL of them.
 */
function listOverlays() {
    const root = worldsRoot();
    if (!fs.existsSync(root))
        return [];
    const records = [];
    let worldDirs;
    try {
        worldDirs = fs.readdirSync(root, { withFileTypes: true });
    }
    catch {
        return [];
    }
    for (const worldEntry of worldDirs) {
        if (!worldEntry.isDirectory())
            continue;
        const worldId = worldEntry.name;
        const worldDir = path.join(root, worldId);
        let cloneDirs;
        try {
            cloneDirs = fs.readdirSync(worldDir, { withFileTypes: true });
        }
        catch {
            continue;
        }
        for (const cloneEntry of cloneDirs) {
            if (!cloneEntry.isDirectory())
                continue;
            const graphifyOut = path.join(worldDir, cloneEntry.name, 'graphify-out');
            if (!fs.existsSync(graphifyOut))
                continue;
            records.push({
                world_id: worldId,
                clone_dir: cloneEntry.name,
                graphify_out_path: graphifyOut,
                graphify_out_size_bytes: dirSizeBytes(graphifyOut),
                node_count: readOverlayNodeCount(graphifyOut),
            });
        }
    }
    return records;
}
/**
 * Collect every pristine workspace KG. Cheap read: re-uses the freshness.json
 * shape committed in A4. Workspace-to-world join is by clone-dir-name; the
 * pristine record exposes `world_count` so the operator can spot orphans
 * (overlays without a pristine, or pristines with zero worlds).
 */
function listPristines(overlays) {
    const root = kgRoot();
    if (!fs.existsSync(root))
        return [];
    const records = [];
    let entries;
    try {
        entries = fs.readdirSync(root, { withFileTypes: true });
    }
    catch {
        return [];
    }
    for (const entry of entries) {
        if (!entry.isDirectory())
            continue;
        const workspace = entry.name;
        // Defensive: a non-conformant directory name shouldn't crash status.
        try {
            validateWorkspaceName(workspace);
        }
        catch {
            continue;
        }
        const fresh = readFreshness(workspace);
        const graphifyOut = path.join(kgPristinePath(workspace), 'graphify-out');
        const size = dirSizeBytes(graphifyOut);
        const worldCount = overlays.filter((o) => o.clone_dir === workspace).length;
        records.push({
            workspace,
            built_at: fresh?.built_at ?? null,
            node_count: fresh?.node_count ?? null,
            graphify_version: fresh?.graphify_version ?? null,
            graphify_out_size_bytes: size,
            world_count: worldCount,
        });
    }
    return records.sort((a, b) => a.workspace.localeCompare(b.workspace));
}
function classifyCap(totalBytes) {
    if (totalBytes >= HARD_CAP_BYTES)
        return 'hard-critical';
    if (totalBytes >= SOFT_CAP_BYTES)
        return 'soft-warn';
    return 'ok';
}
function buildReport(workspaceFilter) {
    if (workspaceFilter)
        validateWorkspaceName(workspaceFilter);
    const allOverlays = listOverlays();
    const allPristines = listPristines(allOverlays);
    const pristines = workspaceFilter
        ? allPristines.filter((p) => p.workspace === workspaceFilter)
        : allPristines;
    const overlays = workspaceFilter
        ? allOverlays.filter((o) => o.clone_dir === workspaceFilter)
        : allOverlays;
    const totalBytes = pristines.reduce((acc, p) => acc + p.graphify_out_size_bytes, 0) +
        overlays.reduce((acc, o) => acc + o.graphify_out_size_bytes, 0);
    return {
        pristines,
        overlays,
        total_bytes: totalBytes,
        cap_status: classifyCap(totalBytes),
    };
}
function renderHuman(report, workspaceFilter) {
    const lines = [];
    if (report.pristines.length === 0 && report.overlays.length === 0) {
        if (workspaceFilter) {
            lines.push(`No KG state for workspace "${workspaceFilter}".`);
        }
        else {
            lines.push('No KG state on this host yet. Run `olam kg build` from a workspace clone.');
        }
        return lines.join('\n');
    }
    if (report.pristines.length > 0) {
        lines.push('Pristines:');
        lines.push([
            'workspace'.padEnd(24),
            'built_at'.padEnd(22),
            'nodes'.padStart(8),
            'size'.padStart(10),
            'worlds'.padStart(7),
        ].join('  '));
        lines.push('-'.repeat(74));
        for (const p of report.pristines) {
            lines.push([
                p.workspace.padEnd(24),
                (p.built_at ?? '—').padEnd(22),
                String(p.node_count ?? '—').padStart(8),
                formatBytes(p.graphify_out_size_bytes).padStart(10),
                String(p.world_count).padStart(7),
            ].join('  '));
        }
        lines.push('');
    }
    if (report.overlays.length > 0) {
        lines.push('Overlays:');
        lines.push([
            'world_id'.padEnd(24),
            'clone'.padEnd(20),
            'nodes'.padStart(8),
            'size'.padStart(10),
        ].join('  '));
        lines.push('-'.repeat(66));
        for (const o of report.overlays) {
            lines.push([
                o.world_id.padEnd(24),
                o.clone_dir.padEnd(20),
                String(o.node_count ?? '—').padStart(8),
                formatBytes(o.graphify_out_size_bytes).padStart(10),
            ].join('  '));
        }
        lines.push('');
    }
    lines.push(`Total: ${formatBytes(report.total_bytes)} (cap: ${report.cap_status})`);
    return lines.join('\n');
}
export async function runKgStatus(workspaceArg, opts) {
    let report;
    try {
        report = buildReport(workspaceArg);
    }
    catch (err) {
        printError(err.message);
        return {
            exitCode: 1,
            report: { pristines: [], overlays: [], total_bytes: 0, cap_status: 'ok' },
        };
    }
    if (opts.json) {
        process.stdout.write(JSON.stringify(report, null, 2) + '\n');
    }
    else {
        process.stdout.write(renderHuman(report, workspaceArg) + '\n');
    }
    // Caps are emitted to stderr regardless of --json so script-callers
    // see them in their error stream.
    if (report.cap_status === 'soft-warn') {
        printWarning(`KG storage at ${formatBytes(report.total_bytes)} — exceeds 1.5GB soft cap. Consider `
            + `pruning stale pristines via \`rm -rf ~/.olam/kg/<workspace>\` after operator review.`);
    }
    else if (report.cap_status === 'hard-critical') {
        printError(`KG storage at ${formatBytes(report.total_bytes)} — exceeds 5GB hard cap. Prune stale `
            + `pristines or overlays before running \`olam kg build\` again.`);
        return { exitCode: 1, report };
    }
    return { exitCode: 0, report };
}
export function registerKgStatusCommand(kg) {
    kg.command('status')
        .description('Report pristine + overlay KG state for every workspace (or one named workspace). '
        + 'Emits 1.5GB soft-cap warning + 5GB hard-cap critical error on stderr.')
        .argument('[workspace]', 'workspace name (lowercase alphanumeric + hyphens/underscores)')
        .option('--json', 'emit the report as JSON instead of a compact table')
        .action(async (workspaceArg, opts) => {
        const r = await runKgStatus(workspaceArg, opts);
        if (r.exitCode !== 0)
            process.exitCode = r.exitCode;
    });
}
//# sourceMappingURL=kg-status.js.map