/**
 * memory_ingest MCP tool — writes Obsidian-compatible Markdown under the vault.
 */
import { createHash } from "node:crypto";
import { formatEnterpriseCitationsMarkdownBlock, normalizeEnterpriseCitations, stableEnterpriseCitationsPayload, } from "./enterprise-citations.js";
import { getObsidianVaultPath } from "./vault-config.js";
import { readToolOutputsFileForIngest } from "./memory-ingest-file.js";
import { readVaultTextFile, withVaultWriteLock, writeVaultTextFileAtomic } from "./vault-utils.js";
import { logMcpToolShape } from "./mcp-tool-log.js";
const MEMORY_DIR = "Memory";
export function slugifyTitle(title) {
    const s = title
        .trim()
        .toLowerCase()
        .replace(/[^a-z0-9]+/g, "-")
        .replace(/^-|-$/g, "")
        .slice(0, 96);
    return s || "note";
}
function normalizeWikilink(name) {
    const t = name.trim();
    const m = t.match(/^\[\[(.+)\]\]$/);
    return (m ? m[1] : t).trim();
}
function formatToolOutputs(toolOutputs) {
    if (toolOutputs === undefined)
        return "";
    if (Array.isArray(toolOutputs)) {
        return toolOutputs
            .map((s) => s.trim())
            .filter(Boolean)
            .join("\n\n---\n\n");
    }
    return toolOutputs.trim();
}
function sectionPayload(input) {
    const toolText = formatToolOutputs(input.toolOutputs);
    return [
        input.insights?.trim() ?? "",
        stableEnterpriseCitationsPayload(input.enterpriseCitations),
        toolText,
        input.conversation?.trim() ?? "",
    ].join("\n\u0000\n");
}
export function hashIngestSection(input) {
    const h = createHash("sha256");
    h.update(sectionPayload(input), "utf8");
    return h.digest("hex");
}
export function extractIngestHashes(markdown) {
    const set = new Set();
    const re = /<!--\s*clawql-hash:([a-f0-9]{64})\s*-->/g;
    let m;
    while ((m = re.exec(markdown)) !== null) {
        set.add(m[1]);
    }
    return set;
}
function buildSectionBody(input, when, options) {
    const hash = hashIngestSection(input);
    const lines = [];
    lines.push(`### Ingestion (${when})`);
    if (input.sessionId?.trim()) {
        lines.push(`_Session:_ \`${input.sessionId.trim().replace(/`/g, "'")}\``);
        lines.push("");
    }
    if (input.insights?.trim()) {
        lines.push("#### Insights");
        lines.push(input.insights.trim());
        lines.push("");
    }
    if (input.enterpriseCitations?.length) {
        lines.push(formatEnterpriseCitationsMarkdownBlock(input.enterpriseCitations));
    }
    if (options?.toolOutputsReadFromFile?.trim()) {
        lines.push(`*Tool outputs were read on the server from* \`${options.toolOutputsReadFromFile.trim().replace(/`/g, "'")}\`.`);
        lines.push("");
    }
    const toolText = formatToolOutputs(input.toolOutputs);
    if (toolText) {
        lines.push("#### Tool outputs");
        lines.push("```text");
        lines.push(toolText);
        lines.push("```");
        lines.push("");
    }
    if (input.conversation?.trim()) {
        lines.push("#### Conversation");
        lines.push("```text");
        lines.push(input.conversation.trim());
        lines.push("```");
        lines.push("");
    }
    lines.push("#### Provenance");
    lines.push(`- **Tool:** \`memory_ingest\` · **UTC:** \`${when}\`${input.sessionId?.trim() ? ` · **session:** \`${input.sessionId.trim().replace(/`/g, "'")}\`` : ""}`);
    lines.push(`- **Section fingerprint:** SHA-256 \`${hash.slice(0, 16)}…\` (canonical hash in the HTML comment below).`);
    lines.push("");
    lines.push(`<!-- clawql-hash:${hash} -->`);
    return lines.join("\n");
}
function buildRelatedLinks(wikilinks) {
    if (!wikilinks?.length)
        return "";
    const items = wikilinks.map((w) => `- [[${normalizeWikilink(w)}]]`).join("\n");
    return `## Related\n\n${items}\n\n`;
}
function buildFrontmatter(title) {
    const iso = new Date().toISOString();
    return [
        "---",
        `title: ${JSON.stringify(title)}`,
        `date: ${iso}`,
        "tags: [clawql-ingest]",
        "clawql_ingest: true",
        `clawql_ingest_created: ${JSON.stringify(iso)}`,
        "---",
        "",
    ].join("\n");
}
export async function runMemoryIngest(input) {
    const vault = getObsidianVaultPath();
    if (!vault) {
        return {
            ok: false,
            error: "Obsidian vault is not configured. Set CLAWQL_OBSIDIAN_VAULT_PATH to a writable directory.",
        };
    }
    const title = input.title?.trim();
    if (!title) {
        return { ok: false, error: "title is required" };
    }
    let effective = { ...input };
    let fileProvenance;
    if (input.toolOutputsFile?.trim()) {
        const r = await readToolOutputsFileForIngest(input.toolOutputsFile.trim());
        if (!r.ok) {
            return { ok: false, error: r.error };
        }
        fileProvenance = r.displayPath;
        effective = {
            ...input,
            toolOutputs: r.text,
            toolOutputsFile: undefined,
        };
    }
    effective = {
        ...effective,
        enterpriseCitations: normalizeEnterpriseCitations(effective.enterpriseCitations),
    };
    const slug = slugifyTitle(title);
    const rel = `${MEMORY_DIR}/${slug}.md`;
    const append = effective.append !== false;
    const hash = hashIngestSection(effective);
    const when = new Date().toISOString();
    const result = await withVaultWriteLock(vault, async () => {
        let existing = "";
        try {
            existing = await readVaultTextFile(vault, rel);
        }
        catch {
            // missing file → keep existing as ""
        }
        if (existing && extractIngestHashes(existing).has(hash)) {
            return {
                ok: true,
                path: rel,
                skipped: true,
                reason: "Identical ingest payload was already stored (content hash match).",
            };
        }
        const section = buildSectionBody(effective, when, {
            toolOutputsReadFromFile: fileProvenance,
        });
        const related = buildRelatedLinks(effective.wikilinks);
        if (!existing) {
            const body = [
                buildFrontmatter(title),
                `# ${title}`,
                "",
                related,
                "---",
                "",
                section,
                "",
            ].join("\n");
            await writeVaultTextFileAtomic(vault, rel, body);
            return { ok: true, path: rel };
        }
        if (!append) {
            const body = [
                buildFrontmatter(title),
                `# ${title}`,
                "",
                related,
                "---",
                "",
                section,
                "",
            ].join("\n");
            await writeVaultTextFileAtomic(vault, rel, body);
            return { ok: true, path: rel };
        }
        const next = `${existing.trimEnd()}\n\n---\n\n${section}\n`;
        await writeVaultTextFileAtomic(vault, rel, next);
        return { ok: true, path: rel };
    });
    if (result.ok && !result.skipped) {
        const indexExtras = {};
        const { syncMemoryDbForVaultScanRoot, loadVaultMerkleSnapshotFromDb, memoryDbSyncEnabled } = await import("./memory-db.js");
        const merkleOn = process.env.CLAWQL_MERKLE_ENABLED === "1";
        let merkleBefore;
        if (merkleOn && memoryDbSyncEnabled()) {
            try {
                merkleBefore = await loadVaultMerkleSnapshotFromDb(vault);
            }
            catch (e) {
                const msg = e instanceof Error ? e.message : String(e);
                console.error(`[clawql-mcp] memory.db merkle snapshot (before ingest sync) failed: ${msg}`);
                merkleBefore = null;
            }
        }
        let syncOk = false;
        if (memoryDbSyncEnabled()) {
            try {
                await syncMemoryDbForVaultScanRoot(vault);
                syncOk = true;
            }
            catch (e) {
                const msg = e instanceof Error ? e.message : String(e);
                console.error(`[clawql-mcp] memory.db sync after ingest failed: ${msg}`);
            }
        }
        if (syncOk) {
            if (process.env.CLAWQL_CUCKOO_ENABLED === "1") {
                indexExtras.cuckooMembershipReady = true;
            }
            if (merkleOn) {
                try {
                    const merkleAfter = await loadVaultMerkleSnapshotFromDb(vault);
                    const merklePrior = merkleBefore ?? null;
                    indexExtras.merkleSnapshotBefore = merklePrior;
                    indexExtras.merkleSnapshot = merkleAfter;
                    indexExtras.merkleRootChanged =
                        merkleAfter === null
                            ? undefined
                            : merklePrior === null
                                ? true
                                : merklePrior.rootHex !== merkleAfter.rootHex;
                }
                catch (e) {
                    const msg = e instanceof Error ? e.message : String(e);
                    console.error(`[clawql-mcp] memory.db merkle snapshot (after ingest sync) failed: ${msg}`);
                }
            }
        }
        try {
            const { updateProviderIndexPage } = await import("./memory-provider-index.js");
            await updateProviderIndexPage(vault);
        }
        catch (e) {
            const msg = e instanceof Error ? e.message : String(e);
            console.error(`[clawql-mcp] provider index update after ingest failed: ${msg}`);
        }
        return { ...result, ...indexExtras };
    }
    return result;
}
export async function handleMemoryIngestToolInput(params) {
    const result = await runMemoryIngest(params);
    logMcpToolShape("memory_ingest", {
        titleChars: params.title?.length ?? 0,
        append: params.append,
        hasInsights: Boolean(params.insights?.trim()),
        enterpriseCitationCount: params.enterpriseCitations?.length ?? 0,
        hasConversation: Boolean(params.conversation?.trim()),
        hasToolOutputsFile: Boolean(params.toolOutputsFile?.trim()),
        hasToolOutputs: Boolean(typeof params.toolOutputs === "string"
            ? params.toolOutputs.trim()
            : params.toolOutputs?.some((s) => s.trim())),
        wikilinkCount: params.wikilinks?.length ?? 0,
        hasSessionId: Boolean(params.sessionId?.trim()),
        ok: result.ok,
        skipped: result.skipped,
        merkleRootChanged: result.merkleRootChanged,
        hasMerkleSnapshot: Boolean(result.merkleSnapshot),
        cuckooMembershipReady: result.cuckooMembershipReady,
    });
    return {
        content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
    };
}
