/**
 * RemoteAckStore — durable queue for operator-bound attention acks that target
 * an attention item OWNED BY A DIFFERENT MACHINE (WS4.1 follow-up, CMT-1416).
 *
 * The problem this closes: when an operator acknowledges/resolves a pooled
 * attention item whose owner is briefly offline, the ack PATCH is lost — the
 * operator's intent evaporates and the item reappears OPEN on the owner's
 * return. This store persists the ack INTENT (with the authenticated operator
 * principal that performed it) so it survives the owner being dark; a drain
 * tick + a boot sweep re-deliver it when the owner comes back.
 *
 * Design constraints (mirrors RemoteCloseAudit / PendingInboundStore):
 *   - Append-then-compact JSONL under logs/ — no DB dependency.
 *   - Each pending ack is idempotent on (itemId, targetMachineId): a re-ack of
 *     the same item just refreshes the intent, never stacks duplicates.
 *   - The operator principal is carried as data the OWNER revalidates at apply
 *     time — this store never authorizes anything, it only remembers intent.
 *   - Best-effort writes; a failed append/compact logs loudly (an unrecorded
 *     ack-intent deserves a trace) but never throws into the ack path.
 *
 * This file ships behind the dark gate multiMachine.seamlessness.ws41DurableAck:
 * when off, the route that feeds it 503s and the store is never constructed, so
 * a single-machine or flag-off agent is a strict no-op.
 */
import fs from 'node:fs';
import path from 'node:path';
export class RemoteAckStore {
    log;
    filePath;
    /** Keyed on `${itemId}::${targetMachineId}` — idempotent intent. */
    pending = new Map();
    loaded = false;
    constructor(stateDir, log = console.log) {
        this.log = log;
        this.filePath = path.join(stateDir, '..', 'logs', 'remote-ack-queue.jsonl');
    }
    key(itemId, targetMachineId) {
        return `${itemId}::${targetMachineId}`;
    }
    ensureLoaded() {
        if (this.loaded)
            return;
        this.loaded = true;
        try {
            const raw = fs.readFileSync(this.filePath, 'utf-8');
            for (const line of raw.split('\n')) {
                const t = line.trim();
                if (!t)
                    continue;
                try {
                    const entry = JSON.parse(t);
                    const k = this.key(entry.itemId, entry.targetMachineId);
                    if (entry._deleted) {
                        this.pending.delete(k);
                    }
                    else {
                        this.pending.set(k, entry);
                    }
                }
                catch {
                    // @silent-fallback-ok — a torn last line (crash mid-append) is skipped;
                    // the rest of the durable log still loads.
                }
            }
        }
        catch {
            // @silent-fallback-ok — absent file reads as empty queue (first run).
        }
    }
    append(entry) {
        try {
            fs.mkdirSync(path.dirname(this.filePath), { recursive: true });
            /* state-registry: remote-ack-queue */
            fs.appendFileSync(this.filePath, JSON.stringify(entry) + '\n');
        }
        catch (err) {
            // Loud, not silent: a lost ack-intent reappears as a re-OPENed item to the
            // operator later — worth a trace even when the log file is the failure.
            this.log(`[remote-ack] QUEUE APPEND FAILED (intent held in memory only): ${err instanceof Error ? err.message : String(err)}`);
        }
    }
    /**
     * Record (or refresh) the operator's ack intent for an item owned elsewhere.
     * Idempotent on (itemId, targetMachineId): a repeat ack updates status/principal
     * and resets attempts rather than stacking a second pending row.
     */
    enqueue(intent) {
        this.ensureLoaded();
        const k = this.key(intent.itemId, intent.targetMachineId);
        const existing = this.pending.get(k);
        const entry = {
            ...intent,
            enqueuedAt: existing?.enqueuedAt ?? new Date().toISOString(),
            attempts: 0,
            lastOutcome: 'pending',
        };
        this.pending.set(k, entry);
        this.append(entry);
        return entry;
    }
    /** Mark a delivery attempt's outcome (drain observability + backoff input). */
    recordAttempt(itemId, targetMachineId, outcome) {
        this.ensureLoaded();
        const k = this.key(itemId, targetMachineId);
        const entry = this.pending.get(k);
        if (!entry)
            return;
        entry.attempts += 1;
        entry.lastOutcome = outcome;
        this.append(entry);
    }
    /** Remove a delivered (or owner-rejected-as-stale) intent — terminal. */
    resolve(itemId, targetMachineId) {
        this.ensureLoaded();
        const k = this.key(itemId, targetMachineId);
        if (!this.pending.has(k))
            return;
        this.pending.delete(k);
        this.append({
            itemId,
            targetMachineId,
            status: '',
            operatorUid: '',
            enqueuedAt: new Date().toISOString(),
            attempts: 0,
            _deleted: true,
        });
    }
    /** All still-pending intents (for the drain tick / boot sweep / observability). */
    list() {
        this.ensureLoaded();
        return Array.from(this.pending.values());
    }
    /** Pending intents targeting one machine (drain when that peer comes online). */
    listForMachine(targetMachineId) {
        return this.list().filter((e) => e.targetMachineId === targetMachineId);
    }
    /** Count of still-pending intents (cheap status read). */
    get size() {
        this.ensureLoaded();
        return this.pending.size;
    }
}
//# sourceMappingURL=RemoteAckStore.js.map