import path from "node:path";
import * as fs from "node:fs/promises";
import { execSync } from "node:child_process";
import parseDuration from "parse-duration";
import { confirmOrCancel } from "toolcraft-design";
import { defaultQueryBudget } from "@poe-code/poe-code-config";
import { cacheStatus, clearCache, editPage, initMemory, installMemory, openMemory, resolveConfiguredMemoryRoot } from "@poe-code/memory";
import memorySkillTemplate from "../../../packages/memory/src/templates/SKILL_memory.md";
import { throwCommandNotFound } from "../command-not-found.js";
import { ValidationError } from "../errors.js";
import { hasOwnErrorCode } from "../../utils/error-codes.js";
import { createExecutionResources, requireInteractiveStdin, resolveCommandFlags, shlexQuote } from "./shared.js";
async function resolveRoot(container) {
    return resolveConfiguredMemoryRoot({
        cwd: container.env.cwd,
        env: container.env.variables,
        fs: container.fs,
        configPath: container.env.configPath,
        projectConfigPath: container.env.projectConfigPath
    });
}
function resolvePageRelPath(input) {
    const trimmed = input.trim();
    if (trimmed.length === 0) {
        throw new ValidationError("Missing page path.");
    }
    const normalized = trimmed.replaceAll("\\", "/");
    const withExt = path.posix.extname(normalized).length === 0 ? `${normalized}.md` : normalized;
    const relPath = path.posix.normalize(withExt.startsWith("pages/") ? withExt : `pages/${withExt}`);
    if (!relPath.startsWith("pages/") || relPath === "pages/.." || relPath.includes("/../")) {
        throw new ValidationError("Page path must remain under memory pages/.");
    }
    return relPath;
}
function displayPageRelPath(relPath) {
    return relPath.startsWith("pages/") ? relPath.slice("pages/".length) : relPath;
}
function parseCacheOlderThan(value) {
    if (value === undefined) {
        return undefined;
    }
    const duration = parseDuration(value);
    if (duration === null || Number.isNaN(duration) || duration < 0) {
        throw new ValidationError(`Invalid duration for --older-than: "${value}".`);
    }
    return duration;
}
function formatCacheDryRunMessage(olderThan) {
    if (olderThan === undefined) {
        return "Would clear all memory cache entries.";
    }
    return `Would clear memory cache entries older than ${olderThan}.`;
}
async function assertInitialized(mem) {
    const status = await mem.statusOf();
    if (!status.initialized) {
        throw new ValidationError(`Memory is not initialized. Run "poe-code memory init" in this project.`);
    }
}
async function queryBudget(container, value) {
    if (value !== undefined) {
        const budget = Number(value);
        if (Number.isFinite(budget) && budget >= 0) {
            return budget;
        }
        throw new ValidationError("Budget must be a finite non-negative number.");
    }
    return defaultQueryBudget({
        fs: container.fs,
        filePath: container.env.configPath,
        projectFilePath: container.env.projectConfigPath
    });
}
function parseDecimalNonNegativeInteger(value, label) {
    if (value === undefined) {
        return undefined;
    }
    if (!isDecimalNonNegativeIntegerText(value)) {
        throw new ValidationError(`${label} must be a decimal non-negative integer.`);
    }
    const parsed = Number(value);
    if (!Number.isSafeInteger(parsed)) {
        throw new ValidationError(`${label} must be a decimal non-negative integer.`);
    }
    return parsed;
}
function isDecimalNonNegativeIntegerText(value) {
    if (value.length === 0) {
        return false;
    }
    if (value.length > 1 && value[0] === "0") {
        return false;
    }
    return [...value].every((char) => char >= "0" && char <= "9");
}
async function readCommandContent(content) {
    if (content !== undefined) {
        return content;
    }
    const chunks = [];
    for await (const chunk of process.stdin) {
        chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
    }
    return Buffer.concat(chunks).toString("utf8");
}
function resolveEditor(container) {
    const editor = container.env.getVariable("EDITOR") ?? container.env.getVariable("VISUAL");
    if (editor === undefined || editor.trim().length === 0) {
        throw new ValidationError("Set $EDITOR to use this command.");
    }
    return editor.trim();
}
function resolveIngestSource(cwd, input) {
    try {
        const url = new URL(input);
        if (url.protocol === "http:" || url.protocol === "https:") {
            return { kind: "url", url: url.toString() };
        }
    }
    catch (error) {
        if (!(error instanceof TypeError)) {
            throw error;
        }
    }
    return { kind: "file", absPath: path.resolve(cwd, input) };
}
export function registerMemoryCommand(program, container) {
    const memory = program
        .command("memory")
        .description("Persistent memory directory commands.")
        .allowExcessArguments()
        .action(function () {
        if (this.args.length > 0) {
            throwCommandNotFound({
                container,
                scope: "cli",
                unknownCommand: this.args.at(0) ?? "",
                helpArgs: ["memory", "--help"],
                moduleUrl: import.meta.url
            });
        }
        this.help();
    });
    memory
        .command("init")
        .description("Create .poe-code/memory/ with empty INDEX.md and LOG.md.")
        .action(async () => {
        const flags = resolveCommandFlags(program);
        const resources = createExecutionResources(container, flags, "memory:init");
        const root = await resolveRoot(container);
        resources.logger.intro("memory init");
        if (flags.dryRun) {
            resources.logger.dryRun(`Would initialize memory at ${root}`);
            return;
        }
        await initMemory(root);
        resources.context.complete({
            success: `Initialized memory at ${path.relative(container.env.cwd, root)}`,
            dry: `Would initialize memory at ${path.relative(container.env.cwd, root)}`
        });
        resources.context.finalize();
    });
    memory
        .command("ls")
        .description("List every page with a one-line description.")
        .action(async () => {
        const root = await resolveRoot(container);
        const mem = openMemory({ root });
        await assertInitialized(mem);
        const pages = await mem.listPages();
        if (pages.length === 0) {
            process.stdout.write("No memory pages yet.\n");
            return;
        }
        for (const page of pages) {
            const description = page.frontmatter.description?.trim() ?? "";
            process.stdout.write(`${displayPageRelPath(page.relPath)}${description.length > 0 ? ` — ${description}` : ""}\n`);
        }
    });
    memory
        .command("show")
        .description("Print a page to stdout.")
        .argument("<path>", "Page path (relative to memory pages/)")
        .action(async (pagePath) => {
        const root = await resolveRoot(container);
        const mem = openMemory({ root });
        await assertInitialized(mem);
        const relPath = resolvePageRelPath(pagePath);
        const absPath = path.join(mem.root, relPath);
        try {
            await mem.readPage(relPath);
            const content = await fs.readFile(absPath, "utf8");
            process.stdout.write(content.endsWith("\n") ? content : `${content}\n`);
        }
        catch (error) {
            if (hasOwnErrorCode(error, "ENOENT")) {
                throw new ValidationError(`Page not found: ${displayPageRelPath(relPath)}`);
            }
            throw error;
        }
    });
    memory
        .command("search")
        .description("Search over memory files for a substring.")
        .argument("<query>", "Search query")
        .action(async (query) => {
        const root = await resolveRoot(container);
        const mem = openMemory({ root });
        await assertInitialized(mem);
        const hits = await mem.searchMemory(query);
        if (hits.length === 0) {
            process.stdout.write("No matches.\n");
            return;
        }
        for (const hit of hits) {
            const displayPath = displayPageRelPath(hit.relPath);
            process.stdout.write(`${displayPath}:${hit.lineNumber}: ${hit.line}\n`);
        }
    });
    memory
        .command("write")
        .description("Replace a page with content read from stdin.")
        .argument("<path>", "Page path (relative to memory pages/)")
        .requiredOption("--reason <text>", "Reason for the memory update")
        .option("--content <text>", "Page content instead of stdin")
        .action(async (pagePath, options) => {
        const flags = resolveCommandFlags(program);
        const root = await resolveRoot(container);
        const mem = openMemory({ root });
        await assertInitialized(mem);
        const relPath = resolvePageRelPath(pagePath);
        const content = await readCommandContent(options.content);
        if (flags.dryRun) {
            createExecutionResources(container, flags, "memory:write").logger.dryRun(`Would write ${relPath}.`);
            return;
        }
        await mem.writePage(relPath, content, { reason: options.reason });
    });
    memory
        .command("append")
        .description("Append stdin content to a page.")
        .argument("<path>", "Page path (relative to memory pages/)")
        .option("--reason <text>", "Reason for the memory update", "append")
        .option("--content <text>", "Content instead of stdin")
        .action(async (pagePath, options) => {
        const flags = resolveCommandFlags(program);
        const root = await resolveRoot(container);
        const mem = openMemory({ root });
        await assertInitialized(mem);
        const relPath = resolvePageRelPath(pagePath);
        const content = await readCommandContent(options.content);
        if (flags.dryRun) {
            createExecutionResources(container, flags, "memory:append").logger.dryRun(`Would append to ${relPath}.`);
            return;
        }
        await mem.appendToPage(relPath, content, { reason: options.reason });
    });
    memory
        .command("edit")
        .description("Open a page in $EDITOR.")
        .argument("<path>", "Page path (relative to memory pages/)")
        .option("--reason <text>", "Reason for the memory update", "edit")
        .action(async (pagePath, options) => {
        const flags = resolveCommandFlags(program);
        const root = await resolveRoot(container);
        const mem = openMemory({ root });
        await assertInitialized(mem);
        const relPath = resolvePageRelPath(pagePath);
        const editor = resolveEditor(container);
        if (flags.dryRun) {
            createExecutionResources(container, flags, "memory:edit").logger.dryRun(`Would open ${relPath} in ${editor}.`);
            return;
        }
        await editPage(root, relPath, {
            reason: options.reason,
            launchEditor: async (filePath) => {
                execSync(`${editor} ${shlexQuote(filePath)}`, { stdio: "inherit" });
            }
        });
    });
    memory
        .command("ingest")
        .description("Fold a file or URL into memory through an agent.")
        .argument("<source>", "Local file path or URL")
        .option("--agent <agent>", "Agent override")
        .option("--reason <text>", "Reason for ingest")
        .option("--timeout-ms <ms>", "Timeout in milliseconds")
        .option("--force", "Bypass an existing cache hit")
        .option("--no-cache-write", "Skip cache persistence")
        .action(async (source, options) => {
        const flags = resolveCommandFlags(program);
        const root = await resolveRoot(container);
        const mem = openMemory({ root });
        await assertInitialized(mem);
        const timeoutMs = parseDecimalNonNegativeInteger(options.timeoutMs, "Timeout");
        const result = await mem.ingest({
            source: resolveIngestSource(container.env.cwd, source),
            agent: options.agent,
            reason: options.reason,
            timeoutMs,
            force: options.force,
            noCacheWrite: options.cacheWrite === false,
            dryRun: flags.dryRun
        });
        process.stdout.write(`${result.cacheHit ? "Cache hit" : "Ingested"}: ${result.diff.created.length} created, ${result.diff.updated.length} updated, ${result.diff.deleted.length} deleted.\n`);
    });
    memory
        .command("lint")
        .description("Audit memory confidence and provenance claims.")
        .action(async () => {
        const root = await resolveRoot(container);
        const mem = openMemory({ root });
        await assertInitialized(mem);
        const audits = await mem.auditClaims({ repoRoot: container.env.cwd });
        if (audits.length === 0) {
            process.stdout.write("No memory lint issues.\n");
            return;
        }
        for (const audit of audits) {
            for (const issue of audit.issues) {
                process.stdout.write(`${audit.page}: ${issue}\n`);
            }
        }
    });
    memory
        .command("query")
        .description("Answer a question using memory-only context.")
        .argument("<question>", "Question")
        .option("--budget <tokens>", "Token budget")
        .option("--agent <agent>", "Agent override")
        .action(async (question, options) => {
        const flags = resolveCommandFlags(program);
        const root = await resolveRoot(container);
        const mem = openMemory({ root });
        await assertInitialized(mem);
        const budget = await queryBudget(container, options.budget);
        if (flags.dryRun) {
            createExecutionResources(container, flags, "memory:query").logger.dryRun(`Would query memory with budget ${budget}.`);
            return;
        }
        const result = await mem.query({
            question,
            budget,
            agent: options.agent
        });
        process.stdout.write(`${result.answer}\n`);
    });
    memory
        .command("explain")
        .description("Summarize a memory page and its relationships.")
        .argument("<path>", "Page path (relative to memory pages/)")
        .option("--budget <tokens>", "Token budget")
        .option("--agent <agent>", "Agent override")
        .action(async (pagePath, options) => {
        const flags = resolveCommandFlags(program);
        const root = await resolveRoot(container);
        const mem = openMemory({ root });
        await assertInitialized(mem);
        const relPath = resolvePageRelPath(pagePath);
        const budget = await queryBudget(container, options.budget);
        if (flags.dryRun) {
            createExecutionResources(container, flags, "memory:explain").logger.dryRun(`Would explain ${relPath} with budget ${budget}.`);
            return;
        }
        const result = await mem.explainPage({
            relPath,
            budget,
            agent: options.agent
        });
        process.stdout.write(`${result.answer}\n`);
    });
    memory
        .command("install")
        .description("Install the memory skill and MCP server configuration.")
        .requiredOption("--agent <agent>", "Target agent")
        .option("--global", "Install skill globally")
        .option("--skill-only", "Install only the skill")
        .option("--mcp-only", "Configure only the MCP server")
        .option("--allow-writes", "Allow MCP append writes")
        .action(async (options) => {
        const flags = resolveCommandFlags(program);
        await installMemory({
            agent: options.agent,
            skillContent: memorySkillTemplate,
            fs: container.fs,
            cwd: container.env.cwd,
            homeDir: container.env.homeDir,
            platform: container.env.platform,
            scope: options.global ? "global" : "local",
            skillOnly: options.skillOnly,
            mcpOnly: options.mcpOnly,
            allowWrites: options.allowWrites,
            dryRun: flags.dryRun
        });
    });
    memory
        .command("status")
        .description("Show memory status.")
        .option("--no-tokens", "Skip token stats calculation")
        .action(async function () {
        const root = await resolveRoot(container);
        const mem = openMemory({ root });
        const options = this.opts();
        await assertInitialized(mem);
        const status = await mem.statusOf();
        process.stdout.write(`Pages: ${status.pageCount}\n`);
        process.stdout.write(`Bytes: ${status.totalBytes}\n`);
        if (status.lastWriteAt) {
            process.stdout.write(`Last write: ${status.lastWriteAt}\n`);
        }
        if (options.tokens) {
            const tokens = await mem.computeTokenStats();
            const ratio = Number.isFinite(tokens.reductionRatio)
                ? `${tokens.reductionRatio.toFixed(2)}×`
                : "0×";
            process.stdout.write(`Tokens: memory=${tokens.memoryTokens}, sources=${tokens.sourceTokens}, ratio=${ratio}\n`);
            if (tokens.missingSources.length > 0) {
                process.stdout.write(`Missing sources: ${tokens.missingSources.join(", ")}\n`);
            }
        }
    });
    const cache = memory
        .command("cache")
        .description("Inspect or clear ingest cache entries.");
    cache
        .command("status")
        .description("Show ingest cache entry count and bytes.")
        .action(async () => {
        const flags = resolveCommandFlags(program);
        const resources = createExecutionResources(container, flags, "memory:cache:status");
        const status = await cacheStatus(await resolveRoot(container));
        resources.logger.info(`${status.entries} cache ${status.entries === 1 ? "entry" : "entries"} (${status.bytes} bytes)`);
    });
    cache
        .command("clear")
        .description("Clear ingest cache entries.")
        .option("--older-than <duration>", "Clear entries older than the duration.")
        .action(async (options) => {
        const flags = resolveCommandFlags(program);
        const resources = createExecutionResources(container, flags, "memory:cache:clear");
        if (!flags.assumeYes) {
            throw new Error("Refusing to clear cache without --yes.");
        }
        const olderThanMs = parseCacheOlderThan(options.olderThan);
        if (flags.dryRun) {
            resources.logger.dryRun(formatCacheDryRunMessage(options.olderThan));
            return;
        }
        const result = await clearCache(await resolveRoot(container), olderThanMs === undefined ? {} : { olderThanMs });
        resources.logger.info(`removed ${result.removed} cache ${result.removed === 1 ? "entry" : "entries"}`);
    });
    memory
        .command("clear")
        .description("Delete all memory content and re-initialize INDEX.md and LOG.md.")
        .action(async () => {
        const flags = resolveCommandFlags(program);
        const resources = createExecutionResources(container, flags, "memory:clear");
        const root = await resolveRoot(container);
        const mem = openMemory({ root });
        resources.logger.intro("memory clear");
        await assertInitialized(mem);
        if (!flags.assumeYes) {
            requireInteractiveStdin("memory clear requires --yes when running without an interactive TTY.");
            await confirmOrCancel({
                message: "Clear all memory pages and cache?"
            });
        }
        if (flags.dryRun) {
            resources.logger.dryRun(`Would clear memory at ${mem.root}`);
            return;
        }
        await mem.clearMemory();
        resources.context.complete({
            success: "Cleared memory.",
            dry: "Would clear memory."
        });
        resources.context.finalize();
    });
}
//# sourceMappingURL=memory.js.map