/**
 * DataSubjectRights -- GDPR Articles 15-22 compliance.
 *
 * Implements data subject rights required by GDPR:
 *   - Art. 15: Right of access (SAR export)
 *   - Art. 16: Right to rectification
 *   - Art. 17: Right to erasure ("right to be forgotten")
 *   - Art. 20: Right to data portability (JSON/CSV export)
 *   - Art. 21: Right to object to processing
 *
 * All requests are logged with an immutable audit trail.
 *
 * @connexum/ai-governance
 */
import * as fs from 'fs';
import * as path from 'path';
import * as crypto from 'crypto';
import { createLogger } from '../util/log.js';
const log = createLogger('dsr');
export class DataSubjectRights {
    config;
    requestsFile;
    auditFile;
    /**
     * P3.5: Optional callback / source function for canonical audit events.
     * When set, findSubjectData() also queries the pack-driven audit chain
     * for events where dataSubjectId matches the pseudonymized subject ID.
     * This enables a single "show me everything about subject X" query that
     * spans both DSR records and the five-dimensional disclosure audit trail.
     *
     * Usage:
     *   dsr.setAuditEventSource(() => classifier.peekAuditEvents());
     */
    auditEventSource = null;
    constructor(config) {
        this.config = {
            responseDeadlineMs: 10 * 60 * 1000, // 10 min (DEV MODE -- production: 30 * 24 * 60 * 60 * 1000 / 30 days)
            scanDirs: [],
            filePatterns: ['.jsonl', '.json', '.log', '.csv'],
            organizationName: 'Organization',
            ...config,
        };
        if (!fs.existsSync(config.dataDir)) {
            fs.mkdirSync(config.dataDir, { recursive: true });
        }
        this.requestsFile = path.join(config.dataDir, 'dsr-requests.json');
        this.auditFile = path.join(config.dataDir, 'dsr-audit.jsonl');
    }
    /**
     * P3.5: Register a source function for canonical pack-driven audit events.
     * When set, findSubjectData() also queries these events for the subject's
     * data, providing a unified audit trail across DSR records and disclosure
     * accounting events.
     *
     * @param source Function returning the current audit event array.
     *               Typically: () => classifier.peekAuditEvents()
     *               or a persistence adapter query.
     */
    setAuditEventSource(source) {
        this.auditEventSource = source;
    }
    /**
     * Submit a new data subject rights request.
     */
    submit(type, subjectId, details, subjectEmail) {
        const now = new Date();
        const request = {
            id: `dsr_${crypto.randomBytes(8).toString('hex')}`,
            type,
            subjectId,
            subjectEmail,
            details,
            status: 'RECEIVED',
            createdAt: now.toISOString(),
            deadline: new Date(now.getTime() + this.config.responseDeadlineMs).toISOString(),
        };
        const requests = this.loadRequests();
        requests.push(request);
        this.saveRequests(requests);
        this.audit('REQUEST_SUBMITTED', request.id, { type, subjectId });
        log.info('DSR submitted', { id: request.id, type, subjectId });
        return request;
    }
    /**
     * Process a Subject Access Request (Art. 15) -- find all data for subject.
     */
    processAccessRequest(requestId) {
        const request = this.getRequest(requestId);
        if (!request)
            throw new Error(`Request ${requestId} not found`);
        if (request.type !== 'ACCESS' && request.type !== 'PORTABILITY') {
            throw new Error(`Request ${requestId} is ${request.type}, not ACCESS/PORTABILITY`);
        }
        this.updateStatus(requestId, 'PROCESSING');
        const inventory = this.findSubjectData(request.subjectId);
        const actions = [];
        // Export data
        const exportDir = path.join(this.config.dataDir, 'exports');
        if (!fs.existsSync(exportDir))
            fs.mkdirSync(exportDir, { recursive: true });
        const format = request.type === 'PORTABILITY' ? 'json' : 'json';
        const exportPath = path.join(exportDir, `${requestId}.${format}`);
        const exportData = {
            requestId,
            subjectId: request.subjectId,
            generatedAt: new Date().toISOString(),
            organization: this.config.organizationName,
            dataInventory: inventory,
        };
        fs.writeFileSync(exportPath, JSON.stringify(exportData, null, 2));
        actions.push({
            action: 'EXPORT',
            file: exportPath,
            details: `Exported ${inventory.length} data locations to ${format.toUpperCase()}`,
            timestamp: new Date().toISOString(),
        });
        const result = {
            filesFound: inventory.map(e => e.file),
            recordCount: inventory.reduce((sum, e) => sum + e.lineNumbers.length, 0),
            actions,
            exportPath,
        };
        this.completeRequest(requestId, result);
        return result;
    }
    /**
     * Process an erasure request (Art. 17) -- redact subject data from files.
     */
    processErasureRequest(requestId) {
        const request = this.getRequest(requestId);
        if (!request)
            throw new Error(`Request ${requestId} not found`);
        if (request.type !== 'ERASURE') {
            throw new Error(`Request ${requestId} is ${request.type}, not ERASURE`);
        }
        this.updateStatus(requestId, 'PROCESSING');
        const inventory = this.findSubjectData(request.subjectId);
        const actions = [];
        for (const entry of inventory) {
            try {
                if (!fs.existsSync(entry.file))
                    continue;
                const content = fs.readFileSync(entry.file, 'utf-8');
                const lines = content.split('\n');
                let modified = false;
                // Process in reverse to preserve line numbers
                for (const lineNum of [...entry.lineNumbers].reverse()) {
                    const idx = lineNum - 1;
                    if (idx >= 0 && idx < lines.length) {
                        // Try to parse as JSON and redact subject fields
                        try {
                            const obj = JSON.parse(lines[idx]);
                            const redacted = this.redactSubjectFields(obj, request.subjectId);
                            lines[idx] = JSON.stringify(redacted);
                            modified = true;
                        }
                        catch {
                            // Not JSON -- redact the subject ID directly
                            lines[idx] = lines[idx].replace(new RegExp(this.escapeRegex(request.subjectId), 'g'), '[ERASED:DSR]');
                            modified = true;
                        }
                    }
                }
                if (modified) {
                    fs.writeFileSync(entry.file, lines.join('\n'));
                    actions.push({
                        action: 'REDACT',
                        file: entry.file,
                        details: `Redacted ${entry.lineNumbers.length} lines`,
                        timestamp: new Date().toISOString(),
                    });
                }
            }
            catch (err) {
                actions.push({
                    action: 'ERROR',
                    file: entry.file,
                    details: `Failed to redact: ${err.message}`,
                    timestamp: new Date().toISOString(),
                });
            }
        }
        const result = {
            filesFound: inventory.map(e => e.file),
            recordCount: actions.filter(a => a.action === 'REDACT').length,
            actions,
        };
        this.completeRequest(requestId, result);
        return result;
    }
    /**
     * Deny a request with reason.
     */
    deny(requestId, reason) {
        const request = this.getRequest(requestId);
        if (!request)
            throw new Error(`Request ${requestId} not found`);
        this.updateStatus(requestId, 'DENIED');
        this.audit('REQUEST_DENIED', requestId, { reason });
    }
    /**
     * Get a specific request.
     */
    getRequest(requestId) {
        return this.loadRequests().find(r => r.id === requestId);
    }
    /**
     * Get all pending (non-completed) requests.
     */
    getPending() {
        return this.loadRequests().filter(r => r.status === 'RECEIVED' || r.status === 'PROCESSING');
    }
    /**
     * Get requests approaching deadline (within 7 days).
     */
    getApproachingDeadline() {
        const sevenDays = 7 * 24 * 60 * 60 * 1000;
        const now = Date.now();
        return this.loadRequests().filter(r => {
            if (r.status === 'COMPLETED' || r.status === 'DENIED')
                return false;
            const deadlineMs = new Date(r.deadline).getTime();
            return (deadlineMs - now) <= sevenDays && (deadlineMs - now) > 0;
        });
    }
    /**
     * Get breached deadline requests.
     */
    getBreachedDeadlines() {
        const now = Date.now();
        return this.loadRequests().filter(r => {
            if (r.status === 'COMPLETED' || r.status === 'DENIED')
                return false;
            return new Date(r.deadline).getTime() < now;
        });
    }
    /**
     * Get DSR statistics.
     */
    getStats() {
        const requests = this.loadRequests();
        const byType = {
            ACCESS: 0, RECTIFICATION: 0, ERASURE: 0, PORTABILITY: 0, OBJECTION: 0,
        };
        const byStatus = {
            RECEIVED: 0, PROCESSING: 0, COMPLETED: 0, DENIED: 0, EXPIRED: 0,
        };
        let completionTotal = 0;
        let completionCount = 0;
        for (const r of requests) {
            byType[r.type]++;
            byStatus[r.status]++;
            if (r.status === 'COMPLETED' && r.completedAt) {
                const duration = new Date(r.completedAt).getTime() - new Date(r.createdAt).getTime();
                completionTotal += duration;
                completionCount++;
            }
        }
        return {
            total: requests.length,
            byType,
            byStatus,
            breachedDeadlines: this.getBreachedDeadlines().length,
            avgCompletionMs: completionCount > 0 ? Math.round(completionTotal / completionCount) : 0,
        };
    }
    // --- Private ---
    /**
     * P3.5: DSR + canonical audit chain join.
     *
     * Scans two sources for the data subject's records:
     *   1. Configured scanDirs (file-based DSR records, existing behavior).
     *   2. Canonical pack-driven audit events (P3.5 disclosure accounting).
     *      Matches on event.dataSubjectId === subjectId (pseudonymized hash).
     *
     * The combined result satisfies "show me everything pertaining to subject X"
     * across packs and DSR channels, as required by HIPAA §164.528 and FERPA §99.32.
     */
    findSubjectData(subjectId) {
        const inventory = [];
        const escaped = this.escapeRegex(subjectId);
        const pattern = new RegExp(escaped, 'gi');
        // Source 1: file-based DSR records (existing behavior)
        for (const dir of this.config.scanDirs) {
            if (!fs.existsSync(dir))
                continue;
            try {
                const files = this.getFilesRecursive(dir);
                for (const file of files) {
                    // Check file extension
                    const ext = path.extname(file);
                    if (!this.config.filePatterns.some(p => ext === p || file.endsWith(p)))
                        continue;
                    try {
                        const content = fs.readFileSync(file, 'utf-8');
                        const lines = content.split('\n');
                        const matchingLines = [];
                        const dataTypes = new Set();
                        for (let i = 0; i < lines.length; i++) {
                            if (pattern.test(lines[i])) {
                                matchingLines.push(i + 1);
                                pattern.lastIndex = 0;
                                // Detect data types in this line
                                if (/email|@/.test(lines[i]))
                                    dataTypes.add('email');
                                if (/name|first.*name|last.*name/i.test(lines[i]))
                                    dataTypes.add('name');
                                if (/agent|source/i.test(lines[i]))
                                    dataTypes.add('agent_activity');
                                if (/ip|address/i.test(lines[i]))
                                    dataTypes.add('ip_address');
                                if (/timestamp|created|date/i.test(lines[i]))
                                    dataTypes.add('timestamp');
                            }
                        }
                        if (matchingLines.length > 0) {
                            inventory.push({
                                file,
                                subjectId,
                                dataTypes: Array.from(dataTypes),
                                lineNumbers: matchingLines,
                            });
                        }
                    }
                    catch {
                        // Skip unreadable files
                    }
                }
            }
            catch {
                // Skip inaccessible directories
            }
        }
        // Source 2: P3.5 canonical audit event chain (five-dimensional identity)
        if (this.auditEventSource) {
            try {
                const auditEvents = this.auditEventSource();
                const matchingEvents = auditEvents.filter(ev => ev.dataSubjectId === subjectId);
                if (matchingEvents.length > 0) {
                    // Group by packId+categoryId for a single synthetic inventory entry
                    const dataTypes = new Set();
                    for (const ev of matchingEvents) {
                        dataTypes.add(`pack:${ev.packId}::${ev.categoryId}`);
                        if (ev.recipientId)
                            dataTypes.add(`recipient:${ev.recipientId}`);
                        if (ev.transportMechanism)
                            dataTypes.add(`transport:${ev.transportMechanism}`);
                    }
                    inventory.push({
                        file: '[canonical-audit-chain]',
                        subjectId,
                        dataTypes: Array.from(dataTypes),
                        lineNumbers: matchingEvents.map((_, idx) => idx + 1),
                    });
                }
            }
            catch {
                // Audit event source errors must not block DSR processing
                log.warn('Failed to query audit event source for DSR join', { subjectId });
            }
        }
        return inventory;
    }
    getFilesRecursive(dir, maxDepth = 5, depth = 0) {
        if (depth >= maxDepth)
            return [];
        const files = [];
        try {
            const entries = fs.readdirSync(dir, { withFileTypes: true });
            for (const entry of entries) {
                const fullPath = path.join(dir, entry.name);
                if (entry.isDirectory() && !entry.name.startsWith('.')) {
                    files.push(...this.getFilesRecursive(fullPath, maxDepth, depth + 1));
                }
                else if (entry.isFile()) {
                    files.push(fullPath);
                }
            }
        }
        catch {
            // Skip inaccessible
        }
        return files;
    }
    redactSubjectFields(obj, subjectId) {
        const result = {};
        for (const [key, value] of Object.entries(obj)) {
            if (typeof value === 'string' && value.includes(subjectId)) {
                result[key] = '[ERASED:DSR]';
            }
            else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
                result[key] = this.redactSubjectFields(value, subjectId);
            }
            else {
                result[key] = value;
            }
        }
        return result;
    }
    escapeRegex(str) {
        return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    }
    loadRequests() {
        if (!fs.existsSync(this.requestsFile))
            return [];
        try {
            return JSON.parse(fs.readFileSync(this.requestsFile, 'utf-8'));
        }
        catch {
            return [];
        }
    }
    saveRequests(requests) {
        fs.writeFileSync(this.requestsFile, JSON.stringify(requests, null, 2));
    }
    updateStatus(requestId, status) {
        const requests = this.loadRequests();
        const request = requests.find(r => r.id === requestId);
        if (request) {
            request.status = status;
            this.saveRequests(requests);
            this.audit('STATUS_UPDATED', requestId, { status });
        }
    }
    completeRequest(requestId, result) {
        const requests = this.loadRequests();
        const request = requests.find(r => r.id === requestId);
        if (request) {
            request.status = 'COMPLETED';
            request.completedAt = new Date().toISOString();
            request.result = result;
            this.saveRequests(requests);
            this.audit('REQUEST_COMPLETED', requestId, {
                filesFound: result.filesFound.length,
                recordCount: result.recordCount,
                actions: result.actions.length,
            });
        }
    }
    audit(action, requestId, data) {
        const entry = {
            timestamp: new Date().toISOString(),
            action,
            requestId,
            ...data,
        };
        try {
            fs.appendFileSync(this.auditFile, JSON.stringify(entry) + '\n');
        }
        catch {
            log.error('Failed to write DSR audit', { requestId, action });
        }
    }
}
//# sourceMappingURL=data-subject-rights.js.map