// Forget / delete selection + envelope construction.
//
// Two pure functions:
//
//   selectMemoriesForForget — filters a caller-supplied MemoryRecord array down to the
//   subset matched by a ForgetSelector. Default `protectPinned: true` excludes pinned
//   memories (the user must explicitly unpin first). Default `protectArchived: false`
//   does NOT exclude archived memories (they are already out of active retrieval; a
//   retention sweep is allowed to re-select them).
//
//   buildForgetOperations — maps each selected MemoryRecord to a MemoryForget envelope.
//   The contracts type literally pins `userAcknowledgedDestructive` to the literal
//   `true`, so the envelope cannot structurally be constructed without acknowledgement.
//   Every envelope is revalidated through validateMemoryForget before returning.
//
// The two-stage shape is deliberate: callers can present the selection to the user for
// confirmation (Memory Center UI #211) before materialising the destructive envelopes.
import { validateMemoryForget } from "@oscharko-dev/keiko-contracts/memory";
import { GovernanceError } from "./errors.js";
// ─── Scope coordinate equality ────────────────────────────────────────────────
// Pure: two scopes match when their discriminator AND coordinate field match exactly.
// Implemented via a canonical "kind:coordinate" string projection to collapse the
// per-kind branching (memory pattern from issue #205 scopeCoordinateKey).
function scopeCoordinateKey(scope) {
    switch (scope.kind) {
        case "user":
            return `user:${scope.userId}`;
        case "workspace":
            return `workspace:${scope.workspaceId}`;
        case "project":
            return `project:${scope.projectId}`;
        case "workflow":
            return `workflow:${scope.workflowDefinitionId}`;
        case "global":
            return "global:";
        default: {
            const _exhaustive = scope;
            void _exhaustive;
            return "unknown:";
        }
    }
}
function scopeEquals(a, b) {
    return scopeCoordinateKey(a) === scopeCoordinateKey(b);
}
// ─── Per-selector filter predicates ───────────────────────────────────────────
function matchById(record, selector) {
    return record.id === selector.memoryId;
}
function matchByScope(record, selector) {
    return scopeEquals(record.scope, selector.scope);
}
function matchByType(record, selector) {
    return scopeEquals(record.scope, selector.scope) && record.type === selector.type;
}
function matchBySourceConversation(record, selector) {
    if (!scopeEquals(record.scope, selector.scope))
        return false;
    return record.provenance.sourceConversationId === selector.sourceConversationId;
}
function matchByTimeWindow(record, selector, nowMs) {
    if (!scopeEquals(record.scope, selector.scope))
        return false;
    return record.createdAt <= nowMs - selector.olderThanMs;
}
function assertSelectorWellFormed(selector) {
    if (selector.kind === "by-time-window") {
        if (!Number.isFinite(selector.olderThanMs) || selector.olderThanMs < 0) {
            throw new GovernanceError("invalid-selector-input", "by-time-window olderThanMs must be a finite non-negative number");
        }
    }
}
function applySelector(record, selector, nowMs) {
    switch (selector.kind) {
        case "by-id":
            return matchById(record, selector);
        case "by-scope":
            return matchByScope(record, selector);
        case "by-type":
            return matchByType(record, selector);
        case "by-source-conversation":
            return matchBySourceConversation(record, selector);
        case "by-time-window":
            return matchByTimeWindow(record, selector, nowMs);
        default: {
            // Exhaustiveness gate: a future widening of ForgetSelector surfaces here at
            // compile time and at runtime as an unsupported-selector error.
            const _exhaustive = selector;
            void _exhaustive;
            throw new GovernanceError("unsupported-selector", "unknown ForgetSelector kind");
        }
    }
}
// ─── Selection ────────────────────────────────────────────────────────────────
export function selectMemoriesForForget(memories, selector, options) {
    assertSelectorWellFormed(selector);
    const protectPinned = options.protectPinned ?? true;
    const protectArchived = options.protectArchived ?? false;
    const selected = [];
    for (const record of memories) {
        if (protectPinned && record.pinned)
            continue;
        if (protectArchived && record.status === "archived")
            continue;
        if (record.status === "forgotten")
            continue;
        if (!applySelector(record, selector, options.nowMs))
            continue;
        selected.push(record);
    }
    return selected;
}
// ─── Envelope construction ────────────────────────────────────────────────────
const DEFAULT_FORGET_REASON = "user-requested forget";
function buildForgetEnvelope(record, context, options) {
    const env = {
        schemaVersion: "1",
        memoryId: record.id,
        reviewerId: context.reviewerId,
        forgottenAt: context.nowMs,
        reason: options.reason ?? DEFAULT_FORGET_REASON,
        userAcknowledgedDestructive: true,
    };
    const v = validateMemoryForget(env);
    if (!v.ok) {
        throw new GovernanceError("envelope-validation-failed", `forget envelope failed contracts validation for memory ${record.id}`, v.errors);
    }
    return env;
}
export function buildForgetOperations(memories, context, options) {
    // `writeTombstone` is currently observed only at the storage seam (vault #206 writes
    // an audit tombstone unconditionally). The flag remains on the option bundle as a
    // future-extension surface and as a forcing function for caller intent — the BFF
    // route handler MUST decide consciously whether the destructive operation is
    // tombstone-yielding.
    void options.writeTombstone;
    const envelopes = [];
    for (const record of memories) {
        envelopes.push(buildForgetEnvelope(record, context, options));
    }
    return envelopes;
}
