/**
 * WebSocket server for VS Code extension communication
 */
import { readFile } from 'node:fs/promises';
import { randomUUID } from 'crypto';
import { WebSocket, WebSocketServer } from 'ws';
import { BoundedMap } from '../utils/bounded-map.js';
import { formatError } from '../utils/error-formatter.js';
import { getLogger } from '../utils/logging/index.js';
import { DEFAULT_PORT, PROTOCOL_VERSION, } from './protocol.js';
let cachedCliVersion = null;
async function getCliVersion() {
    if (cachedCliVersion) {
        return cachedCliVersion;
    }
    try {
        const content = await readFile(new URL('../../package.json', import.meta.url), 'utf-8');
        const packageJson = JSON.parse(content);
        cachedCliVersion = packageJson.version ?? '0.0.0';
        return cachedCliVersion;
    }
    catch (error) {
        console.warn('Failed to load CLI version from package.json:', error);
        cachedCliVersion = '0.0.0';
        return cachedCliVersion;
    }
}
export class VSCodeServer {
    port;
    wss = null;
    clients = new Set();
    pendingChanges = new BoundedMap({
        maxSize: 1000,
        ttl: 30 * 60 * 1000, // 30 minutes
    });
    callbacks = {};
    currentModel;
    currentProvider;
    cliVersion = '0.0.0';
    constructor(port = DEFAULT_PORT) {
        this.port = port;
    }
    /**
     * Get the actual port the server is listening on
     */
    getPort() {
        return this.port;
    }
    /**
     * Try to start the WebSocket server on a specific port
     */
    async tryStartOnPort(port) {
        return new Promise(resolve => {
            try {
                const wss = new WebSocketServer({
                    port,
                    host: '127.0.0.1', // Only accept local connections
                });
                wss.on('listening', () => {
                    this.wss = wss;
                    this.port = port;
                    this.wss.on('connection', ws => {
                        this.handleConnection(ws);
                    });
                    resolve(true);
                });
                wss.on('error', _error => {
                    wss.close();
                    resolve(false);
                });
            }
            catch (_error) {
                resolve(false);
            }
        });
    }
    /**
     * Start the WebSocket server with automatic port fallback
     * If the requested port is in use, tries up to 10 alternative ports
     */
    async start() {
        this.cliVersion = await getCliVersion();
        const logger = getLogger();
        const requestedPort = this.port;
        const maxRetries = 10;
        // Try the requested port first
        const success = await this.tryStartOnPort(requestedPort);
        if (success) {
            logger.info(`VS Code server listening on port ${this.port}`);
            return true;
        }
        // If failed, try alternative ports
        logger.warn(`Port ${requestedPort} is in use, trying alternative ports...`);
        for (let i = 1; i <= maxRetries; i++) {
            const alternativePort = requestedPort + i;
            const success = await this.tryStartOnPort(alternativePort);
            if (success) {
                logger.info(`VS Code server listening on port ${this.port} (requested ${requestedPort} was in use)`);
                return true;
            }
        }
        // All ports failed
        logger.error(`Failed to start VS Code server. Tried ports ${requestedPort}-${requestedPort + maxRetries}`);
        console.error(`[VS Code] Could not start server. Ports ${requestedPort}-${requestedPort + maxRetries} are all in use.`);
        console.error('[VS Code] Try closing other nanocoder instances or VS Code windows.');
        return false;
    }
    /**
     * Stop the WebSocket server
     */
    async stop() {
        // Close all client connections
        for (const client of this.clients) {
            client.close();
        }
        this.clients.clear();
        // Close server
        return new Promise(resolve => {
            if (this.wss) {
                this.wss.close(() => {
                    this.wss = null;
                    resolve();
                });
            }
            else {
                resolve();
            }
        });
    }
    /**
     * Register callbacks for client messages
     */
    onCallbacks(callbacks) {
        this.callbacks = { ...this.callbacks, ...callbacks };
    }
    /**
     * Check if any clients are connected
     */
    hasConnections() {
        return this.clients.size > 0;
    }
    /**
     * Get number of connected clients
     */
    getConnectionCount() {
        return this.clients.size;
    }
    /**
     * Send a file change notification to VS Code
     */
    sendFileChange(filePath, originalContent, newContent, toolName, toolArgs) {
        const id = randomUUID();
        // Store pending change
        this.pendingChanges.set(id, {
            id,
            filePath,
            originalContent,
            newContent,
            toolName,
            timestamp: Date.now(),
        });
        const message = {
            type: 'file_change',
            id,
            filePath,
            originalContent,
            newContent,
            toolName,
            toolArgs,
        };
        this.broadcast(message);
        return id;
    }
    /**
     * Send an assistant message to VS Code
     */
    sendAssistantMessage(content, isGenerating = false) {
        const message = {
            type: 'assistant_message',
            content,
            isGenerating,
        };
        this.broadcast(message);
    }
    /**
     * Send status update to VS Code
     */
    sendStatus(model, provider) {
        this.currentModel = model;
        this.currentProvider = provider;
        const message = {
            type: 'status',
            connected: true,
            model,
            provider,
            workingDirectory: process.cwd(),
        };
        this.broadcast(message);
    }
    /**
     * Request diagnostics from VS Code
     */
    requestDiagnostics(filePath) {
        const message = {
            type: 'diagnostics_request',
            filePath,
        };
        this.broadcast(message);
    }
    /**
     * Close diff preview in VS Code (when tool is confirmed/rejected in CLI)
     */
    closeDiff(id) {
        const message = {
            type: 'close_diff',
            id,
        };
        this.broadcast(message);
        // Also remove from pending changes
        this.pendingChanges.delete(id);
    }
    /**
     * Close all pending diff previews
     */
    closeAllDiffs() {
        const pendingIds = Array.from(this.pendingChanges.keys());
        for (const id of pendingIds) {
            this.closeDiff(id);
        }
    }
    /**
     * Get a pending change by ID
     */
    getPendingChange(id) {
        return this.pendingChanges.get(id);
    }
    /**
     * Remove a pending change
     */
    removePendingChange(id) {
        this.pendingChanges.delete(id);
    }
    /**
     * Get all pending changes
     */
    getAllPendingChanges() {
        return Array.from(this.pendingChanges.values());
    }
    handleConnection(ws) {
        this.clients.add(ws);
        // Send connection acknowledgment
        const ack = {
            type: 'connection_ack',
            protocolVersion: PROTOCOL_VERSION,
            cliVersion: this.cliVersion,
        };
        ws.send(JSON.stringify(ack));
        // Send current status
        if (this.currentModel || this.currentProvider) {
            this.sendStatus(this.currentModel, this.currentProvider);
        }
        // Notify callback
        this.callbacks.onConnect?.();
        ws.on('message', (data) => {
            try {
                const message = JSON.parse(data.toString());
                this.handleMessage(message);
            }
            catch (error) {
                const logger = getLogger();
                logger.error({ error: formatError(error) }, 'Failed to parse message from VS Code');
            }
        });
        ws.on('close', () => {
            this.clients.delete(ws);
            this.callbacks.onDisconnect?.();
        });
        ws.on('error', _error => {
            this.clients.delete(ws);
        });
    }
    handleMessage(message) {
        switch (message.type) {
            case 'send_prompt':
                this.callbacks.onPrompt?.(message.prompt, message.context);
                break;
            case 'apply_change':
                this.pendingChanges.delete(message.id);
                this.callbacks.onChangeApplied?.(message.id);
                break;
            case 'reject_change':
                this.pendingChanges.delete(message.id);
                this.callbacks.onChangeRejected?.(message.id);
                break;
            case 'get_status':
                this.sendStatus(this.currentModel, this.currentProvider);
                break;
            case 'context':
                this.callbacks.onContext?.({
                    workspaceFolder: message.workspaceFolder,
                    openFiles: message.openFiles,
                    activeFile: message.activeFile,
                    diagnostics: message.diagnostics,
                });
                break;
            case 'diagnostics_response':
                this.callbacks.onDiagnosticsResponse?.(message.diagnostics);
                break;
        }
    }
    broadcast(message) {
        const data = JSON.stringify(message);
        for (const client of this.clients) {
            if (client.readyState === WebSocket.OPEN) {
                client.send(data);
            }
        }
    }
}
// Singleton instance for global access
let serverInstance = null;
let serverInitPromise = null;
/**
 * Get or create the VS Code server singleton
 * Uses promise-based initialization to prevent race conditions
 */
