// Memory retention policy enforcer (Epic #204, Issue #214).
//
// Pure-ish function over the public MemoryVaultStore port. For each caller-supplied scope
// it lists every memory, classifies each as `keep` / `expire-age` / `expire-proposal` /
// `evict-overflow`, and issues `vault.deleteMemory(id, { tombstone: true, ... })` for
// every non-keep classification. The audit handler (#214 bridge) observes the resulting
// `memory:tombstoned` events and appends `memory:forgotten` records to the audit ledger
// — retention enforcement is therefore self-audited through the existing seam.
//
// Hard invariants:
//
//   - Pinned records are NEVER eligible for retention. The pin flag overrides every other
//     classification reason; tested explicitly.
//   - Retention iterates the scopes the caller passes. The public MemoryVaultStore has no
//     `listAllScopes()` capability, so a global enumeration would require an internal
//     port extension. Out of scope here.
//   - Forgotten-purge (purgeForgottenAfterMs) is a no-op in this PR: the public port
//     exposes `listTombstonesByScope` for read but no API to delete tombstones, so we
//     cannot purge them through the public seam. The field is accepted for forward
//     compatibility and the result reports the count of tombstones THAT WOULD be purged,
//     but no destructive call is issued. A future PR can extend the port.
//
// Why the operations are issued in a sequence of one-by-one deletes: the vault's delete
// is already wrapped in a SQLite transaction (#206), and emitting a batch API here would
// hide the per-event audit signal that the handler depends on.
import { memoryScopeKey } from "./memory-scope-sanitizer.js";
// ─── Pure classification ──────────────────────────────────────────────────────
function isAgeExpired(record, nowMs, policy) {
    if (policy.maxAgeMs === undefined) {
        return false;
    }
    return nowMs - record.updatedAt > policy.maxAgeMs;
}
function isProposalExpired(record, nowMs, policy) {
    if (policy.expireProposalsAfterMs === undefined) {
        return false;
    }
    if (record.status !== "proposed") {
        return false;
    }
    return nowMs - record.updatedAt > policy.expireProposalsAfterMs;
}
// Returns the records that should be evicted by `maxRecordsPerScope`. Pinned records are
// excluded from the count entirely so the cap applies to the non-pinned working set.
function selectOverflowEvictions(records, policy) {
    if (policy.maxRecordsPerScope === undefined) {
        return [];
    }
    const nonPinned = records.filter((r) => !r.pinned);
    if (nonPinned.length <= policy.maxRecordsPerScope) {
        return [];
    }
    // Oldest-first by updatedAt; truncate to (count - cap) entries.
    const sorted = [...nonPinned].sort((a, b) => a.updatedAt - b.updatedAt);
    const overflowCount = nonPinned.length - policy.maxRecordsPerScope;
    return sorted.slice(0, overflowCount);
}
// Returns either the reason a single record should be forgotten, or undefined to keep it.
// Pinned records always return undefined. The age check takes precedence over the
// proposal-expiry check so the reported reason is the most descriptive applicable kind.
function classifyAgeOrProposal(record, nowMs, policy) {
    if (record.pinned) {
        return undefined;
    }
    if (isAgeExpired(record, nowMs, policy)) {
        return "expire-age";
    }
    if (isProposalExpired(record, nowMs, policy)) {
        return "expire-proposal";
    }
    return undefined;
}
// Combines per-record age/proposal classification with the per-scope overflow eviction
// pass. Returns the deduplicated set of decisions; if a record qualifies under both an
// age rule and an overflow rule, the age rule wins (more descriptive).
function classifyScope(scope, records, nowMs, policy) {
    const decisions = new Map();
    for (const record of records) {
        const reason = classifyAgeOrProposal(record, nowMs, policy);
        if (reason === undefined) {
            continue;
        }
        decisions.set(record.id, {
            memoryId: record.id,
            scope,
            reason,
            updatedAt: record.updatedAt,
            status: record.status,
        });
    }
    for (const record of selectOverflowEvictions(records, policy)) {
        if (decisions.has(record.id)) {
            continue;
        }
        decisions.set(record.id, {
            memoryId: record.id,
            scope,
            reason: "evict-overflow",
            updatedAt: record.updatedAt,
            status: record.status,
        });
    }
    return [...decisions.values()];
}
export function applyMemoryRetention(options) {
    const { vault, scopes, policy, nowMs } = options;
    const decisions = new Map();
    let evaluated = 0;
    let forgottenPurgeBacklog = 0;
    const uniqueScopes = [...new Map(scopes.map((scope) => [memoryScopeKey(scope), scope])).values()];
    for (const scope of uniqueScopes) {
        const records = vault.listMemoriesByScope(scope, { includeExpired: true });
        evaluated += records.length;
        for (const decision of classifyScope(scope, records, nowMs, policy)) {
            decisions.set(decision.memoryId, decision);
        }
        forgottenPurgeBacklog += countPurgeBacklog(vault, scope, policy, nowMs);
    }
    const forgotten = [...decisions.values()];
    for (const decision of forgotten) {
        vault.deleteMemory(decision.memoryId, {
            tombstone: true,
            forgetterSurface: "retention",
            reason: decision.reason,
            nowMs,
        });
    }
    return {
        evaluated,
        forgotten,
        kept: evaluated - forgotten.length,
        byReason: countByReason(forgotten),
        forgottenPurgeBacklog,
    };
}
function countByReason(decisions) {
    const counts = {
        "expire-age": 0,
        "expire-proposal": 0,
        "evict-overflow": 0,
    };
    for (const d of decisions) {
        counts[d.reason] += 1;
    }
    return counts;
}
function countPurgeBacklog(vault, scope, policy, nowMs) {
    if (policy.purgeForgottenAfterMs === undefined) {
        return 0;
    }
    const tombstones = vault.listTombstonesByScope(scope);
    let count = 0;
    for (const t of tombstones) {
        if (nowMs - t.forgottenAt > policy.purgeForgottenAfterMs) {
            count += 1;
        }
    }
    return count;
}
