/**
 * In-Process Log Buffer
 *
 * Ring buffer that captures structured output from in-process Crewly Agent
 * runtimes. Provides the same interface as PTY terminal capture so the
 * frontend Side Terminal panel can display crewly-agent activity.
 *
 * Extends EventEmitter to support real-time WebSocket streaming of log entries
 * to the frontend Side Terminal panel.
 *
 * Also persists log entries to disk at ~/.crewly/logs/sessions/{sessionName}.log,
 * matching the file-based logging that PTY-based agents get via PtySessionBackend.
 *
 * @module services/agent/crewly-agent/in-process-log-buffer
 */
import { EventEmitter } from 'events';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { CREWLY_CONSTANTS, LOG_ROTATION_CONSTANTS } from '../../../constants.js';
/** Maximum number of entries to keep per session */
const MAX_ENTRIES_PER_SESSION = 500;
/**
 * Singleton buffer that captures in-process agent output.
 *
 * Used by CrewlyAgentRuntimeService to log tool calls, responses,
 * and errors. The terminal controller reads from this buffer when
 * the requested session is an in-process agent (no PTY).
 *
 * Emits 'data' events with (sessionName, formattedLine) when new entries
 * are appended, enabling real-time WebSocket streaming via TerminalGateway.
 *
 * @example
 * ```typescript
 * const buffer = InProcessLogBuffer.getInstance();
 * buffer.append('crewly-assistant', 'info', 'Calling get_team_status tool...');
 * const output = buffer.capture('crewly-assistant', 50);
 *
 * // Real-time streaming
 * buffer.on('data', (sessionName, formattedLine) => {
 *   console.log(`[${sessionName}] ${formattedLine}`);
 * });
 * ```
 */
