"use strict";
/**
 * Cross-Memory Socket Bridge (Phase 6)
 *
 * Bidirectional TCP bridge between editors (Cursor, VSCode, etc.) and the
 * Fivo Cell daemon. Editors send events (file_focus, cursor_move, AI prompt,
 * AI response, terminal commands, etc.) and the bridge stores them in the
 * shared event DB. Other tools can read the timeline via the existing
 * `replay` engine.
 *
 * Protocol: newline-delimited JSON.
 *   client → server:  { type: "event", project, event: {...} }
 *   client → server:  { type: "ping" }
 *   server → client:  { type: "ack", id }
 *   server → client:  { type: "pong" }
 *   server → client:  { type: "event", event: {...} }   // broadcast
 *   server → client:  { type: "error", message }
 */
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.getDefaultPort = getDefaultPort;
exports.setDefaultPort = setDefaultPort;
exports.discoverBridgePort = discoverBridgePort;
exports.getEditorStates = getEditorStates;
exports.getEditorState = getEditorState;
exports.clearAllEditorStates = clearAllEditorStates;
exports.startBridge = startBridge;
exports.stopBridge = stopBridge;
exports.isBridgeRunning = isBridgeRunning;
exports.getBridgeInfo = getBridgeInfo;
exports.isPortInUse = isPortInUse;
exports.killStaleBridge = killStaleBridge;
exports.queryBridgeEditorStates = queryBridgeEditorStates;
const net = __importStar(require("net"));
const os = __importStar(require("os"));
const path = __importStar(require("path"));
const fs = __importStar(require("fs"));
const event_capture_1 = require("../../01-context/sessions/event-capture");
const DEFAULT_PORT = 9877;
let _server = null;
let _port = DEFAULT_PORT;
let _host = '127.0.0.1';
let _startedAt = null;
let _clients = new Map();
let _totalEvents = 0;
let _nextClientId = 1;
function defaultPidFile() {
    return path.join(os.homedir(), '.fivo', 'cell', 'bridge.pid');
}
function readPidFile() {
    try {
        const raw = fs.readFileSync(defaultPidFile(), 'utf8');
        return JSON.parse(raw);
    }
    catch {
        return null;
    }
}
function writePidFile(port, pid) {
    try {
        const fp = defaultPidFile();
        fs.mkdirSync(path.dirname(fp), { recursive: true });
        fs.writeFileSync(fp, JSON.stringify({ port, pid }));
    }
    catch { /* best-effort */ }
}
function deletePidFile() {
    try {
        fs.unlinkSync(defaultPidFile());
    }
    catch { /* ignore */ }
}
function getDefaultPort() { return _port; }
function setDefaultPort(p) { _port = p; }
/**
 * Discover the port of a running bridge by reading the pid file.
 * Returns null if no pid file exists, or the recorded port if it does.
 * Callers can use this as a default when --port is not specified.
 */
