"use strict";
/**
 * MultiStoreDeletionWorker -- P4-RET-04 Multi-store deletion enforcement
 *
 * Scheduled worker that sweeps records whose retention window has elapsed
 * across ALL configured stores: filesystem, PostgreSQL, S3 (lifecycle export),
 * and log aggregators (Splunk, Datadog, Azure Sentinel).
 *
 * Design principles:
 *   1. Abort-on-chain-append-failure (AC 16): the retention:deleted audit event
 *      must be appended to the audit chain BEFORE the delete executes.
 *      If chain append fails, the delete is aborted (fail-closed).
 *   2. Legal-hold check: LegalHoldStore.findHoldsMatching() is called per
 *      record. Any active hold emits retention.delete.refused.legal-hold and
 *      skips the record.
 *   3. Cross-store consistency (AC 16b): if FS delete succeeds but DB delete
 *      fails (PT-RET-10), the record is placed in 'failed-delete' state and
 *      a retention.delete.cross-store-failure incident event fires.
 *   4. Tick interval: 60 seconds in production; 15 seconds when any active
 *      pack has healthcareStrict=true (HIPAA/HITECH packs).
 *
 * Spec: P4-RET-01 Sections 2.4, AC 11-16
 *
 * @connexum/ai-governance
 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.MultiStoreDeletionWorker = void 0;
const crypto = __importStar(require("node:crypto"));
// ---------------------------------------------------------------------------
// Healthcare-strict pack IDs (trigger short tick interval)
// ---------------------------------------------------------------------------
const HEALTHCARE_STRICT_PACKS = new Set(['hipaa', 'hitech']);
const PRODUCTION_TICK_MS = 60_000; // 60 seconds
const HEALTHCARE_TICK_MS = 15_000; // 15 seconds
// ---------------------------------------------------------------------------
// MultiStoreDeletionWorker
// ---------------------------------------------------------------------------
class MultiStoreDeletionWorker {
    config;
    timer = null;
    /** In-memory log of records that entered failed-delete state. */
    failedDeletes = [];
    constructor(config) {
        const activePackIds = config.activePackIds ?? [];
        const hasHealthcareStrict = activePackIds.some((id) => HEALTHCARE_STRICT_PACKS.has(id));
        const defaultInterval = hasHealthcareStrict ? HEALTHCARE_TICK_MS : PRODUCTION_TICK_MS;
        this.config = {
            ...config,
            activePackIds,
            tickIntervalMs: config.tickIntervalMs ?? defaultInterval,
        };
    }
    // -------------------------------------------------------------------------
    // Lifecycle
    // -------------------------------------------------------------------------
    start() {
        if (this.timer)
            return;
        this.timer = setInterval(() => {
            void this.runOnce();
        }, this.config.tickIntervalMs);
        // Allow process to exit without waiting for the next tick.
        if (typeof this.timer.unref === 'function') {
            this.timer.unref();
        }
    }
    stop() {
        if (this.timer) {
            clearInterval(this.timer);
            this.timer = null;
        }
    }
    getTickIntervalMs() {
        return this.config.tickIntervalMs;
    }
    getFailedDeletes() {
        return this.failedDeletes;
    }
    // -------------------------------------------------------------------------
    // Core sweep
    // -------------------------------------------------------------------------
    /**
     * Execute a single deletion sweep across all stores.
     *
     * For each expired record (per store):
     *   1. Check legal holds. If any active hold matches -- emit refused event,
     *      skip, continue.
     *   2. Append retention:deleted to the audit chain. If append fails --
     *      abort the delete for this record (PT-RET-9 fail-closed).
     *   3. Execute the deletion in the primary store.
     *   4. If deletion in a secondary store fails -- emit
     *      retention.delete.cross-store-failure incident event and record
     *      the failed-delete state (PT-RET-10).
     */
    async runOnce() {
        const jobId = crypto.randomUUID();
        const startedAt = new Date().toISOString();
        const startMs = Date.now();
        const cutoff = new Date();
        const storeResults = {};
        let totalScanned = 0;
        let totalDeleted = 0;
        let totalHeld = 0;
        let totalFailed = 0;
        for (const store of this.config.stores) {
            const result = await this.sweepStore(store, cutoff, jobId);
            storeResults[store.storeId] = result;
            totalScanned += result.scanned;
            totalDeleted += result.deleted;
            totalHeld += result.held;
            totalFailed += result.failed;
        }
        return {
            jobId,
            startedAt,
            completedAt: new Date().toISOString(),
            durationMs: Date.now() - startMs,
            totalScanned,
            totalDeleted,
            totalHeld,
            totalFailed,
            storeResults,
        };
    }
    // -------------------------------------------------------------------------
    // Per-store sweep
    // -------------------------------------------------------------------------
    async sweepStore(store, cutoff, jobId) {
        let scanned = 0;
        let deleted = 0;
        let held = 0;
        let failed = 0;
        const failedRecordIds = [];
        let expired;
        try {
            expired = await store.listExpiredRecords(cutoff);
        }
        catch {
            // If listing fails treat as 0 records scanned -- do not mask the error.
            return { scanned: 0, deleted: 0, held: 0, failed: 0, failedRecordIds: [] };
        }
        scanned = expired.length;
        for (const record of expired) {
            // ------------------------------------------------------------------
            // Step 1: Legal hold check
            // ------------------------------------------------------------------
            let activeHolds = [];
            try {
                activeHolds = await this.config.holdStore.findHoldsMatching({
                    tenantId: record.tenantId,
                    contractId: record.contractId,
                    agentId: record.agentId,
                    packBinding: record.packBinding,
                });
            }
            catch {
                // If hold check fails, be conservative: treat as held.
                activeHolds = [{ holdId: 'unknown', status: 'active' }];
            }
            if (activeHolds.length > 0) {
                // Record is under an active hold -- emit refused event and skip.
                const refusedAt = new Date().toISOString();
                const holdVerifier = crypto
                    .createHash('sha256')
                    .update(`${jobId}:${record.id}:hold-refused:${refusedAt}`)
                    .digest('hex');
                await this.emitSafe({
                    eventType: 'retention.delete.refused.legal-hold',
                    recordId: record.id,
                    packBindings: record.packBinding ?? [],
                    deletedAt: refusedAt,
                    verifier: holdVerifier,
                    tenantId: record.tenantId,
                    dataClass: record.dataClass,
                    reason: `Active legal hold(s): ${activeHolds.map((h) => h.holdId).join(', ')}`,
                });
                held++;
                continue;
            }
            // ------------------------------------------------------------------
            // Step 2: Append to audit chain BEFORE deleting (PT-RET-9 fail-closed)
            // ------------------------------------------------------------------
            const deletedAt = new Date().toISOString();
            const verifier = crypto
                .createHash('sha256')
                .update(`${jobId}:${record.id}:${store.storeId}:${deletedAt}`)
                .digest('hex');
            const auditEvent = {
                eventType: 'retention.deleted',
                recordId: record.id,
                packBindings: record.packBinding ?? [],
                deletedAt,
                storesAffected: [store.storeId],
                verifier,
                tenantId: record.tenantId,
                dataClass: record.dataClass,
            };
            let chainEntryId;
            try {
                chainEntryId = await this.config.auditChain.append(auditEvent);
            }
            catch (chainErr) {
                // PT-RET-9: chain append failure -- abort delete, do NOT proceed.
                await this.emitSafe({
                    ...auditEvent,
                    eventType: 'retention.delete.chain-append-failed',
                    reason: `Audit chain append failed: ${chainErr.message}. Delete aborted (fail-closed).`,
                });
                failed++;
                failedRecordIds.push(record.id);
                continue;
            }
            // chainEntryId suppressed if not used -- reference to satisfy linter
            void chainEntryId;
            // ------------------------------------------------------------------
            // Step 3: Execute deletion
            // ------------------------------------------------------------------
            try {
                await store.deleteRecord(record);
                deleted++;
            }
            catch (deleteErr) {
                // PT-RET-10: delete failed after chain append.
                // Place record in failed-delete state and emit incident event.
                const failedRecord = {
                    recordId: record.id,
                    tenantId: record.tenantId,
                    dataClass: record.dataClass,
                    failedStore: store.storeId,
                    succeededStores: [],
                    failedAt: new Date().toISOString(),
                    reason: deleteErr.message,
                };
                this.failedDeletes.push(failedRecord);
                await this.emitSafe({
                    eventType: 'retention.delete.cross-store-failure',
                    recordId: record.id,
                    packBindings: record.packBinding ?? [],
                    deletedAt: new Date().toISOString(),
                    storesAffected: [],
                    verifier,
                    tenantId: record.tenantId,
                    dataClass: record.dataClass,
                    failedStore: store.storeId,
                    reason: deleteErr.message,
                });
                failed++;
                failedRecordIds.push(record.id);
            }
        }
        return { scanned, deleted, held, failed, failedRecordIds };
    }
    // -------------------------------------------------------------------------
    // Safe audit emit (does not throw -- used for bookkeeping events)
    // -------------------------------------------------------------------------
    async emitSafe(event) {
        try {
            await this.config.auditChain.append(event);
        }
        catch {
            // Secondary emit failure is logged but not propagated.
            // The primary append-before-delete gate is enforced in sweepStore().
        }
    }
}
exports.MultiStoreDeletionWorker = MultiStoreDeletionWorker;
//# sourceMappingURL=multi-store-deletion-worker.js.map