export async function getVSCodeServer(port) {
    if (serverInstance) {
        return serverInstance;
    }
    if (serverInitPromise) {
        return serverInitPromise;
    }
    // Create server synchronously to ensure serverInstance is set immediately
    // This is important for synchronous functions like sendFileChangeToVSCode
    serverInstance = new VSCodeServer(port);
    serverInitPromise = Promise.resolve(serverInstance);
    return serverInitPromise;
}
/**
 * Get the VS Code server instance if it exists (synchronous)
 * Returns null if not yet initialized
 * Use this when you need synchronous access and the server may not be initialized
 */
export function getVSCodeServerSync() {
    return serverInstance;
}
/**
 * Check if VS Code server is active and has connections
 */
export function isVSCodeConnected() {
    return serverInstance?.hasConnections() ?? false;
}
/**
 * Send a file change to VS Code for preview/approval
 * This is the main entry point for tools to integrate with VS Code
 */
export function sendFileChangeToVSCode(filePath, originalContent, newContent, toolName, toolArgs) {
    if (!serverInstance?.hasConnections()) {
        return null;
    }
    return serverInstance.sendFileChange(filePath, originalContent, newContent, toolName, toolArgs);
}
/**
 * Close a diff preview in VS Code (when tool confirmed/rejected in CLI)
 */
export function closeDiffInVSCode(id) {
    if (!id || !serverInstance?.hasConnections()) {
        return;
    }
    serverInstance.closeDiff(id);
}
/**
 * Close all pending diff previews in VS Code
 */
export function closeAllDiffsInVSCode() {
    if (!serverInstance?.hasConnections()) {
        return;
    }
    serverInstance.closeAllDiffs();
}
//# sourceMappingURL=vscode-server.js.map