function discoverBridgePort() {
    const data = readPidFile();
    if (!data)
        return null;
    // Verify the pid is still alive
    try {
        process.kill(data.pid, 0);
        return data.port;
    }
    catch {
        return null;
    }
}
// ─── Editor Presence Store ────────────────────────────────────────────────
// Map<clientId, BridgeEditorState> — most recent state per connected editor
// We dedupe by (editorName + project) on read so reconnecting doesn't accumulate.
const _editorStates = new Map();
// Default TTL: 10 minutes. A state older than this is considered stale and filtered out.
const STATE_TTL_MS = 10 * 60 * 1000;
function getEditorStates(project) {
    const now = Date.now();
    // Filter out stale states first
    const fresh = [];
    for (const s of _editorStates.values()) {
        if (s.timestamp) {
            const age = now - new Date(s.timestamp).getTime();
            if (age > STATE_TTL_MS)
                continue;
        }
        fresh.push(s);
    }
    // Dedupe by (editorName + project) — keep the most recent
    const byKey = new Map();
    for (const s of fresh) {
        const key = `${s.editorName}::${s.project}`;
        const existing = byKey.get(key);
        if (!existing || (s.timestamp && existing.timestamp && s.timestamp > existing.timestamp)) {
            byKey.set(key, s);
        }
    }
    const all = Array.from(byKey.values());
    if (!project)
        return all;
    const lc = project.toLowerCase();
    return all.filter(s => s.project.toLowerCase() === lc);
}
function getEditorState(project) {
    const states = getEditorStates(project);
    if (states.length === 0)
        return null;
    // Return most recent
    states.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
    return states[0];
}
function clearAllEditorStates() {
    _editorStates.clear();
}
function dispatchEvent(project, ev) {
    const t = ev.eventType;
    const sessionId = ev.sessionId;
    const md = ev.metadata;
    try {
        switch (t) {
            case 'file_save':
                return (0, event_capture_1.recordFileSave)({ project, sessionId, filePath: ev.filePath || '' }, md);
            case 'file_create':
                return (0, event_capture_1.recordFileCreate)({ project, sessionId, filePath: ev.filePath || '' }, md);
            case 'file_delete':
                return (0, event_capture_1.recordFileDelete)({ project, sessionId, filePath: ev.filePath || '' }, md);
            case 'file_focus':
                return (0, event_capture_1.recordFileFocus)({ project, sessionId, filePath: ev.filePath || '' }, md);
            case 'file_blur':
                return (0, event_capture_1.recordFileBlur)({ project, sessionId, filePath: ev.filePath || '' }, md);
            case 'cursor_move':
                return (0, event_capture_1.recordCursorMove)({
                    project, sessionId, filePath: ev.filePath || '',
                    line: ev.line ?? 0, column: ev.column ?? 0,
                }, md);
            case 'terminal_command':
                return (0, event_capture_1.recordTerminalCommand)({
                    project, sessionId, command: ev.command || '',
                    cwd: ev.metadata?.cwd || '',
                }, md);
            case 'terminal_command_done':
                return (0, event_capture_1.recordTerminalCommandDone)({
                    project, sessionId, command: ev.command || '',
                    cwd: ev.metadata?.cwd || '',
                }, md);
            case 'test_run':
                return (0, event_capture_1.recordTestRun)({
                    project, sessionId,
                }, md);
            case 'test_pass':
                return (0, event_capture_1.recordTestPass)({
                    project, sessionId, testFile: ev.filePath || '',
                    testName: ev.testName || '',
                }, md);
            case 'test_fail':
                return (0, event_capture_1.recordTestFail)({
                    project, sessionId, testFile: ev.filePath || '',
                    testName: ev.testName || '',
                    errorMessage: ev.errorMessage || '',
                }, md);
            case 'error':
                return (0, event_capture_1.recordError)({
                    project, sessionId,
                    errorMessage: ev.errorMessage || ev.message || '',
                    errorType: 'other',
                    filePath: ev.filePath,
                    stackTrace: ev.errorStack,
                }, md);
            case 'build_start':
                return (0, event_capture_1.recordBuildStart)({
                    project, sessionId,
                    buildType: 'other',
                }, md);
            case 'build_end':
                return (0, event_capture_1.recordBuildEnd)({
                    project, sessionId,
                    buildType: 'other',
                    durationMs: ev.durationMs ?? 0,
                    success: ev.success ?? false,
                }, md);
            case 'git_commit':
                return (0, event_capture_1.recordGitCommit)({
                    project, sessionId,
                    commitHash: ev.commitHash || 'unknown',
                    message: ev.message || '',
                    branch: ev.branch || 'main',
                }, md);
            case 'ai_prompt':
                return (0, event_capture_1.recordAIPrompt)({
                    project, sessionId,
                    toolName: ev.toolName || 'unknown',
                    taskType: ev.taskType,
                }, md);
            case 'ai_response':
                return (0, event_capture_1.recordAIResponse)({
                    project, sessionId,
                    toolName: ev.toolName || 'unknown',
                    durationMs: ev.durationMs,
                    accepted: ev.success,
                    taskType: ev.taskType,
                }, md);
            case 'session_start':
                return (0, event_capture_1.recordSessionStart)({
                    project, sessionId,
                    toolName: ev.toolName || 'unknown',
                }, md);
            case 'session_end':
                return (0, event_capture_1.recordSessionEnd)({
                    project, sessionId,
                    toolName: ev.toolName || 'unknown',
                }, md);
            default:
                return -1;
        }
    }
    catch {
        return -1;
    }
}
function send(socket, msg) {
    try {
        socket.write(JSON.stringify(msg) + '\n');
    }
    catch { /* ignore */ }
}
function broadcast(msg, excludeClientId) {
    for (const [id, c] of _clients.entries()) {
        if (id === excludeClientId)
            continue;
        send(c.socket, msg);
    }
}
function handleClientMessage(client, raw) {
    let parsed;
    try {
        parsed = JSON.parse(raw);
    }
    catch {
        send(client.socket, { type: 'error', message: 'Invalid JSON' });
        return;
    }
    if (!parsed || typeof parsed !== 'object' || !('type' in parsed)) {
        send(client.socket, { type: 'error', message: 'Missing type field' });
        return;
    }
    if (parsed.type === 'ping') {
        const uptime = _startedAt
            ? Math.floor((Date.now() - new Date(_startedAt).getTime()) / 1000)
            : 0;
        send(client.socket, { type: 'pong', uptimeSeconds: uptime });
        return;
    }
    if (parsed.type === 'event') {
        const ev = parsed;
        if (!ev.project) {
            send(client.socket, { type: 'error', message: 'Missing project in event' });
            return;
        }
        if (!ev.event || !ev.event.eventType) {
            send(client.socket, { type: 'error', message: 'Missing event.eventType' });
            return;
        }
        client.project = ev.project;
        const id = dispatchEvent(ev.project, ev.event);
        client.eventsReceived++;
        _totalEvents++;
        send(client.socket, { type: 'ack', id });
        broadcast({
            type: 'event',
            project: ev.project,
            event: { ...ev.event, _id: id, _receivedAt: new Date().toISOString() },
        }, client.id);
        return;
    }
    if (parsed.type === 'editor_state') {
        const msg = parsed;
        if (!msg.state || typeof msg.state !== 'object') {
            send(client.socket, { type: 'error', message: 'Missing state in editor_state' });
            return;
        }
        if (!msg.state.editorName) {
            send(client.socket, { type: 'error', message: 'Missing editorName' });
            return;
        }
        if (!msg.state.filePath) {
            send(client.socket, { type: 'error', message: 'Missing filePath' });
            return;
        }
        if (typeof msg.state.line !== 'number' || typeof msg.state.column !== 'number') {
            send(client.socket, { type: 'error', message: 'line and column must be numbers' });
            return;
        }
        if (!msg.state.project) {
            send(client.socket, { type: 'error', message: 'Missing project' });
            return;
        }
        // Stash the editor name on the client so it shows in `bridge status`
        client.editorName = msg.state.editorName;
        client.project = msg.state.project;
        // Clean expired entries first to keep the map bounded
        const now = Date.now();
        for (const [k, v] of _editorStates.entries()) {
            if (v.timestamp && (now - new Date(v.timestamp).getTime()) > STATE_TTL_MS) {
                _editorStates.delete(k);
            }
        }
        _editorStates.set(client.id, { ...msg.state, timestamp: new Date().toISOString() });
        send(client.socket, { type: 'ack', id: client.id });
        return;
    }
    if (parsed.type === 'editor_state_query') {
        const msg = parsed;
        const states = getEditorStates(msg.project);
        send(client.socket, { type: 'editor_state_response', states });
        return;
    }
    send(client.socket, { type: 'error', message: 'Unknown type: ' + String(parsed.type) });
}
function startBridge(port = DEFAULT_PORT, host = '127.0.0.1') {
    return new Promise((resolve, reject) => {
        if (_server) {
            reject(new Error('Bridge already running on port ' + _port));
            return;
        }
        _port = port;
        _host = host;
        _clients = new Map();
        _totalEvents = 0;
        _nextClientId = 1;
        const server = net.createServer((socket) => {
            const id = _nextClientId++;
            const client = {
                id,
                socket,
                remoteAddress: socket.remoteAddress || 'unknown',
                remotePort: socket.remotePort || 0,
                project: '(unknown)',
                editorName: '(unknown)',
                buffer: '',
                connectedAt: new Date().toISOString(),
                eventsReceived: 0,
            };
            _clients.set(id, client);
            socket.on('data', (chunk) => {
                client.buffer += chunk.toString('utf8');
                let idx;
                while ((idx = client.buffer.indexOf('\n')) >= 0) {
                    const line = client.buffer.slice(0, idx).trim();
                    client.buffer = client.buffer.slice(idx + 1);
                    if (line.length > 0)
                        handleClientMessage(client, line);
                }
            });
            socket.on('error', () => {
                try {
                    socket.destroy();
                }
                catch { /* ignore */ }
            });
            socket.on('end', () => {
                // Client half-closed; force cleanup so count stays accurate
                try {
                    socket.destroy();
                }
                catch { /* ignore */ }
            });
            socket.on('close', () => {
                _clients.delete(id);
                // Keep editor state after disconnect — it's a "presence" not a "connection".
                // State is only cleared when:
                //   1) Another editor_state message overwrites it (same clientId won't return
                //      so we key by clientId, but new connections get a new id, so we'd
                //      accumulate. Instead, key by editorName+project to dedupe.)
                //   2) Explicit `editor_state_clear` message
                //   3) TTL expires (handled in getEditorStates via timestamp check)
                // For now: just remove the client from the active list. State remains.
            });
        });
        server.on('error', (err) => {
            if (err.code === 'EADDRINUSE') {
                reject(new Error(`Port ${_port} already in use`));
            }
            else {
                reject(err);
            }
        });
        server.listen(_port, _host, () => {
            _server = server;
            _startedAt = new Date().toISOString();
            writePidFile(_port, process.pid);
            resolve(getBridgeInfo());
        });
    });
}
function stopBridge() {
    if (!_server)
        return false;
    for (const c of _clients.values()) {
        try {
            c.socket.destroy();
        }
        catch { /* ignore */ }
    }
    _clients.clear();
    _editorStates.clear();
    try {
        _server.close();
    }
    catch { /* ignore */ }
    _server = null;
    _startedAt = null;
    deletePidFile();
    return true;
}
function isBridgeRunning() { return _server !== null; }
function getBridgeInfo() {
    return {
        running: _server !== null,
        port: _port,
        host: _host,
        startedAt: _startedAt,
        clientCount: _clients.size,
        totalEventsReceived: _totalEvents,
        clients: Array.from(_clients.values()).map(c => ({
            id: c.id,
            remoteAddress: c.remoteAddress,
            remotePort: c.remotePort,
            project: c.project,
            connectedAt: c.connectedAt,
            eventsReceived: c.eventsReceived,
        })),
        editorStates: Array.from(_editorStates.values()),
    };
}
function isPortInUse(port, host = '127.0.0.1') {
    return new Promise((resolve) => {
        const tester = net.createServer()
            .once('error', (err) => {
            if (err.code === 'EADDRINUSE')
                resolve(true);
            else
                resolve(false);
        })
            .once('listening', () => {
            tester.close(() => resolve(false));
        })
            .listen(port, host);
    });
}
function killStaleBridge() {
    const info = readPidFile();
    if (!info)
        return false;
    try {
        process.kill(info.pid, 0);
        try {
            process.kill(info.pid, 'SIGTERM');
        }
        catch { /* ignore */ }
        return true;
    }
    catch {
        deletePidFile();
        return false;
    }
}
function queryBridgeEditorStates(project, port = DEFAULT_PORT, host = '127.0.0.1', timeoutMs = 1500) {
    return new Promise((resolve) => {
        const socket = new net.Socket();
        let buffer = '';
        let resolved = false;
        const finish = (result) => {
            if (resolved)
                return;
            resolved = true;
            try {
                socket.destroy();
            }
            catch { /* ignore */ }
            resolve(result);
        };
        const timer = setTimeout(() => finish({ connected: false, states: [], error: 'timeout' }), timeoutMs);
        socket.once('error', (e) => {
            clearTimeout(timer);
            finish({ connected: false, states: [], error: e.message });
        });
        socket.connect(port, host, () => {
            socket.write(JSON.stringify({ type: 'editor_state_query', project }) + '\n');
        });
        socket.on('data', (chunk) => {
            buffer += chunk.toString('utf8');
            const idx = buffer.indexOf('\n');
            if (idx >= 0) {
                const line = buffer.slice(0, idx).trim();
                clearTimeout(timer);
                try {
                    const parsed = JSON.parse(line);
                    if (parsed.type === 'editor_state_response') {
                        finish({ connected: true, states: parsed.states });
                    }
                    else {
                        finish({ connected: true, states: [] });
                    }
                }
                catch (e) {
                    finish({ connected: true, states: [], error: 'parse error: ' + e.message });
                }
            }
        });
    });
}
//# sourceMappingURL=socket-bridge.js.map