export class InProcessLogBuffer extends EventEmitter {
    static instance = null;
    sessions = new Map();
    /** File write streams for persisting logs to disk */
    logStreams = new Map();
    /** Resolved path to ~/.crewly/logs/sessions/ */
    sessionLogsDir;
    constructor() {
        super();
        this.sessionLogsDir = path.join(os.homedir(), CREWLY_CONSTANTS.PATHS.CREWLY_HOME, CREWLY_CONSTANTS.PATHS.LOGS_DIR, LOG_ROTATION_CONSTANTS.SESSIONS_LOG_DIR);
        // Ensure directory exists (non-fatal if it fails)
        try {
            fs.mkdirSync(this.sessionLogsDir, { recursive: true });
        }
        catch {
            // Directory may already exist or be uncreatable — logging will be skipped
        }
    }
    /**
     * Get the singleton instance.
     *
     * @returns The shared InProcessLogBuffer instance
     */
    static getInstance() {
        if (!InProcessLogBuffer.instance) {
            InProcessLogBuffer.instance = new InProcessLogBuffer();
        }
        return InProcessLogBuffer.instance;
    }
    /**
     * Reset the singleton (for testing).
     */
    static resetInstance() {
        if (InProcessLogBuffer.instance) {
            // Close all open file streams
            for (const stream of InProcessLogBuffer.instance.logStreams.values()) {
                try {
                    stream.end();
                }
                catch { /* ignore */ }
            }
            InProcessLogBuffer.instance.logStreams.clear();
            InProcessLogBuffer.instance.removeAllListeners();
        }
        InProcessLogBuffer.instance = null;
    }
    /**
     * Format a log entry into a single display line.
     *
     * @param entry - The log entry to format
     * @returns Formatted string like "[HH:MM:SS.mmm] ERROR: message"
     */
    formatEntry(entry) {
        const ts = entry.timestamp.substring(11, 23); // HH:MM:SS.mmm
        const prefix = entry.level === 'error' ? 'ERROR' : entry.level === 'warn' ? 'WARN' : entry.level === 'debug' ? 'DEBUG' : '';
        return prefix ? `[${ts}] ${prefix}: ${entry.message}` : `[${ts}] ${entry.message}`;
    }
    /**
     * Append a log entry for a session.
     *
     * Emits a 'data' event with the session name and formatted line for
     * real-time WebSocket streaming.
     *
     * @param sessionName - In-process agent session name
     * @param level - Log level
     * @param message - Log message text
     */
    append(sessionName, level, message) {
        if (!this.sessions.has(sessionName)) {
            this.sessions.set(sessionName, []);
        }
        const entries = this.sessions.get(sessionName);
        const entry = {
            timestamp: new Date().toISOString(),
            level,
            message,
        };
        entries.push(entry);
        // Ring buffer — drop oldest entries
        if (entries.length > MAX_ENTRIES_PER_SESSION) {
            entries.splice(0, entries.length - MAX_ENTRIES_PER_SESSION);
        }
        // Emit for real-time WebSocket streaming
        const formattedLine = this.formatEntry(entry);
        this.emit('data', sessionName, formattedLine);
        // Persist to disk (fire-and-forget, non-blocking)
        this.writeToFile(sessionName, entry, formattedLine);
    }
    /**
     * Check if a session has any log entries.
     *
     * @param sessionName - Session to check
     * @returns True if the session has been registered with at least one entry
     */
    hasSession(sessionName) {
        return this.sessions.has(sessionName);
    }
    /**
     * Capture recent output from an in-process session, formatted like
     * terminal output for frontend compatibility.
     *
     * @param sessionName - Session to capture from
     * @param lines - Maximum number of lines to return
     * @returns Formatted output string (one log entry per line)
     */
    capture(sessionName, lines = 100) {
        const entries = this.sessions.get(sessionName);
        if (!entries || entries.length === 0) {
            return '[crewly-agent] No output yet';
        }
        const slice = entries.slice(-lines);
        return slice.map(e => this.formatEntry(e)).join('\n');
    }
    /**
     * Register a session (creates empty entry list and opens a file log stream).
     *
     * @param sessionName - Session to register
     */
    registerSession(sessionName) {
        if (!this.sessions.has(sessionName)) {
            this.sessions.set(sessionName, []);
        }
        this.openLogStream(sessionName);
    }
    /**
     * Remove a session's log buffer and close its file stream.
     * The log file is preserved on disk for post-mortem analysis.
     *
     * @param sessionName - Session to remove
     */
    removeSession(sessionName) {
        this.sessions.delete(sessionName);
        this.closeLogStream(sessionName);
    }
    /**
     * Get all registered in-process session names.
     *
     * @returns Array of session names
     */
    getSessionNames() {
        return Array.from(this.sessions.keys());
    }
    /**
     * Clear all sessions and close all file streams (for testing).
     */
    clear() {
        this.sessions.clear();
        for (const stream of this.logStreams.values()) {
            try {
                stream.end();
            }
            catch { /* ignore */ }
        }
        this.logStreams.clear();
    }
    // ===== Private file I/O helpers =====
    /**
     * Get the file path for a session's persistent log.
     *
     * @param sessionName - Session name
     * @returns Absolute path to the log file
     */
    getLogPath(sessionName) {
        return path.join(this.sessionLogsDir, `${sessionName}.log`);
    }
    /**
     * Open a writable file stream for a session log.
     * Appends a session marker so multiple runs are distinguishable.
     *
     * @param sessionName - Session name
     */
    openLogStream(sessionName) {
        // Don't open duplicate streams
        if (this.logStreams.has(sessionName))
            return;
        try {
            const logPath = this.getLogPath(sessionName);
            const fileExists = fs.existsSync(logPath);
            const stream = fs.createWriteStream(logPath, { flags: 'a' });
            stream.on('error', () => {
                // Silently remove broken streams — disk logging is best-effort
                this.logStreams.delete(sessionName);
            });
            const marker = fileExists ? 'RESTARTED' : 'STARTED';
            stream.write(`\n--- SESSION ${marker} at ${new Date().toISOString()} ---\n\n`);
            this.logStreams.set(sessionName, stream);
        }
        catch {
            // Non-fatal — in-memory buffer and WebSocket streaming still work
        }
    }
    /**
     * Close and remove the file stream for a session.
     *
     * @param sessionName - Session name
     */
    closeLogStream(sessionName) {
        const stream = this.logStreams.get(sessionName);
        if (stream) {
            try {
                stream.write(`\n--- SESSION ENDED at ${new Date().toISOString()} ---\n`);
                stream.end();
            }
            catch { /* ignore */ }
            this.logStreams.delete(sessionName);
        }
    }
    /**
     * Write a formatted log entry to the session's file stream.
     * Non-blocking, fire-and-forget — errors are silently ignored.
     *
     * @param sessionName - Session name
     * @param entry - The log entry (for full timestamp in file)
     * @param formattedLine - Pre-formatted display line
     */
    writeToFile(sessionName, entry, formattedLine) {
        const stream = this.logStreams.get(sessionName);
        if (!stream)
            return;
        try {
            stream.write(`${entry.timestamp} ${formattedLine}\n`);
        }
        catch {
            // Non-fatal — disk logging is best-effort
        }
    }
}
//# sourceMappingURL=in-process-log-buffer.js.map