/**
 * AuditIntegrity -- Tamper-evident audit logs with hash chaining.
 *
 * Each log entry includes a SHA-256 hash of itself + the previous entry's hash,
 * creating an immutable chain. Any modification or deletion is detectable.
 *
 * Required by:
 *   - SOC 2 CC7.2: Monitoring of system components
 *   - HIPAA 164.312(b): Audit controls
 *   - PCI DSS Req 10: Track and monitor access
 *   - ISO 27001 8.15: Logging
 *
 * Format per line (JSONL):
 *   { ...entry, _seq: N, _prevHash: "abc...", _hash: "def..." }
 *
 * @connexum/ai-governance
 */
import * as crypto from 'crypto';
import * as fs from 'fs';
import * as path from 'path';
export class AuditIntegrity {
    config;
    currentFile;
    currentSeq = 0;
    lastHash = '';
    constructor(config) {
        this.config = {
            prefix: 'audit-chain',
            maxEntriesPerFile: 10000,
            ...config,
        };
        if (!fs.existsSync(config.logDir)) {
            fs.mkdirSync(config.logDir, { recursive: true });
        }
        this.currentFile = this.getCurrentLogFile();
        this.initializeFromExisting();
    }
    /**
     * Append a tamper-evident entry to the audit log.
     */
    append(data) {
        // Add timestamp if not present
        if (!data.timestamp) {
            data.timestamp = new Date().toISOString();
        }
        this.currentSeq++;
        // Check if rotation needed
        if (this.currentSeq > this.config.maxEntriesPerFile) {
            this.rotate();
        }
        const entry = {
            data,
            _seq: this.currentSeq,
            _prevHash: this.lastHash,
            _hash: '', // Computed below
        };
        // Compute hash of the entry (excluding _hash itself)
        entry._hash = this.computeEntryHash(entry);
        this.lastHash = entry._hash;
        // Write to file
        try {
            fs.appendFileSync(this.currentFile, JSON.stringify(entry) + '\n');
        }
        catch (err) {
            process.stderr.write(`[AUDIT INTEGRITY CRITICAL] Failed to write audit entry: ${err.message}\n`);
            throw new Error(`Audit write failure: ${err.message}`);
        }
        return entry;
    }
    /**
     * Verify the integrity of all audit log files.
     * Checks that every entry's hash matches its content + previous hash.
     */
    verify() {
        const files = this.listLogFiles();
        const result = {
            valid: true,
            totalEntries: 0,
            brokenAt: -1,
            files,
            errors: [],
        };
        let prevHash = '';
        let globalSeq = 0;
        for (const file of files) {
            const lines = this.readLines(file);
            for (let i = 0; i < lines.length; i++) {
                globalSeq++;
                result.totalEntries++;
                try {
                    const entry = JSON.parse(lines[i]);
                    // Verify previous hash chain
                    if (entry._prevHash !== prevHash) {
                        result.valid = false;
                        if (result.brokenAt === -1)
                            result.brokenAt = globalSeq;
                        result.errors.push(`Entry ${globalSeq} (file: ${path.basename(file)}, seq: ${entry._seq}): ` +
                            `prevHash mismatch. Expected ${prevHash.slice(0, 16)}, got ${entry._prevHash.slice(0, 16)}`);
                    }
                    // Verify self-hash
                    const expectedHash = this.computeEntryHash(entry);
                    if (entry._hash !== expectedHash) {
                        result.valid = false;
                        if (result.brokenAt === -1)
                            result.brokenAt = globalSeq;
                        result.errors.push(`Entry ${globalSeq} (file: ${path.basename(file)}, seq: ${entry._seq}): ` +
                            `hash mismatch. Data may have been tampered.`);
                    }
                    prevHash = entry._hash;
                }
                catch (err) {
                    result.valid = false;
                    if (result.brokenAt === -1)
                        result.brokenAt = globalSeq;
                    result.errors.push(`Entry ${globalSeq} (file: ${path.basename(file)}): ` +
                        `Parse error: ${err.message}`);
                }
            }
        }
        return result;
    }
    /**
     * Read all entries from all log files.
     */
    readAll() {
        const files = this.listLogFiles();
        const entries = [];
        for (const file of files) {
            const lines = this.readLines(file);
            for (const line of lines) {
                try {
                    entries.push(JSON.parse(line));
                }
                catch {
                    // Skip malformed
                }
            }
        }
        return entries;
    }
    /**
     * Read entries from the most recent N entries.
     */
    readRecent(limit = 50) {
        const all = this.readAll();
        return all.slice(-limit);
    }
    /**
     * Get the current log file path.
     */
    getLogFile() {
        return this.currentFile;
    }
    /**
     * Get statistics about the audit chain.
     */
    getStats() {
        const files = this.listLogFiles();
        let totalEntries = 0;
        for (const file of files) {
            totalEntries += this.readLines(file).length;
        }
        return {
            files: files.length,
            totalEntries,
            currentSeq: this.currentSeq,
            lastHash: this.lastHash,
        };
    }
    // --- Private helpers ---
    computeEntryHash(entry) {
        // Hash everything except _hash itself
        const hashInput = JSON.stringify({
            data: entry.data,
            _seq: entry._seq,
            _prevHash: entry._prevHash,
        });
        return crypto.createHash('sha256').update(hashInput).digest('hex');
    }
    getCurrentLogFile() {
        const date = new Date().toISOString().split('T')[0];
        return path.join(this.config.logDir, `${this.config.prefix}-${date}.jsonl`);
    }
    rotate() {
        // Start a new file with a timestamp suffix
        const timestamp = Date.now();
        this.currentFile = path.join(this.config.logDir, `${this.config.prefix}-${new Date().toISOString().split('T')[0]}-${timestamp}.jsonl`);
        this.currentSeq = 0;
        // lastHash carries forward to maintain chain across files
    }
    initializeFromExisting() {
        const files = this.listLogFiles();
        if (files.length === 0)
            return;
        // Read the last file's last entry to get chain state
        const lastFile = files[files.length - 1];
        const lines = this.readLines(lastFile);
        if (lines.length === 0)
            return;
        try {
            const lastEntry = JSON.parse(lines[lines.length - 1]);
            // Guard: legacy flat entries (pre-fix) parse as valid JSON but lack
            // _hash/_seq. Treat missing or non-string/non-number fields as a fresh
            // chain start rather than inheriting undefined state.
            if (typeof lastEntry._hash !== 'string' || typeof lastEntry._seq !== 'number') {
                this.lastHash = '';
                this.currentSeq = 0;
                return;
            }
            this.lastHash = lastEntry._hash;
            this.currentSeq = lastEntry._seq;
            // If last file is today's file, continue in it
            if (lastFile === this.currentFile) {
                // Already set
            }
            else {
                // Today's file is new, seq resets but hash continues
                this.currentSeq = 0;
            }
        }
        catch {
            // Corrupted last entry -- start fresh chain
            this.lastHash = '';
            this.currentSeq = 0;
        }
    }
    listLogFiles() {
        if (!fs.existsSync(this.config.logDir))
            return [];
        return fs.readdirSync(this.config.logDir)
            .filter(f => f.startsWith(this.config.prefix) && f.endsWith('.jsonl'))
            .sort()
            .map(f => path.join(this.config.logDir, f));
    }
    readLines(file) {
        try {
            return fs.readFileSync(file, 'utf-8')
                .trim()
                .split('\n')
                .filter(Boolean);
        }
        catch {
            return [];
        }
    }
}
//# sourceMappingURL=audit-integrity.js.map