import { EventEmitter } from 'events';
import * as fs from 'fs';
import * as path from 'path';
import { resolveNexusStateDir } from './runtime-registry.js';
// ─────────────────────────────────────────────────────────────────────────────
// EventBus Singleton with Cross-Process JSONL Bridge
// ─────────────────────────────────────────────────────────────────────────────
let _eventsFile;
function getEventsFile() {
    if (!_eventsFile) {
        _eventsFile = path.join(resolveNexusStateDir(), 'events.jsonl');
    }
    return _eventsFile;
}
const MAX_EVENT_FILE_BYTES = 10 * 1024 * 1024;
const MAX_EVENT_ARCHIVES = 3;
const EVENT_ARCHIVE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
const MAX_REHYDRATE_BYTES_PER_FILE = 256 * 1024;
const MAX_REHYDRATE_FILES = 2;
function readRecentJsonlLines(target, maxBytes, maxLines) {
    const stat = fs.statSync(target);
    const start = Math.max(0, stat.size - maxBytes);
    const fd = fs.openSync(target, 'r');
    try {
        const buf = Buffer.alloc(stat.size - start);
        fs.readSync(fd, buf, 0, buf.length, start);
        let chunk = buf.toString('utf8');
        if (start > 0) {
            const firstNewline = chunk.indexOf('\n');
            chunk = firstNewline === -1 ? '' : chunk.slice(firstNewline + 1);
        }
        const lines = chunk
            .split('\n')
            .map((line) => line.trim())
            .filter(Boolean);
        return lines.slice(-maxLines);
    }
    finally {
        fs.closeSync(fd);
    }
}
class EventBusEngine {
    emitter = new EventEmitter();
    history = [];
    MAX_HISTORY = 1000;
    seenIds = new Set();
    fileOffset = 0;
    pollHandle = null;
    constructor() {
        // Increase limit for many dashboard connections
        this.emitter.setMaxListeners(50);
        // Ensure directory exists
        const dir = path.dirname(getEventsFile());
        if (!fs.existsSync(dir)) {
            fs.mkdirSync(dir, { recursive: true });
        }
        this.pruneArchivedEvents();
        this.rehydrateHistoryFromDisk();
    }
    /**
     * Emit a strongly-typed Nexus Prime event
     */
    emit(type, data) {
        const event = {
            id: `evt_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`,
            type,
            timestamp: Date.now(),
            data
        };
        this.recordEvent(event);
        // Broadcast in-process
        this.emitter.emit('nexus_event', event);
        // Write to JSONL file for cross-process bridge
        try {
            this.rotateEventsFileIfNeeded();
            fs.appendFileSync(getEventsFile(), JSON.stringify(event) + '\n');
        }
        catch { /* ignore write errors */ }
    }
    /**
     * Poll the JSONL file for events from other processes.
     * Call this from DashboardServer to bridge cross-process events.
     */
    startFilePolling(intervalMs = 30_000) {
        if (this.pollHandle)
            return;
        // Skip to end of file initially so we don't replay old events
        try {
            if (fs.existsSync(getEventsFile())) {
                this.fileOffset = fs.statSync(getEventsFile()).size;
            }
        }
        catch { /* ignore */ }
        this.pollHandle = setInterval(() => {
            try {
                if (!fs.existsSync(getEventsFile()))
                    return;
                const stat = fs.statSync(getEventsFile());
                if (stat.size <= this.fileOffset) {
                    // File was truncated or no new data (poll at 30s to reduce CPU)
                    if (stat.size < this.fileOffset)
                        this.fileOffset = 0;
                    return;
                }
                // Read only new bytes
                const fd = fs.openSync(getEventsFile(), 'r');
                const buf = Buffer.alloc(stat.size - this.fileOffset);
                fs.readSync(fd, buf, 0, buf.length, this.fileOffset);
                fs.closeSync(fd);
                // Ensure we only process complete lines (ending in \n)
                const chunk = buf.toString('utf-8');
                const lastNewline = chunk.lastIndexOf('\n');
                if (lastNewline === -1) {
                    // No complete line yet, wait for next tick
                    return;
                }
                const validChunk = chunk.substring(0, lastNewline);
                this.fileOffset += Buffer.byteLength(validChunk) + 1; // +1 for the newline
                // Parse JSONL lines
                const lines = validChunk.split('\n').filter(Boolean);
                for (const line of lines) {
                    try {
                        const event = JSON.parse(line);
                        if (!this.seenIds.has(event.id)) {
                            this.recordEvent(event);
                            // Broadcast to in-process listeners (SSE clients)
                            this.emitter.emit('nexus_event', event);
                        }
                    }
                    catch { /* skip malformed lines */ }
                }
            }
            catch { /* ignore poll errors */ }
        }, intervalMs);
        this.pollHandle.unref();
    }
    isFilePollingActive() {
        return this.pollHandle !== null;
    }
    /** Stop file polling */
    stopFilePolling() {
        if (this.pollHandle) {
            clearInterval(this.pollHandle);
            this.pollHandle = null;
        }
    }
    /**
     * Listen to all events (useful for SSE streams)
     */
    onEvent(handler) {
        this.emitter.on('nexus_event', handler);
        // Return an unsubscribe function
        return () => this.emitter.off('nexus_event', handler);
    }
    /**
     * Listen to specific event types
     */
    on(type, handler) {
        const wrapper = (event) => {
            if (event.type === type) {
                handler(event.data);
            }
        };
        this.emitter.on('nexus_event', wrapper);
        return () => this.emitter.off('nexus_event', wrapper);
    }
    /**
     * Get historical events
     */
    getHistory(sinceTimestamp = 0) {
        return this.history.filter(e => e.timestamp > sinceTimestamp);
    }
    /**
     * Clear history
     */
    clear() {
        this.history = [];
        this.seenIds.clear();
    }
    recordEvent(event) {
        this.history.push(event);
        this.seenIds.add(event.id);
        this.trimHistory();
    }
    trimHistory() {
        if (this.history.length <= this.MAX_HISTORY) {
            return;
        }
        this.history = this.history.slice(-this.MAX_HISTORY);
        this.seenIds = new Set(this.history.map((event) => event.id));
    }
    rehydrateHistoryFromDisk() {
        const files = [
            getEventsFile(),
            ...Array.from({ length: MAX_EVENT_ARCHIVES }, (_, index) => `${getEventsFile()}.${index + 1}`),
        ].slice(0, MAX_REHYDRATE_FILES);
        let remaining = this.MAX_HISTORY;
        for (const target of files) {
            if (remaining <= 0)
                break;
            if (!fs.existsSync(target))
                continue;
            try {
                const lines = readRecentJsonlLines(target, MAX_REHYDRATE_BYTES_PER_FILE, remaining);
                for (const line of lines) {
                    try {
                        const event = JSON.parse(line);
                        if (!event?.id || this.seenIds.has(event.id))
                            continue;
                        this.recordEvent(event);
                        remaining -= 1;
                        if (remaining <= 0) {
                            break;
                        }
                    }
                    catch {
                        // Skip malformed archived events.
                    }
                }
            }
            catch {
                // Ignore unreadable archives and keep runtime eventing alive.
            }
        }
    }
    rotateEventsFileIfNeeded() {
        try {
            this.pruneArchivedEvents();
            if (!fs.existsSync(getEventsFile()))
                return;
            const size = fs.statSync(getEventsFile()).size;
            if (size < MAX_EVENT_FILE_BYTES)
                return;
            const oldestArchive = `${getEventsFile()}.${MAX_EVENT_ARCHIVES}`;
            if (fs.existsSync(oldestArchive)) {
                fs.unlinkSync(oldestArchive);
            }
            for (let index = MAX_EVENT_ARCHIVES - 1; index >= 1; index -= 1) {
                const current = `${getEventsFile()}.${index}`;
                const next = `${getEventsFile()}.${index + 1}`;
                if (fs.existsSync(current)) {
                    fs.renameSync(current, next);
                }
            }
            fs.renameSync(getEventsFile(), `${getEventsFile()}.1`);
            this.pruneArchivedEvents();
        }
        catch {
            // Ignore rotation failures and continue writing to the active event file.
        }
    }
    pruneArchivedEvents() {
        const cutoff = Date.now() - EVENT_ARCHIVE_TTL_MS;
        for (let index = 1; index <= MAX_EVENT_ARCHIVES; index += 1) {
            const archive = `${getEventsFile()}.${index}`;
            if (!fs.existsSync(archive))
                continue;
            try {
                const stats = fs.statSync(archive);
                if (stats.mtimeMs < cutoff) {
                    fs.unlinkSync(archive);
                }
            }
            catch {
                // Ignore stale archive cleanup failures.
            }
        }
    }
}
// Export a singleton instance
export const nexusEventBus = new EventBusEngine();
