/**
 * RetentionEnforcementLogStore -- F-NEW-RETENTION-ENFORCEMENT-TRACKER
 *
 * Persists per-store enforcement run history so the
 * GET /api/v1/retention/store-enforcement/:tenantId endpoint can return
 * real timestamps instead of null, upgrading dataMode from
 * 'policy-matrix-only' to 'full'.
 *
 * Architecture note:
 *   DbRetentionSweep runs globally (not per-tenant) and sweeps tables by
 *   retentionAt. AggregatorReconciler runs per-tenant. RetentionManager
 *   is write-time.
 *
 *   For the 'db' store we record one log entry per DbRetentionSweep.runOnce()
 *   invocation, aggregating across all tenants in that run. The storeName is
 *   always 'db' for these entries, and tenantId is set to the special sentinel
 *   GLOBAL_DB_SWEEP_TENANT to indicate a global (multi-tenant) sweep.
 *
 *   For log-aggregator stores (splunk, datadog, sentinel) the AggregatorReconciler
 *   calls recordRun() per-tenant after each reconcileTenant() call. storeName
 *   is 'splunk' | 'datadog' | 'sentinel'.
 *
 *   The endpoint for tenantId=X reads rows for that tenant OR the global sweep
 *   sentinel so it can surface db sweep history even before per-tenant tracking
 *   is wired.
 *
 * In-memory implementation is the default (sidecar mode, tests). The
 * InMemoryEnforcementLogStore is used when no DATABASE_URL is configured.
 * A PostgreSQL-backed implementation wrapping PrismaClient can be substituted
 * by passing an adapter to the store-enforcement endpoint.
 *
 * @connexum/ai-governance
 */
import * as crypto from 'node:crypto';
// ---------------------------------------------------------------------------
// Sentinel tenant ID for global (non-per-tenant) DB sweeps
// ---------------------------------------------------------------------------
export const GLOBAL_DB_SWEEP_TENANT = '__global_db_sweep__';
// ---------------------------------------------------------------------------
// In-memory implementation
// ---------------------------------------------------------------------------
export class InMemoryEnforcementLogStore {
    entries = [];
    async recordRun(input) {
        const entry = {
            id: crypto.randomUUID(),
            tenantId: input.tenantId,
            storeName: input.storeName,
            startedAt: input.startedAt,
            completedAt: input.completedAt,
            status: input.status,
            recordsScanned: input.recordsScanned,
            recordsPurged: input.recordsPurged,
            failureReason: input.failureReason ?? null,
        };
        this.entries.push(entry);
        return entry;
    }
    async latestCompletedRunsPerStore(tenantId) {
        const result = new Map();
        // Completed entries for this tenant OR the global db-sweep sentinel.
        const relevant = this.entries.filter((e) => e.status === 'completed' &&
            (e.tenantId === tenantId || e.tenantId === GLOBAL_DB_SWEEP_TENANT));
        // Per-store: find the entry with the latest completedAt.
        for (const entry of relevant) {
            const existing = result.get(entry.storeName);
            if (!existing) {
                result.set(entry.storeName, entry);
            }
            else {
                const existingAt = existing.completedAt?.getTime() ?? 0;
                const entryAt = entry.completedAt?.getTime() ?? 0;
                if (entryAt > existingAt) {
                    result.set(entry.storeName, entry);
                }
            }
        }
        return result;
    }
    async hasAnyCompletedRun(tenantId) {
        return this.entries.some((e) => e.status === 'completed' &&
            (e.tenantId === tenantId || e.tenantId === GLOBAL_DB_SWEEP_TENANT));
    }
    async recentFailedRuns(tenantId, sinceHours) {
        const cutoffMs = Date.now() - sinceHours * 3600 * 1000;
        return this.entries
            .filter((e) => e.status === 'failed' &&
            (e.tenantId === tenantId || e.tenantId === GLOBAL_DB_SWEEP_TENANT) &&
            e.startedAt.getTime() >= cutoffMs)
            .sort((a, b) => b.startedAt.getTime() - a.startedAt.getTime());
    }
    /** Test helper: return all stored entries. */
    allEntries() {
        return this.entries;
    }
    /** Test helper: clear all stored entries. */
    clear() {
        this.entries.length = 0;
    }
}
// ---------------------------------------------------------------------------
// trackEnforcementRun -- convenience wrapper used by DbRetentionSweep and
// AggregatorReconciler to record a run without try/catch boilerplate at the
// call site.
// ---------------------------------------------------------------------------
/**
 * Wraps an enforcement run function, records start + completion/failure to
 * the log store, and re-throws on failure (so the caller's error path is
 * preserved).
 *
 * @param logStore   The EnforcementLogStore to record to.
 * @param tenantId   Tenant this run belongs to (use GLOBAL_DB_SWEEP_TENANT for global sweeps).
 * @param storeName  Which store this run operates on.
 * @param runFn      The actual enforcement function. Must return { recordsScanned, recordsPurged }.
 */
export async function trackEnforcementRun(logStore, tenantId, storeName, runFn) {
    const startedAt = new Date();
    try {
        const result = await runFn();
        await logStore.recordRun({
            tenantId,
            storeName,
            startedAt,
            completedAt: new Date(),
            status: 'completed',
            recordsScanned: result.recordsScanned,
            recordsPurged: result.recordsPurged,
        });
        return result;
    }
    catch (err) {
        await logStore.recordRun({
            tenantId,
            storeName,
            startedAt,
            completedAt: new Date(),
            status: 'failed',
            recordsScanned: 0,
            recordsPurged: 0,
            failureReason: String(err),
        });
        throw err;
    }
}
//# sourceMappingURL=enforcement-log-store.js.map