import { existsSync } from 'fs';
import { join } from 'path';
import { pathToFileURL } from 'url';
import { logger } from '../../utils/logger.js';
import { LspClient } from './client.js';
import { getBinaryPath, ensureBinary } from './binary-downloader.js';
import { summarizeError } from '../../utils/error-display.js';
/**
 * Resolve a server command: check node_modules/.bin/ first (bundled),
 * then fall back to system PATH via Bun.which().
 * Returns the resolved command path, or the original command if neither found.
 */
function resolveCommand(command, workingDirectory) {
    // Try node_modules/.bin in project root
    const bundled = join(workingDirectory, 'node_modules', '.bin', command);
    if (existsSync(bundled))
        return bundled;
    // Try .goodvibes/bin/ (downloaded binaries)
    const downloaded = getBinaryPath(join(workingDirectory, '.goodvibes', 'bin'), command);
    if (existsSync(downloaded))
        return downloaded;
    // Try system PATH
    const system = Bun.which(command);
    if (system)
        return system;
    // Return as-is — spawn will fail with a clear error
    return command;
}
/** Well-known LSP server command → language IDs mapping for auto-detection. */
const WELL_KNOWN_SERVERS = [
    { command: 'typescript-language-server', langIds: ['typescript', 'tsx', 'javascript'], args: ['--stdio'] },
    { command: 'pyright-langserver', langIds: ['python'], args: ['--stdio'] },
    { command: 'pylsp', langIds: ['python'], args: [] },
    { command: 'rust-analyzer', langIds: ['rust'], args: [] },
    { command: 'gopls', langIds: ['go'], args: ['serve'] },
    { command: 'bash-language-server', langIds: ['bash'], args: ['start'] },
    { command: 'vscode-css-language-server', langIds: ['css'], args: ['--stdio'] },
    { command: 'vscode-html-language-server', langIds: ['html'], args: ['--stdio'] },
    { command: 'vscode-json-language-server', langIds: ['json'], args: ['--stdio'] },
];
export class LspService {
    clients = new Map();
    configs = new Map();
    initializing = new Map();
    roots;
    constructor(roots) {
        this.roots = roots;
    }
    /** Register a server configuration for a language. */
    registerServer(langId, config) {
        this.configs.set(langId, config);
    }
    /**
     * Get or start a client for a language.
     * Returns null if no server is configured or if the server fails to start.
     */
    async getClient(langId) {
        // Return existing running client
        const existing = this.clients.get(langId);
        if (existing?.isRunning)
            return existing;
        // No config registered
        if (!this.configs.has(langId))
            return null;
        // Deduplicate concurrent initialization
        const inFlight = this.initializing.get(langId);
        if (inFlight)
            return inFlight;
        const initPromise = this._startClient(langId);
        this.initializing.set(langId, initPromise);
        try {
            const client = await initPromise;
            return client;
        }
        finally {
            this.initializing.delete(langId);
        }
    }
    async _startClient(langId) {
        // Ensure binary is available (may trigger download for rust-analyzer/gopls).
        // Called here so it's covered by the dedup guard in getClient().
        await this.ensureServer(langId);
        const config = this.configs.get(langId);
        if (!config)
            return null;
        const resolvedCommand = resolveCommand(config.command, this.roots.workingDirectory);
        const client = new LspClient(resolvedCommand, config.args);
        try {
            await client.start();
            await this._initializeServer(client);
            this.clients.set(langId, client);
            logger.info('LspService: started server', { langId, command: config.command });
            return client;
        }
        catch (err) {
            logger.error('LspService: failed to start server', { langId, err: summarizeError(err) });
            try {
                await client.stop();
            }
            catch { /* ignore */ }
            return null;
        }
    }
    /**
     * Check if a server is available for a language
     * (config exists AND command is installed on PATH).
     */
    async isAvailable(langId) {
        const config = this.configs.get(langId);
        if (!config)
            return false;
        // Check bundled first
        const bundled = join(this.roots.workingDirectory, 'node_modules', '.bin', config.command);
        if (existsSync(bundled))
            return true;
        // Check downloaded binaries
        const downloaded = getBinaryPath(this.roots.resolveProjectPath('bin'), config.command);
        if (existsSync(downloaded))
            return true;
        // Then system PATH
        try {
            const resolved = Bun.which(config.command);
            return resolved !== null;
        }
        catch {
            return false;
        }
    }
    /** Initialize a server with the LSP handshake. */
    async _initializeServer(client) {
        await client.request('initialize', {
            processId: process.pid,
            rootUri: pathToFileURL(this.roots.workingDirectory).href,
            capabilities: {
                textDocument: {
                    documentSymbol: { hierarchicalDocumentSymbolSupport: true },
                    definition: {},
                    references: {},
                    hover: {},
                    rename: { prepareSupport: true },
                    publishDiagnostics: {},
                },
            },
        });
        client.notify('initialized', {});
    }
    /** Stop all running servers. */
    async shutdown() {
        const stops = Array.from(this.clients.values()).map(c => c.stop().catch(() => { }));
        await Promise.all(stops);
        this.clients.clear();
        this.initializing.clear();
    }
    /**
     * Auto-detect installed LSP servers and return their configs.
     * Registers detected servers automatically.
     */
    async detectServers() {
        const detected = new Map();
        for (const { command, langIds, args } of WELL_KNOWN_SERVERS) {
            let found = false;
            // Check bundled first, then .goodvibes/bin/, then system PATH
            const bundledPath = join(this.roots.workingDirectory, 'node_modules', '.bin', command);
            if (existsSync(bundledPath)) {
                found = true;
            }
            // Check .goodvibes/bin/ (downloaded binaries)
            if (!found) {
                const downloaded = getBinaryPath(this.roots.resolveProjectPath('bin'), command);
                found = existsSync(downloaded);
            }
            if (!found) {
                try {
                    found = Bun.which(command) !== null;
                }
                catch {
                    found = false;
                }
            }
            if (found) {
                const config = { command, args };
                for (const langId of langIds) {
                    // Don't overwrite already-registered configs
                    if (!this.configs.has(langId)) {
                        this.configs.set(langId, config);
                    }
                    // Add to returned detected map regardless
                    if (!detected.has(langId)) {
                        detected.set(langId, config);
                    }
                }
                logger.info('LspService: detected server', { command, langIds });
            }
        }
        return detected;
    }
    /**
     * Ensure a server binary is available for the given language.
     * Downloads the binary if it's a known downloadable server (rust-analyzer, gopls)
     * and not already available. Non-blocking for bundled/PATH servers.
     */
    async ensureServer(langId) {
        const config = this.configs.get(langId);
        if (!config)
            return;
        // Only try download for servers that aren't already available
        const available = await this.isAvailable(langId);
        if (available)
            return;
        // Try to download/install the binary
        await ensureBinary(this.roots.resolveProjectPath('bin'), config.command);
    }
}
