import { getSession } from "../lib/neo4j.js";
import { mintFilterToken } from "../lib/filter-token.js";
const DEFAULT_LIMIT = 200;
const MAX_LIMIT = 1000;
const PROJECT_AND_ORDER = `
  WITH c,
       size([x IN msgs WHERE x.role = 'assistant'])                   AS assistantMsgCount,
       size([x IN msgs WHERE x.role = 'user' OR x.role IS NULL])      AS realUserMsgCount,
       head([x IN msgs WHERE x.role = 'user' | x.content])            AS firstUserMsg
  RETURN elementId(c)                           AS elementId,
         c.sessionId                        AS sessionId,
         assistantMsgCount                       AS assistantMsgCount,
         realUserMsgCount                        AS realUserMsgCount,
         CASE WHEN firstUserMsg IS NULL THEN NULL
              WHEN size(firstUserMsg) > 120 THEN left(firstUserMsg, 120) + '…'
              ELSE firstUserMsg
         END                                     AS firstUserMsgPreview,
         toString(c.trashedAt)                   AS trashedAt
  ORDER BY c.createdAt ASC
  LIMIT toInteger($limit)
`;
/**
 * Canonical selection cypher per filter. Agent-authored cypher never reaches
 * the DB; these literals are the only delete-selection path from the admin
 * agent's tool surface.
 */
function buildSelectionCypher(filter) {
    if (filter === "empty") {
        return `
      MATCH (c:Conversation)
      WHERE c.accountId = $accountId
        AND NOT c:Trashed
        AND c.deletedAt IS NULL
        AND ($agentTypeFilter IS NULL OR c.agentType = $agentTypeFilter)
        AND NOT EXISTS { MATCH (:Message)-[:PART_OF]->(c) }
      WITH c, [] AS msgs
      ${PROJECT_AND_ORDER}
    `;
    }
    // single-assistant
    return `
    MATCH (c:Conversation)
    WHERE c.accountId = $accountId
      AND NOT c:Trashed
      AND c.deletedAt IS NULL
      AND ($agentTypeFilter IS NULL OR c.agentType = $agentTypeFilter)
    WITH c, [(m:Message)-[:PART_OF]->(c) | m] AS msgs
    WHERE size(msgs) = 1
      AND size([x IN msgs WHERE x.role = 'assistant']) = 1
    ${PROJECT_AND_ORDER}
  `;
}
/** Server-side re-check for `memory-delete` token verification. */
export async function reverifyCandidate(params) {
    const { session, accountId, filter, agentType, elementId } = params;
    const cypher = filter === "empty"
        ? `
        MATCH (c:Conversation)
        WHERE elementId(c) = $elementId
          AND c.accountId = $accountId
          AND NOT c:Trashed
          AND c.deletedAt IS NULL
          AND ($agentTypeFilter IS NULL OR c.agentType = $agentTypeFilter)
          AND NOT EXISTS { MATCH (:Message)-[:PART_OF]->(c) }
        RETURN elementId(c) AS eid
      `
        : `
        MATCH (c:Conversation)
        WHERE elementId(c) = $elementId
          AND c.accountId = $accountId
          AND NOT c:Trashed
          AND c.deletedAt IS NULL
          AND ($agentTypeFilter IS NULL OR c.agentType = $agentTypeFilter)
        WITH c, [(m:Message)-[:PART_OF]->(c) | m] AS msgs
        WHERE size(msgs) = 1 AND size([x IN msgs WHERE x.role = 'assistant']) = 1
        RETURN elementId(c) AS eid
      `;
    const result = await session.run(cypher, {
        elementId,
        accountId,
        agentTypeFilter: agentType ?? null,
    });
    return result.records.length === 1;
}
export async function memoryFindCandidates(params) {
    const t0 = Date.now();
    const { accountId, filter, agentType, dryRun = false } = params;
    const limit = Math.min(params.limit ?? DEFAULT_LIMIT, MAX_LIMIT);
    const session = getSession();
    try {
        const cypher = buildSelectionCypher(filter);
        const result = await session.run(cypher, {
            accountId,
            agentTypeFilter: agentType ?? null,
            limit,
        });
        const items = result.records.map((r) => ({
            elementId: r.get("elementId"),
            sessionId: r.get("sessionId") ?? null,
            assistantMsgCount: Number(r.get("assistantMsgCount") ?? 0),
            realUserMsgCount: Number(r.get("realUserMsgCount") ?? 0),
            firstUserMsgPreview: r.get("firstUserMsgPreview") ?? null,
            trashedAt: r.get("trashedAt") ?? null,
        }));
        // Under dryRun the caller wants to see what would match without receiving
        // an actionable token — so the agent cannot "accidentally" feed a dryRun
        // result to memory-delete. Returning an empty string rather than omitting
        // the field keeps the result shape stable for the MCP surface.
        const filterToken = dryRun ? "" : mintFilterToken({ filter, agentType, accountId });
        process.stderr.write(`[memory-find-candidates] filter=${filter} agentType=${agentType ?? "any"} matched=${items.length} sampled=${items.length} dryRun=${dryRun} ms=${Date.now() - t0}\n`);
        return {
            filter,
            agentType,
            matched: items.length,
            sampled: items.length,
            items,
            filterToken,
            dryRun,
        };
    }
    finally {
        await session.close();
    }
}
//# sourceMappingURL=memory-find-candidates.js.map