"use strict";
/**
 * RetentionManager -- Enforces data retention policies per compliance framework.
 *
 * Manages lifecycle of audit logs, session data, and incident records
 * with framework-specific retention periods.
 *
 * Retention periods (configurable per pack):
 *   - HIPAA: 6 years (45 CFR 164.530(j))
 *   - PCI DSS: 12 months (Req 10.7)
 *   - SOC 2: 12 months (CC7.4)
 *   - GDPR: Purpose-dependent (Art. 5(1)(e))
 *   - SOX: 7 years (SOX Act Section 802)
 *
 * P4-RET-04 upgrade (AC 11):
 *   - enforce() now consults PolicyMatrix.resolve() per file for dynamic
 *     retention days and action (replaces per-category config lookup).
 *   - enforce() queries LegalHoldStore.findHoldsMatching() before each
 *     delete. Any active hold aborts the delete and emits
 *     'retention:blocked-by-legal-hold' via the audit emitter.
 *   - Every actual delete emits 'retention:deleted' via the audit emitter.
 *   - Audit-chain append is fail-closed: if append throws, the delete does
 *     NOT proceed (prevents orphaned deletes -- AC 16).
 *   - Per-file DataClassTag is read from a sidecar '<file>.meta.json'
 *     when present; falls back to category-level tag from managedDir config.
 *
 * @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.RetentionManager = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const crypto = __importStar(require("crypto"));
const fs_1 = require("../util/fs");
const log_1 = require("../util/log");
const log = (0, log_1.createLogger)('retention-manager');
class RetentionManager {
    config;
    policyMap = new Map();
    constructor(config) {
        this.config = {
            dryRun: false,
            ...config,
        };
        (0, fs_1.ensureDir)(config.logDir);
        for (const policy of config.policies) {
            this.policyMap.set(policy.category, policy);
            if (policy.action === 'archive' && policy.archiveDir) {
                (0, fs_1.ensureDir)(policy.archiveDir);
            }
        }
    }
    /**
     * Run retention enforcement across all managed directories.
     *
     * Legacy sync path (no PolicyMatrix / LegalHoldStore). When P4-RET-04
     * options (policyMatrix, legalHoldStore, auditEmitter) are configured,
     * use enforceAsync() instead to get hold-checked, audit-chained enforcement.
     *
     * This method is preserved for backward compatibility with existing tests
     * and callers that do not yet need P4-RET-04 semantics.
     */
    enforce() {
        return this.enforceSync();
    }
    /**
     * Async enforcement (P4-RET-04). Returns a Promise<RetentionReport>.
     * This is the primary method when PolicyMatrix + LegalHoldStore are wired.
     *
     * For backward compat, callers that do not need P4-RET-04 semantics can
     * call enforceSync() which has the old behaviour (no hold check, no matrix).
     */
    async enforceAsync() {
        const jobId = this.config.jobId ?? crypto.randomUUID();
        const report = {
            timestamp: new Date().toISOString(),
            totalFilesScanned: 0,
            filesDeleted: 0,
            filesArchived: 0,
            filesRetained: 0,
            actions: [],
            policiesApplied: Array.from(this.policyMap.keys()),
        };
        for (const dir of this.config.managedDirs) {
            if (!fs.existsSync(dir.path))
                continue;
            const files = this.listFiles(dir.path, dir.filePattern);
            for (const file of files) {
                report.totalFilesScanned++;
                const action = await this.processFileAsync(file, dir, jobId);
                report.actions.push(action);
                switch (action.action) {
                    case 'deleted':
                        report.filesDeleted++;
                        break;
                    case 'archived':
                        report.filesArchived++;
                        break;
                    case 'retained':
                        report.filesRetained++;
                        break;
                }
            }
        }
        const logFile = path.join(this.config.logDir, 'retention-actions.jsonl');
        (0, fs_1.appendJsonl)(logFile, {
            type: 'retention-report',
            ...report,
            actions: report.actions.length,
        });
        log.info(`Retention enforcement complete`, {
            jobId,
            scanned: report.totalFilesScanned,
            deleted: report.filesDeleted,
            archived: report.filesArchived,
            retained: report.filesRetained,
        });
        return report;
    }
    /**
     * Legacy synchronous enforcement. No PolicyMatrix, no LegalHoldStore,
     * no audit emission. Maintained for backward compatibility with callers
     * that have not yet migrated to P4-RET-04.
     */
    enforceSync() {
        const report = {
            timestamp: new Date().toISOString(),
            totalFilesScanned: 0,
            filesDeleted: 0,
            filesArchived: 0,
            filesRetained: 0,
            actions: [],
            policiesApplied: Array.from(this.policyMap.keys()),
        };
        for (const dir of this.config.managedDirs) {
            const policy = this.policyMap.get(dir.category);
            if (!policy) {
                log.warn(`No retention policy for category: ${dir.category}`);
                continue;
            }
            if (!fs.existsSync(dir.path))
                continue;
            const files = this.listFiles(dir.path, dir.filePattern);
            for (const file of files) {
                report.totalFilesScanned++;
                const action = this.processFile(file, policy);
                report.actions.push(action);
                switch (action.action) {
                    case 'deleted':
                        report.filesDeleted++;
                        break;
                    case 'archived':
                        report.filesArchived++;
                        break;
                    case 'retained':
                        report.filesRetained++;
                        break;
                }
            }
        }
        const logFile = path.join(this.config.logDir, 'retention-actions.jsonl');
        (0, fs_1.appendJsonl)(logFile, {
            type: 'retention-report',
            ...report,
            actions: report.actions.length,
        });
        log.info(`Retention enforcement complete`, {
            scanned: report.totalFilesScanned,
            deleted: report.filesDeleted,
            archived: report.filesArchived,
            retained: report.filesRetained,
        });
        return report;
    }
    /**
     * Check what would happen without executing (dry run).
     * Uses enforceSync() for backward compat.
     */
    preview() {
        const savedDryRun = this.config.dryRun;
        this.config.dryRun = true;
        const report = this.enforceSync();
        this.config.dryRun = savedDryRun;
        return report;
    }
    /**
     * Get the effective retention policy for a category.
     */
    getPolicy(category) {
        return this.policyMap.get(category) || null;
    }
    /**
     * Get all policies.
     */
    getPolicies() {
        return Array.from(this.policyMap.values());
    }
    /**
     * Check if a specific file should be retained or expired.
     */
    checkFile(filePath, category) {
        const policy = this.policyMap.get(category);
        if (!policy)
            return { expired: false, ageDays: 0, retentionDays: 0 };
        const ageDays = this.getFileAgeDays(filePath);
        return {
            expired: ageDays > policy.retentionDays,
            ageDays,
            retentionDays: policy.retentionDays,
        };
    }
    // --- Private ---
    /**
     * Read per-file DataClassTag from a sidecar <file>.meta.json.
     * Falls back to the managedDir's defaultTag if no sidecar exists.
     * Returns null if neither is available.
     */
    readSidecarTag(filePath, dirDefaultTag) {
        const sidecarPath = `${filePath}.meta.json`;
        try {
            if (fs.existsSync(sidecarPath)) {
                const raw = fs.readFileSync(sidecarPath, 'utf-8');
                const meta = JSON.parse(raw);
                if (meta.dataClassTag)
                    return meta.dataClassTag;
            }
        }
        catch {
            // Sidecar unreadable -- fall through to default
        }
        return dirDefaultTag ?? null;
    }
    /**
     * P4-RET-04 async file processor. Handles PolicyMatrix resolution,
     * LegalHoldStore check, and fail-closed audit emission before delete.
     */
    async processFileAsync(filePath, dir, jobId) {
        const ageDays = this.getFileAgeDays(filePath);
        const now = new Date().toISOString();
        const category = dir.category;
        // --- Resolve retention policy ---
        let retentionDays;
        let action;
        let dataClassTag;
        let resolvedRetention;
        const tag = this.readSidecarTag(filePath, dir.defaultTag);
        if (tag && this.config.policyMatrix) {
            try {
                resolvedRetention = this.config.policyMatrix.resolve({
                    tenantId: tag.tenantId,
                    contractId: tag.contractId,
                    dataClass: tag.dataClass,
                    packBinding: tag.packBinding,
                });
                retentionDays = resolvedRetention.retentionDays;
                action = resolvedRetention.action;
                dataClassTag = tag;
            }
            catch {
                // PolicyMatrix threw (e.g. MissingRetentionPolicyError) -- skip file
                return {
                    timestamp: now,
                    file: filePath,
                    category,
                    action: 'skipped',
                    reason: 'PolicyMatrix could not resolve retention policy; skipping until policy is defined.',
                    fileAgeDays: ageDays,
                    retentionDays: 0,
                    dryRun: this.config.dryRun,
                    dataClassTag: tag ?? undefined,
                };
            }
        }
        else {
            // Fallback to legacy category policy
            const policy = this.policyMap.get(category);
            if (!policy) {
                log.warn(`No retention policy for category: ${category}`);
                return {
                    timestamp: now,
                    file: filePath,
                    category,
                    action: 'skipped',
                    reason: `No policy for category "${category}"`,
                    fileAgeDays: ageDays,
                    retentionDays: 0,
                    dryRun: this.config.dryRun,
                };
            }
            retentionDays = policy.retentionDays;
            action = policy.action;
            dataClassTag = tag ?? undefined;
        }
        // --- Not expired ---
        if (ageDays <= retentionDays) {
            return {
                timestamp: now,
                file: filePath,
                category,
                action: 'retained',
                reason: `Within retention period (${ageDays}d / ${retentionDays}d)`,
                fileAgeDays: ageDays,
                retentionDays,
                dryRun: this.config.dryRun,
                dataClassTag,
            };
        }
        // --- Expired: legal hold check (AC 11) ---
        if (this.config.legalHoldStore) {
            const record = {
                tenantId: tag?.tenantId,
                contractId: tag?.contractId,
                packBinding: tag?.packBinding,
            };
            const holds = await this.config.legalHoldStore.findHoldsMatching(record);
            if (holds.length > 0) {
                const holdId = holds[0].holdId;
                // Emit blocked event (best-effort -- spec says emit but don't fail close on block)
                if (this.config.auditEmitter) {
                    try {
                        await this.config.auditEmitter('retention:blocked-by-legal-hold', {
                            file: filePath,
                            category,
                            holdId,
                            dataClass: tag?.dataClass,
                            tenantId: tag?.tenantId,
                            jobId,
                            store: 'fs',
                            legalHoldChecked: true,
                        });
                    }
                    catch {
                        // Non-fatal: emit failure on a block event is logged but does not change behaviour
                        log.warn(`Failed to emit retention:blocked-by-legal-hold for ${filePath}`);
                    }
                }
                log.info(`Skipping ${filePath}: blocked by legal hold ${holdId}`);
                return {
                    timestamp: now,
                    file: filePath,
                    category,
                    action: 'skipped',
                    reason: `Blocked by legal hold ${holdId}`,
                    fileAgeDays: ageDays,
                    retentionDays,
                    dryRun: this.config.dryRun,
                    dataClassTag,
                };
            }
        }
        // --- Dry run ---
        if (this.config.dryRun) {
            return {
                timestamp: now,
                file: filePath,
                category,
                action: action === 'archive' ? 'archived' : 'deleted',
                reason: `[DRY RUN] Would ${action} (${ageDays}d > ${retentionDays}d)`,
                fileAgeDays: ageDays,
                retentionDays,
                dryRun: true,
                dataClassTag,
            };
        }
        // --- Fail-closed audit emit before delete (AC 16) ---
        if (this.config.auditEmitter) {
            try {
                await this.config.auditEmitter('retention:deleted', {
                    recordId: filePath,
                    dataClass: tag?.dataClass,
                    tenantId: tag?.tenantId,
                    contractId: tag?.contractId,
                    policySource: resolvedRetention?.policySource ?? 'default',
                    retentionDaysApplied: retentionDays,
                    jobId,
                    legalHoldChecked: this.config.legalHoldStore != null,
                    store: 'fs',
                });
            }
            catch (err) {
                // Audit chain append failed -- DO NOT proceed with delete (fail-closed, AC 16)
                log.error(`Audit chain append failed for ${filePath}; skipping delete to prevent orphaned delete`);
                return {
                    timestamp: now,
                    file: filePath,
                    category,
                    action: 'skipped',
                    reason: `Audit chain append failed: ${err.message}`,
                    fileAgeDays: ageDays,
                    retentionDays,
                    dryRun: false,
                    dataClassTag,
                };
            }
        }
        // --- Execute delete/archive ---
        if (action === 'archive') {
            const policy = this.policyMap.get(category);
            if (policy?.action === 'archive' && policy.archiveDir) {
                return this.archiveFile(filePath, policy, ageDays, dataClassTag);
            }
        }
        return this.deleteFile(filePath, { category, retentionDays, reference: resolvedRetention?.policySource ?? category, action }, ageDays, dataClassTag);
    }
    processFile(filePath, policy) {
        const ageDays = this.getFileAgeDays(filePath);
        const now = new Date().toISOString();
        if (ageDays <= policy.retentionDays) {
            return {
                timestamp: now,
                file: filePath,
                category: policy.category,
                action: 'retained',
                reason: `Within retention period (${ageDays}d / ${policy.retentionDays}d)`,
                fileAgeDays: ageDays,
                retentionDays: policy.retentionDays,
                dryRun: this.config.dryRun,
            };
        }
        // File has expired
        if (this.config.dryRun) {
            return {
                timestamp: now,
                file: filePath,
                category: policy.category,
                action: policy.action === 'archive' ? 'archived' : 'deleted',
                reason: `[DRY RUN] Would ${policy.action} (${ageDays}d > ${policy.retentionDays}d, ref: ${policy.reference})`,
                fileAgeDays: ageDays,
                retentionDays: policy.retentionDays,
                dryRun: true,
            };
        }
        if (policy.action === 'archive' && policy.archiveDir) {
            return this.archiveFile(filePath, policy, ageDays);
        }
        return this.deleteFile(filePath, policy, ageDays);
    }
    archiveFile(filePath, policy, ageDays, dataClassTag) {
        const now = new Date().toISOString();
        try {
            const dest = path.join(policy.archiveDir, path.basename(filePath));
            fs.copyFileSync(filePath, dest);
            fs.unlinkSync(filePath);
            log.info(`Archived: ${filePath} -> ${dest}`);
            return {
                timestamp: now,
                file: filePath,
                category: policy.category,
                action: 'archived',
                reason: `Archived after ${ageDays}d (max ${policy.retentionDays}d, ref: ${policy.reference})`,
                fileAgeDays: ageDays,
                retentionDays: policy.retentionDays,
                dryRun: false,
                dataClassTag,
            };
        }
        catch (err) {
            log.error(`Failed to archive ${filePath}: ${err.message}`);
            return {
                timestamp: now,
                file: filePath,
                category: policy.category,
                action: 'skipped',
                reason: `Archive failed: ${err.message}`,
                fileAgeDays: ageDays,
                retentionDays: policy.retentionDays,
                dryRun: false,
                dataClassTag,
            };
        }
    }
    deleteFile(filePath, policy, ageDays, dataClassTag) {
        const now = new Date().toISOString();
        try {
            fs.unlinkSync(filePath);
            log.info(`Deleted: ${filePath} (${ageDays}d old, policy: ${policy.retentionDays}d)`);
            return {
                timestamp: now,
                file: filePath,
                category: policy.category,
                action: 'deleted',
                reason: `Deleted after ${ageDays}d (max ${policy.retentionDays}d, ref: ${policy.reference})`,
                fileAgeDays: ageDays,
                retentionDays: policy.retentionDays,
                dryRun: false,
                dataClassTag,
            };
        }
        catch (err) {
            log.error(`Failed to delete ${filePath}: ${err.message}`);
            return {
                timestamp: now,
                file: filePath,
                category: policy.category,
                action: 'skipped',
                reason: `Delete failed: ${err.message}`,
                fileAgeDays: ageDays,
                retentionDays: policy.retentionDays,
                dryRun: false,
                dataClassTag,
            };
        }
    }
    getFileAgeDays(filePath) {
        try {
            const stat = fs.statSync(filePath);
            const ageMs = Date.now() - stat.mtimeMs;
            return Math.floor(ageMs / 86400000);
        }
        catch {
            return 0;
        }
    }
    listFiles(dir, pattern) {
        try {
            return fs.readdirSync(dir)
                .filter(f => {
                if (pattern) {
                    const regex = new RegExp(pattern.replace(/\*/g, '.*'));
                    return regex.test(f);
                }
                return true;
            })
                .map(f => path.join(dir, f))
                .filter(f => fs.statSync(f).isFile());
        }
        catch {
            return [];
        }
    }
}
exports.RetentionManager = RetentionManager;
//# sourceMappingURL=retention-manager.js.map