/**
 * conversation-archive-enrich-rejection MCP tool.
 *
 * Durable rejection memory for conversation-archive-enrich. Writes to a
 * sidecar JSONL file alongside the account's state directory; the read side
 * (filter step) lives in `conversation-archive-derive-insights.ts` so the
 * path constant has a single source of truth.
 *
 * Modes:
 *   record — append a rejection line. O_APPEND is POSIX-atomic for writes
 *            ≤ PIPE_BUF; one JSONL line is well under that, so concurrent
 *            appends never corrupt the file.
 *   undo   — read all lines, drop matches by (chunkElementId, kind,
 *            contentHash), then atomic-rename the rewritten file. A `wire`
 *            of the same key triggers undo so the operator's change of mind
 *            doesn't leave stale rejections behind.
 *
 * Residual race: a `record` append landing between the `undo`'s read and
 * rename is overwritten. The per-row operator gate is single-threaded
 * (one chat-turn at a time), so this window is not exploitable in normal
 * operation. A `flock`-based fix is deferred until rejection volume warrants.
 */
import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
import { dirname } from "node:path";
import { rejectionsJsonlPath, rejectionKey, } from "./conversation-archive-derive-insights.js";
import { withWriteEvent } from "../lib/log-ingest.js";
function readAllLines(path) {
    if (!existsSync(path))
        return [];
    const raw = readFileSync(path, "utf8");
    return raw.split("\n").filter((line) => line.trim().length > 0);
}
function countMatches(lines, chunkElementId, kind, contentHash) {
    const target = rejectionKey(chunkElementId, kind, contentHash);
    let n = 0;
    for (const line of lines) {
        try {
            const obj = JSON.parse(line);
            if (typeof obj.chunkElementId === "string" &&
                typeof obj.kind === "string" &&
                typeof obj.contentHash === "string" &&
                rejectionKey(obj.chunkElementId, obj.kind, obj.contentHash) === target) {
                n += 1;
            }
        }
        catch {
            // Parse failure here is a counting concern, not a correctness one —
            // the filter side already emits rejection-parse-skip.
        }
    }
    return n;
}
export async function conversationArchiveEnrichRejection(params) {
    return withWriteEvent({
        tool: "conversation-archive-enrich-rejection",
        accountId: params.accountId,
        name: `${params.kind}.${params.mode}`,
    }, () => Promise.resolve(conversationArchiveEnrichRejectionImpl(params)));
}
function conversationArchiveEnrichRejectionImpl(params) {
    const { accountId, archiveElementId, chunkElementId, kind, contentHash, mode, sessionId, } = params;
    const path = rejectionsJsonlPath(accountId);
    mkdirSync(dirname(path), { recursive: true });
    const before = readAllLines(path);
    const beforeCount = countMatches(before, chunkElementId, kind, contentHash);
    if (mode === "record") {
        if (beforeCount > 0) {
            // Idempotent: a second `record` for the same key is a silent no-op.
            process.stderr.write(`[conversation-archive-enrich] rejected chunk=${chunkElementId} kind=${kind} contentHash=${contentHash} stored (already-present)\n`);
            return { ok: true, mode, beforeCount, afterCount: beforeCount, path };
        }
        const line = {
            archiveElementId,
            chunkElementId,
            kind,
            contentHash,
            rejectedAt: new Date().toISOString(),
            rejectedBySession: sessionId ?? null,
        };
        appendFileSync(path, JSON.stringify(line) + "\n");
        process.stderr.write(`[conversation-archive-enrich] rejected chunk=${chunkElementId} kind=${kind} contentHash=${contentHash} stored\n`);
        return { ok: true, mode, beforeCount, afterCount: beforeCount + 1, path };
    }
    // mode === "undo"
    if (beforeCount === 0) {
        return { ok: true, mode, beforeCount: 0, afterCount: 0, path };
    }
    const target = rejectionKey(chunkElementId, kind, contentHash);
    const remaining = [];
    for (const line of before) {
        try {
            const obj = JSON.parse(line);
            if (typeof obj.chunkElementId === "string" &&
                typeof obj.kind === "string" &&
                typeof obj.contentHash === "string" &&
                rejectionKey(obj.chunkElementId, obj.kind, obj.contentHash) === target) {
                continue;
            }
            remaining.push(line);
        }
        catch {
            // Preserve malformed lines through undo — corruption is the filter
            // side's problem to surface, not this tool's to silently drop.
            remaining.push(line);
        }
    }
    const tmp = path + ".tmp";
    writeFileSync(tmp, remaining.length ? remaining.join("\n") + "\n" : "");
    renameSync(tmp, path);
    process.stderr.write(`[conversation-archive-enrich] rejection-undone chunk=${chunkElementId} kind=${kind} contentHash=${contentHash}\n`);
    // The filter loop above removes EVERY matching line, not just one. Under
    // the residual record-vs-undo race documented at the top of this file, a
    // single key can briefly have multiple JSONL lines; afterCount must reflect
    // the actual remaining count for that key (always 0 after a successful
    // undo) rather than `beforeCount - 1` (only correct when beforeCount==1).
    return {
        ok: true,
        mode,
        beforeCount,
        afterCount: 0,
        path,
    };
}
//# sourceMappingURL=conversation-archive-enrich-rejection.js.map