// `keiko memory` — operator surface for the governed memory vault (Epic #204).
//
//   maintain   Run one bounded maintenance pass IN-PROCESS (consolidate + decay + reinforce +
//              forget) against the local vault and print the applied counts. Reuses the exact same
//              `runMemoryMaintenance` core the BFF route uses, so the CLI and UI never drift.
//   stats      Print memory counts by status, by scope kind, and the total.
//
// The vault is opened at the resolved memory dir (default $KEIKO_MEMORY_DIR or the platform state
// dir; override with --memory-dir). Tests inject a vault via deps so no disk is touched. Exit 0 on
// success, 1 on a runtime error (vault open / maintenance fault), 2 on usage (unknown/missing
// subcommand).
import { createMemoryVault } from "@oscharko-dev/keiko-memory-vault";
import { createMemoryEmbedder, runMemoryMaintenance, } from "@oscharko-dev/keiko-server";
import { loadConfigFromFile, requestOpenAIEmbedding, GatewayError, } from "@oscharko-dev/keiko-model-gateway";
const USAGE = `Usage:
  keiko memory maintain [--memory-dir PATH]   Run a bounded consolidate + decay + forget pass.
  keiko memory stats [--memory-dir PATH]      Print memory counts by status and scope.
  keiko memory reembed [--memory-dir PATH] [--limit N] [--config PATH]
                                              Backfill embeddings for accepted memories lacking one.

Opens the local memory vault (default $KEIKO_MEMORY_DIR or the platform state dir; override with
--memory-dir). \`maintain\` strengthens recalled memories, decays stale ones, archives faded ones,
forgets expired/very-faint ones, and auto-supersedes pairwise correction conflicts. \`reembed\`
computes the embedding for each accepted memory that has none (bounded by --limit, default 200), so
pre-existing memories become semantically retrievable; it is gated on an embedding model being
configured (via --config / $KEIKO_CONFIG_FILE) and is best-effort.
`;
const DEFAULT_REEMBED_LIMIT = 200;
function flagValue(args, name) {
    const i = args.indexOf(name);
    if (i === -1)
        return undefined;
    const value = args[i + 1];
    return value === undefined || value.startsWith("--") ? undefined : value;
}
function resolveVault(args, env, deps) {
    if (deps.vault !== undefined)
        return deps.vault;
    const memoryDir = flagValue(args, "--memory-dir");
    if (deps.openVault !== undefined)
        return deps.openVault(memoryDir, env);
    return createMemoryVault({
        ...(memoryDir !== undefined ? { memoryDir } : {}),
        env,
    });
}
function scopeKindOf(scope) {
    return scope.kind;
}
function tallyBy(records, keyOf) {
    const counts = new Map();
    for (const record of records) {
        const key = keyOf(record);
        counts.set(key, (counts.get(key) ?? 0) + 1);
    }
    return counts;
}
function renderTally(title, counts) {
    const rows = [...counts.entries()].sort((a, b) => a[0].localeCompare(b[0]));
    if (rows.length === 0)
        return `${title}:\n  (none)\n`;
    const body = rows.map(([key, n]) => `  ${key}: ${String(n)}`).join("\n");
    return `${title}:\n${body}\n`;
}
function renderStats(records) {
    const byStatus = tallyBy(records, (record) => record.status);
    const byScope = tallyBy(records, (record) => scopeKindOf(record.scope));
    return (renderTally("By status", byStatus) +
        renderTally("By scope", byScope) +
        `Total: ${String(records.length)}\n`);
}
function renderMaintenanceReport(counts) {
    return [
        "Memory maintenance complete.",
        `  promoted:          ${String(counts.promoted)}`,
        `  reinforced:        ${String(counts.reinforced)}`,
        `  decayed:           ${String(counts.decayed)}`,
        `  archived:          ${String(counts.archived)}`,
        `  forgotten:         ${String(counts.forgotten)}`,
        `  superseded:        ${String(counts.superseded)}`,
        `  edgesCreated:      ${String(counts.edgesCreated)}`,
        `  clustersInspected: ${String(counts.clustersInspected)}`,
        "",
    ].join("\n");
}
function runStats(args, io, env, deps) {
    const vault = resolveVault(args, env, deps);
    try {
        const records = vault.listMemories({ includeExpired: true });
        io.out(renderStats(records));
        return 0;
    }
    finally {
        if (deps.vault === undefined)
            vault.close();
    }
}
function runMaintain(args, io, env, deps) {
    const vault = resolveVault(args, env, deps);
    try {
        const counts = runMemoryMaintenance(vault);
        io.out(renderMaintenanceReport(counts));
        return 0;
    }
    finally {
        if (deps.vault === undefined)
            vault.close();
    }
}
// Resolves the production embedder from the gateway config (--config / $KEIKO_CONFIG_FILE), or
// null when no config source is available, the config cannot be loaded, or no embedding-capable
// model is configured. The test seam (deps.embedText) short-circuits this entirely. A GatewayError
// is treated as "no model" (best-effort backfill never hard-fails on a config problem).
function resolveEmbedder(args, env, deps) {
    if (deps.embedText !== undefined)
        return deps.embedText;
    const configPath = flagValue(args, "--config") ?? env.KEIKO_CONFIG_FILE;
    if (configPath === undefined)
        return null;
    try {
        return createMemoryEmbedder(loadConfigFromFile(configPath, env), requestOpenAIEmbedding);
    }
    catch (error) {
        if (error instanceof GatewayError)
            return null;
        throw error;
    }
}
function parseLimit(args) {
    const raw = flagValue(args, "--limit");
    if (raw === undefined)
        return DEFAULT_REEMBED_LIMIT;
    const parsed = Number.parseInt(raw, 10);
    return Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_REEMBED_LIMIT;
}
async function backfillEmbeddings(vault, embed, limit) {
    const accepted = vault.listMemories({ status: ["accepted"], includeExpired: true, limit });
    const counts = { embedded: 0, skipped: 0, failed: 0 };
    for (const record of accepted) {
        if (vault.getEmbedding(record.id) !== undefined) {
            counts.skipped += 1;
            continue;
        }
        const input = await embed(record.body);
        if (input === null) {
            counts.failed += 1;
            continue;
        }
        try {
            vault.upsertEmbedding(record.id, input);
            counts.embedded += 1;
        }
        catch {
            counts.failed += 1;
        }
    }
    return counts;
}
function renderReembedReport(counts) {
    return [
        "Memory re-embedding complete.",
        `  embedded: ${String(counts.embedded)}`,
        `  skipped:  ${String(counts.skipped)}`,
        `  failed:   ${String(counts.failed)}`,
        "",
    ].join("\n");
}
async function reembed(args, io, env, deps) {
    const embed = resolveEmbedder(args, env, deps);
    if (embed === null) {
        io.out("No embedding model is configured — skipping re-embedding. " +
            "Provide a gateway config with --config PATH or $KEIKO_CONFIG_FILE.\n");
        return 0;
    }
    const vault = resolveVault(args, env, deps);
    try {
        const counts = await backfillEmbeddings(vault, embed, parseLimit(args));
        io.out(renderReembedReport(counts));
        return 0;
    }
    finally {
        if (deps.vault === undefined)
            vault.close();
    }
}
// async wrapper so a sync-or-async failure surfaces as exit 1 (the sync subcommands rely on
// dispatchSubcommand's try/catch, which cannot catch a rejected Promise).
async function runReembed(args, io, env, deps) {
    try {
        return await reembed(args, io, env, deps);
    }
    catch (error) {
        io.err(`keiko memory: ${error instanceof Error ? error.message : String(error)}\n`);
        return 1;
    }
}
function dispatchSubcommand(sub, args, io, env, deps) {
    try {
        if (sub === "maintain")
            return runMaintain(args, io, env, deps);
        if (sub === "stats")
            return runStats(args, io, env, deps);
        if (sub === "reembed")
            return runReembed(args, io, env, deps);
    }
    catch (error) {
        io.err(`keiko memory: ${error instanceof Error ? error.message : String(error)}\n`);
        return 1;
    }
    io.err(`keiko memory: unknown subcommand: ${sub}\n`);
    io.err(USAGE);
    return 2;
}
export function runMemoryCli(rest, io, env = {}, deps = {}) {
    const sub = rest[0];
    if (sub === undefined || sub === "--help" || sub === "-h") {
        io.out(USAGE);
        return sub === undefined ? 2 : 0;
    }
    return dispatchSubcommand(sub, rest.slice(1), io, env, deps);
}
