/**
 * UsherSignalStore — durable, read-only-from-the-outside store of the Usher's
 * re-surface signals + its precision metrics (rung 4 of continuous-working-awareness).
 *
 * Signal-only: the Usher writes suggestions here; consumers PULL them
 * (GET /usher/signals). It never injects. The metrics (fired / acted) — paired
 * with the HumanAsDetectorLog miss-map — are the precision read that gates rung 5.
 *
 * File-backed per topic at {stateDir}/usher/<topicId>.json. Atomic writes
 * (temp+rename); best-effort (metering/signalling must never throw into the
 * message path). Spec: docs/specs/cwa-usher.md §3–4.
 */
import fs from 'node:fs';
import path from 'node:path';
import { randomUUID } from 'node:crypto';
const MAX_SIGNALS_PER_TOPIC = 50;
function emptyFile(topicId) {
    return { topicId, signals: [], metrics: { fired: 0, acted: 0, last_fired_at: null }, schemaVersion: 1 };
}
export class UsherSignalStore {
    dir;
    constructor(stateDir) {
        this.dir = path.join(stateDir, 'usher');
        try {
            fs.mkdirSync(this.dir, { recursive: true });
        }
        catch (err) {
            console.error(`[UsherSignalStore] mkdir failed: ${err}`);
        }
    }
    filePath(topicId) {
        return path.join(this.dir, `${topicId}.json`);
    }
    load(topicId) {
        try {
            const fp = this.filePath(topicId);
            if (fs.existsSync(fp)) {
                const parsed = JSON.parse(fs.readFileSync(fp, 'utf-8'));
                if (!Array.isArray(parsed.signals))
                    parsed.signals = [];
                if (!parsed.metrics)
                    parsed.metrics = emptyFile(topicId).metrics;
                return parsed;
            }
        }
        catch (err) {
            console.error(`[UsherSignalStore] corrupt file for ${topicId}, fresh: ${err}`);
        }
        return emptyFile(topicId);
    }
    save(file) {
        try {
            const fp = this.filePath(file.topicId);
            const tmp = `${fp}.tmp-${process.pid}-${Date.now()}`;
            fs.writeFileSync(tmp, JSON.stringify(file, null, 2));
            fs.renameSync(tmp, fp);
        }
        catch (err) {
            console.error(`[UsherSignalStore] save failed: ${err}`);
        }
    }
    /** Record a fired signal (best-effort; never throws). Returns the signal id, or null. */
    recordSignal(topicId, s) {
        try {
            const file = this.load(topicId);
            const signal = {
                id: `usig-${randomUUID()}`,
                contextRef: s.contextRef,
                contextText: s.contextText,
                reason: s.reason,
                turn: s.turn,
                at: s.at ?? new Date().toISOString(),
                acted: false,
            };
            file.signals.push(signal);
            if (file.signals.length > MAX_SIGNALS_PER_TOPIC) {
                file.signals = file.signals.slice(-MAX_SIGNALS_PER_TOPIC);
            }
            file.metrics.fired += 1;
            file.metrics.last_fired_at = signal.at;
            this.save(file);
            return signal.id;
        }
        catch (err) {
            console.error(`[UsherSignalStore] recordSignal failed: ${err}`);
            return null;
        }
    }
    /**
     * Mark a signal as acted-on (precision numerator). Best-effort, idempotent
     * (a second call on the same signal is a no-op returning false).
     *
     * `opts.via` records WHICH correlation path confirmed usefulness ('use' =
     * the agent used it in a reply; 'miss' = the user had to correct on it) and
     * is reflected in the split metrics. `opts.at` lets callers stamp a
     * deterministic timestamp (tests); defaults to now.
     */
    markActed(topicId, signalId, opts) {
        try {
            const file = this.load(topicId);
            const sig = file.signals.find(x => x.id === signalId);
            if (!sig || sig.acted)
                return false;
            sig.acted = true;
            if (opts?.via)
                sig.actedVia = opts.via;
            sig.actedAt = opts?.at ?? new Date().toISOString();
            file.metrics.acted += 1;
            if (opts?.via === 'use')
                file.metrics.acted_by_use = (file.metrics.acted_by_use ?? 0) + 1;
            else if (opts?.via === 'miss')
                file.metrics.acted_by_miss = (file.metrics.acted_by_miss ?? 0) + 1;
            this.save(file);
            return true;
        }
        catch {
            return false;
        }
    }
    getSignals(topicId, limit = 20) {
        const file = this.load(topicId);
        return file.signals.slice(-limit).reverse();
    }
    getMetrics(topicId) {
        return this.load(topicId).metrics;
    }
}
//# sourceMappingURL=UsherSignalStore.js.map