import { getSession } from "../lib/neo4j.js";
import { trashNode } from "../../../../../lib/graph-trash/dist/index.js";
import { verifyFilterToken, filterTokenFingerprint } from "../lib/filter-token.js";
import { reverifyCandidate } from "./memory-find-candidates.js";
import { withWriteEvent } from "../lib/log-ingest.js";
import { checkLabelDeleteGate } from "../lib/label-delete-gate.js";
export class FilterTokenError extends Error {
    reason;
    constructor(message, reason) {
        super(message);
        this.reason = reason;
        this.name = "FilterTokenError";
    }
}
export async function memoryDelete(params) {
    return withWriteEvent({ tool: "memory-delete", accountId: params.accountId }, () => memoryDeleteImpl(params), (r) => ({ elementId: r.elementId, labels: r.labels }));
}
function filterReservedCascade(nodes, parentEid, kind) {
    const out = [];
    for (const n of nodes) {
        if (n.labels.includes("LocalBusiness")) {
            process.stderr.write(`[memory-delete:cascade-skip-reserved] parent=${parentEid} elementId=${n.eid} kind=${kind} label=LocalBusiness\n`);
            continue;
        }
        out.push(n.eid);
    }
    return out;
}
async function memoryDeleteImpl(params) {
    const t0 = Date.now();
    const { accountId, elementId, reason, filterToken, origin } = params;
    const session = getSession();
    try {
        // Filter-token re-verification. Must run before trashNode so a
        // rejection never mutates the graph. Observable outcome: the
        // `[memory-delete]` log line carries `reverified=true|false` and
        // `outcome=trashed|rejected` — never silent.
        let reverified = false;
        if (filterToken) {
            const verify = verifyFilterToken(filterToken);
            if (!verify.ok) {
                const ms = Date.now() - t0;
                process.stderr.write(`[memory-delete] elementId=${elementId} filterToken=${filterTokenFingerprint(filterToken)} reverified=false outcome=rejected reason=${verify.reason} ms=${ms}\n`);
                throw new FilterTokenError(`memory-delete: filter token rejected (${verify.reason})`, verify.reason);
            }
            if (verify.payload.accountId !== accountId) {
                const ms = Date.now() - t0;
                process.stderr.write(`[memory-delete] elementId=${elementId} filterToken=${filterTokenFingerprint(filterToken)} reverified=false outcome=rejected reason=account-mismatch ms=${ms}\n`);
                throw new FilterTokenError(`memory-delete: filter token account mismatch`, "mismatch");
            }
            const stillMatches = await reverifyCandidate({
                session,
                accountId,
                filter: verify.payload.filter,
                agentType: verify.payload.agentType,
                elementId,
            });
            if (!stillMatches) {
                const ms = Date.now() - t0;
                process.stderr.write(`[memory-delete] elementId=${elementId} filterToken=${filterTokenFingerprint(filterToken)} reverified=false outcome=rejected reason=predicate-no-longer-matches ms=${ms}\n`);
                throw new FilterTokenError(`memory-delete: target ${elementId} no longer matches filter '${verify.payload.filter}'`, "mismatch");
            }
            reverified = true;
        }
        // Task 443 — refuse non-system deletes targeting the per-account root
        // `:LocalBusiness`. Resolve labels by elementId in a single round trip
        // before any mutation so a rejection never trashes the graph.
        const labelResult = await session.run(`MATCH (n) WHERE elementId(n) = $eid AND n.accountId = $accountId RETURN labels(n) AS labels`, { eid: elementId, accountId });
        const preLabels = labelResult.records[0]?.get("labels") ?? [];
        const deleteGate = checkLabelDeleteGate({
            labels: preLabels,
            origin,
            accountId,
            elementId,
            tool: "memory-delete",
        });
        if (!deleteGate.ok) {
            const ms = Date.now() - t0;
            const tokenFp = filterToken ? filterTokenFingerprint(filterToken) : "none";
            process.stderr.write(`[memory-delete] elementId=${elementId} filterToken=${tokenFp} reverified=${reverified} outcome=rejected reason=reserved-label ms=${ms}\n`);
            throw new Error(deleteGate.message);
        }
        const root = await trashNode({
            session,
            accountId,
            elementId,
            by: "memory-delete",
            reason,
        });
        let sections = 0;
        let chunks = 0;
        // `:KnowledgeDocument` (document path) and `:ConversationArchive`
        // (conversation path — Task 397) both own :Section/:Chunk children
        // via :HAS_SECTION/:HAS_CHUNK; cascade is identical for both.
        const cascadeParent = root.labels.includes("KnowledgeDocument") ? "KnowledgeDocument"
            : root.labels.includes("ConversationArchive") ? "ConversationArchive"
                : null;
        if (cascadeParent) {
            const childResult = await session.run(`MATCH (d) WHERE elementId(d) = $eid
         OPTIONAL MATCH (d)-[:HAS_SECTION]->(s:Section)
         OPTIONAL MATCH (s)-[:HAS_CHUNK]->(c:Chunk)
         WITH collect(DISTINCT s) AS sections, collect(DISTINCT c) AS chunks
         RETURN [s IN sections WHERE s IS NOT NULL | {eid: elementId(s), labels: labels(s)}] AS sections,
                [c IN chunks   WHERE c IS NOT NULL | {eid: elementId(c), labels: labels(c)}] AS chunks`, { eid: elementId });
            const rawSections = childResult.records[0]?.get("sections") ?? [];
            const rawChunks = childResult.records[0]?.get("chunks") ?? [];
            // Task 443 — cascade safety: skip any descendant that resolves to
            // `:LocalBusiness`. Section/Chunk should never carry that label, so
            // a skip indicates an upstream schema corruption worth logging.
            const sectionIds = filterReservedCascade(rawSections, elementId, "Section");
            const chunkIds = filterReservedCascade(rawChunks, elementId, "Chunk");
            for (const sid of sectionIds) {
                const r = await trashNode({
                    session,
                    accountId,
                    elementId: sid,
                    by: `memory-delete:cascade-from-${elementId}`,
                    reason: reason ?? `cascade from ${cascadeParent} ${elementId}`,
                });
                if (r.trashed)
                    sections++;
            }
            for (const cid of chunkIds) {
                const r = await trashNode({
                    session,
                    accountId,
                    elementId: cid,
                    by: `memory-delete:cascade-from-${elementId}`,
                    reason: reason ?? `cascade from ${cascadeParent} ${elementId}`,
                });
                if (r.trashed)
                    chunks++;
            }
        }
        const ms = Date.now() - t0;
        const outcome = root.alreadyTrashed ? "already-trashed" : "trashed";
        const tokenFp = filterToken ? filterTokenFingerprint(filterToken) : "none";
        process.stderr.write(`[memory-delete] elementId=${elementId} filterToken=${tokenFp} reverified=${reverified} outcome=${outcome} ms=${ms}\n`);
        return {
            elementId,
            labels: root.labels,
            trashedAt: root.trashedAt,
            alreadyTrashed: root.alreadyTrashed,
            cascade: { sections, chunks },
        };
    }
    finally {
        await session.close();
    }
}
//# sourceMappingURL=memory-delete.js.map