import { logger } from '../../utils/logger.js';
import { summarizeError } from '../../utils/error-display.js';
const DEFAULT_TIMEOUT_MS = 30_000;
export class LspClient {
    command;
    args;
    options;
    proc = null;
    nextId = 1;
    pendingRequests = new Map();
    buffer = '';
    readLoopRunning = false;
    constructor(command, args, options) {
        this.command = command;
        this.args = args;
        this.options = options;
    }
    /** Start the LSP server process. */
    async start() {
        if (this.proc)
            return;
        try {
            this.proc = Bun.spawn([this.command, ...this.args], {
                stdin: 'pipe',
                stdout: 'pipe',
                stderr: 'pipe',
                cwd: this.options?.cwd,
                env: this.options?.env ? { ...process.env, ...this.options.env } : undefined,
            });
            this._startReadLoop();
        }
        catch (err) {
            logger.error('LspClient: failed to start process', { command: this.command, err: summarizeError(err) });
            this.proc = null;
            throw err;
        }
    }
    /** Send a request and wait for response. */
    async request(method, params) {
        if (!this.proc || !this.isRunning) {
            throw new Error('LspClient: server is not running');
        }
        const id = this.nextId++;
        const msg = { jsonrpc: '2.0', id, method, params };
        const json = JSON.stringify(msg);
        const frame = LspClient.encodeFrame(json);
        return new Promise((resolve, reject) => {
            const timeoutMs = this.options?.timeout ?? DEFAULT_TIMEOUT_MS;
            const timer = setTimeout(() => {
                this.pendingRequests.delete(id);
                reject(new Error(`LspClient: request '${method}' timed out after ${timeoutMs}ms`));
            }, timeoutMs);
            this.pendingRequests.set(id, {
                resolve: resolve,
                reject,
                timer,
            });
            try {
                this.proc.stdin.write(frame);
            }
            catch (err) {
                clearTimeout(timer);
                this.pendingRequests.delete(id);
                reject(new Error(`LspClient: failed to write request: ${summarizeError(err)}`));
            }
        });
    }
    /** Send a notification (no response expected). */
    notify(method, params) {
        if (!this.proc || !this.isRunning)
            return;
        try {
            const msg = { jsonrpc: '2.0', method, params };
            const json = JSON.stringify(msg);
            const frame = LspClient.encodeFrame(json);
            this.proc.stdin.write(frame);
        }
        catch (err) {
            logger.error('LspClient: failed to send notification', { method, err: summarizeError(err) });
        }
    }
    /** Stop the server process. */
    async stop() {
        if (!this.proc)
            return;
        // Reject all pending requests
        for (const [id, pending] of this.pendingRequests) {
            clearTimeout(pending.timer);
            pending.reject(new Error('LspClient: server stopped'));
            this.pendingRequests.delete(id);
        }
        try {
            this.proc.stdin.end();
            this.proc.kill();
            await this.proc.exited;
        }
        catch {
            // Ignore errors during shutdown
        }
        finally {
            this.proc = null;
            this.buffer = '';
            this.readLoopRunning = false;
        }
    }
    /** Is the server running? */
    get isRunning() {
        if (!this.proc)
            return false;
        // Bun.spawn process: check exitCode — null means still running
        try {
            return this.proc.exitCode === null;
        }
        catch {
            return false;
        }
    }
    // ---------------------------------------------------------------------------
    // Private helpers
    // ---------------------------------------------------------------------------
    _startReadLoop() {
        if (this.readLoopRunning || !this.proc)
            return;
        this.readLoopRunning = true;
        const proc = this.proc;
        const decoder = new TextDecoder();
        (async () => {
            try {
                const reader = proc.stdout.getReader();
                while (true) {
                    const { done, value } = await reader.read();
                    if (done)
                        break;
                    this.buffer += decoder.decode(value, { stream: true });
                    this._processBuffer();
                }
            }
            catch (err) {
                logger.debug('LspClient: stdout read loop ended', { err: summarizeError(err) });
            }
            finally {
                this.readLoopRunning = false;
                // Reject any remaining pending requests
                for (const [id, pending] of this.pendingRequests) {
                    clearTimeout(pending.timer);
                    pending.reject(new Error('LspClient: server process exited unexpectedly'));
                    this.pendingRequests.delete(id);
                }
            }
        })();
    }
    _processBuffer() {
        while (true) {
            // Look for the header terminator
            const headerEnd = this.buffer.indexOf('\r\n\r\n');
            if (headerEnd === -1)
                break;
            const header = this.buffer.slice(0, headerEnd);
            const contentLengthMatch = header.match(/Content-Length:\s*(\d+)/i);
            if (!contentLengthMatch) {
                // Malformed header — skip to next boundary
                this.buffer = this.buffer.slice(headerEnd + 4);
                continue;
            }
            const contentLength = parseInt(contentLengthMatch[1], 10);
            const bodyStart = headerEnd + 4;
            const bodyEnd = bodyStart + contentLength;
            // Wait for full body
            if (this.buffer.length < bodyEnd)
                break;
            const body = this.buffer.slice(bodyStart, bodyEnd);
            this.buffer = this.buffer.slice(bodyEnd);
            this._dispatchMessage(body);
        }
    }
    _dispatchMessage(body) {
        let msg;
        try {
            msg = JSON.parse(body);
        }
        catch (err) {
            logger.error('LspClient: failed to parse JSON-RPC message', { err: summarizeError(err), body: body.slice(0, 200) });
            return;
        }
        if (typeof msg !== 'object' || msg === null)
            return;
        const response = msg;
        if ('id' in response && typeof response.id === 'number') {
            const pending = this.pendingRequests.get(response.id);
            if (pending) {
                clearTimeout(pending.timer);
                this.pendingRequests.delete(response.id);
                if (response.error) {
                    pending.reject(new Error(`LSP error ${response.error.code}: ${response.error.message}`));
                }
                else {
                    pending.resolve(response.result);
                }
            }
        }
        // Notifications (no id) are silently ignored for now
    }
    /** Encode a JSON-RPC frame (Content-Length framing). Exposed for testing. */
    static encodeFrame(json) {
        const bytes = Buffer.byteLength(json, 'utf-8');
        return `Content-Length: ${bytes}\r\n\r\n${json}`;
    }
    /** Parse all complete JSON-RPC messages from a buffer string. Returns [messages, remainingBuffer]. */
    static parseMessages(buffer) {
        const messages = [];
        let remaining = buffer;
        while (true) {
            const headerEnd = remaining.indexOf('\r\n\r\n');
            if (headerEnd === -1)
                break;
            const header = remaining.slice(0, headerEnd);
            const contentLengthMatch = header.match(/Content-Length:\s*(\d+)/i);
            if (!contentLengthMatch) {
                remaining = remaining.slice(headerEnd + 4);
                continue;
            }
            const contentLength = parseInt(contentLengthMatch[1], 10);
            const bodyStart = headerEnd + 4;
            const bodyEnd = bodyStart + contentLength;
            if (remaining.length < bodyEnd)
                break;
            const body = remaining.slice(bodyStart, bodyEnd);
            remaining = remaining.slice(bodyEnd);
            try {
                messages.push(JSON.parse(body));
            }
            catch {
                // skip malformed
            }
        }
        return [messages, remaining];
    }
}
