/**
 * Vigthoria CLI → DevTools Bridge Telemetry Client
 *
 * Connects the local CLI to the remote bridge server in "commando" mode,
 * streaming real-time activity (commands, tool calls, model responses,
 * file edits, errors) and receiving admin-issued commands.
 *
 * Design principles:
 *  - Fire-and-forget: never blocks the CLI main flow
 *  - Auto-reconnects with exponential back-off
 *  - Opt-in via --bridge <url> flag
 *  - Sensitive data (API keys, tokens) is never transmitted
 */
import WebSocket from 'ws';
import * as os from 'os';
import chalk from 'chalk';
import { CH } from './logger.js';
// ── Singleton accessor ───────────────────────────────────────────────
let _instance = null;
/** Get the active bridge client (may be null if --bridge was not used). */
export function getBridgeClient() {
    return _instance;
}
// ── BridgeClient ─────────────────────────────────────────────────────
export class BridgeClient {
    ws = null;
    url;
    apiKey;
    machineLabel;
    clientId;
    connected = false;
    reconnectTimer = null;
    heartbeatTimer = null;
    queue = []; // buffered events while disconnected
    maxQueueSize = 500;
    reconnectDelay = 2000; // ms, doubles on failure up to 30 s
    destroyed = false;
    onAdminCommand;
    constructor(opts) {
        this.url = opts.bridgeUrl.replace(/\/$/, '');
        if (!this.url.includes('/ws')) {
            this.url = this.url.replace(/^http/, 'ws') + '/ws';
        }
        this.apiKey = opts.apiKey;
        this.machineLabel = opts.machineLabel || os.hostname();
        this.clientId = `cli-${this.machineLabel}-${Date.now().toString(36)}`;
        this.onAdminCommand = opts.onAdminCommand;
        _instance = this;
    }
    // ── Lifecycle ────────────────────────────────────────────────────
    async connect() {
        if (this.destroyed)
            return;
        return new Promise((resolve) => {
            try {
                this.ws = new WebSocket(this.url, { handshakeTimeout: 8000 });
                this.ws.on('open', () => {
                    this.connected = true;
                    this.reconnectDelay = 2000;
                    // Authenticate as CLI commando client
                    this.sendRaw({
                        type: 'auth',
                        payload: {
                            source: 'cli-commando',
                            clientId: this.clientId,
                            machineLabel: this.machineLabel,
                            hostname: os.hostname(),
                            platform: os.platform(),
                            arch: os.arch(),
                            nodeVersion: process.version,
                            auth: this.apiKey ? { apiKey: this.apiKey } : undefined,
                        },
                    });
                    // Flush buffered events
                    this.flushQueue();
                    this.startHeartbeat();
                    resolve();
                });
                this.ws.on('message', (raw) => {
                    try {
                        const msg = JSON.parse(raw.toString());
                        if (msg.type === 'admin:command' && this.onAdminCommand) {
                            this.onAdminCommand(msg);
                        }
                    }
                    catch {
                        // ignore unparseable messages
                    }
                });
                this.ws.on('close', () => {
                    this.connected = false;
                    this.stopHeartbeat();
                    this.scheduleReconnect();
                });
                this.ws.on('error', () => {
                    this.connected = false;
                    this.stopHeartbeat();
                    this.scheduleReconnect();
                    if (process.env.DEBUG || process.env.VIGTHORIA_DEBUG) {
                        console.log(chalk.yellow(`${CH.warn} Bridge: connection failed, will retry in background.`));
                    }
                    resolve(); // resolve even on failure – must never block CLI
                });
            }
            catch {
                resolve(); // swallow – bridge is optional
            }
        });
    }
    destroy() {
        this.destroyed = true;
        this.stopHeartbeat();
        if (this.reconnectTimer)
            clearTimeout(this.reconnectTimer);
        if (this.ws) {
            try {
                this.ws.close();
            }
            catch { /* ignore */ }
        }
        this.ws = null;
        this.connected = false;
        if (_instance === this)
            _instance = null;
    }
    get isConnected() {
        return this.connected;
    }
    // ── Telemetry emitters (public API used by CLI code) ─────────────
    /** CLI session started (command, flags, cwd). */
    emitStart(data) {
        this.emit('cli:start', data);
    }
    /** User entered a prompt / message. */
    emitPrompt(data) {
        this.emit('cli:prompt', data);
    }
    /** Model response received (summary only, not full content). */
    emitModelResponse(data) {
        this.emit('cli:model-response', data);
    }
    /** Tool is being called. */
    emitToolCall(data) {
        // Redact sensitive arg values
        const safeArgs = {};
        for (const [k, v] of Object.entries(data.args)) {
            if (/key|token|password|secret|auth/i.test(k)) {
                safeArgs[k] = '***';
            }
            else if (typeof v === 'string' && v.length > 2000) {
                safeArgs[k] = v.slice(0, 200) + `...[${v.length} chars]`;
            }
            else {
                safeArgs[k] = v;
            }
        }
        this.emit('cli:tool-call', { tool: data.tool, args: safeArgs });
    }
    /** Tool finished executing. */
    emitToolResult(data) {
        this.emit('cli:tool-result', data);
    }
    /** File was written or edited. */
    emitFileEdit(data) {
        this.emit('cli:file-edit', data);
    }
    /** Error occurred. */
    emitError(data) {
        this.emit('cli:error', data);
    }
    /** Mode changed (agent / operator / chat). */
    emitModeChange(data) {
        this.emit('cli:mode-change', data);
    }
    /** Session ended. */
    emitEnd(data) {
        this.emit('cli:end', data);
    }
    // ── Internals ────────────────────────────────────────────────────
    emit(type, payload) {
        const event = {
            type,
            payload,
            ts: Date.now(),
            clientId: this.clientId,
        };
        const json = JSON.stringify(event);
        if (this.connected && this.ws?.readyState === WebSocket.OPEN) {
            try {
                this.ws.send(json);
            }
            catch {
                this.bufferEvent(json);
            }
        }
        else {
            this.bufferEvent(json);
        }
    }
    sendRaw(obj) {
        if (this.ws?.readyState === WebSocket.OPEN) {
            this.ws.send(JSON.stringify(obj));
        }
    }
    bufferEvent(json) {
        if (this.queue.length >= this.maxQueueSize) {
            this.queue.shift(); // drop oldest
        }
        this.queue.push(json);
    }
    flushQueue() {
        while (this.queue.length > 0 && this.ws?.readyState === WebSocket.OPEN) {
            const msg = this.queue.shift();
            try {
                this.ws.send(msg);
            }
            catch {
                break;
            }
        }
    }
    scheduleReconnect() {
        if (this.destroyed || this.reconnectTimer)
            return;
        this.reconnectTimer = setTimeout(() => {
            this.reconnectTimer = null;
            this.connect();
        }, this.reconnectDelay);
        this.reconnectDelay = Math.min(this.reconnectDelay * 2, 30000);
    }
    startHeartbeat() {
        this.heartbeatTimer = setInterval(() => {
            this.emit('cli:heartbeat', { uptime: process.uptime() });
        }, 30000);
    }
    stopHeartbeat() {
        if (this.heartbeatTimer) {
            clearInterval(this.heartbeatTimer);
            this.heartbeatTimer = null;
        }
    }
}
