/**
 * CodeIntelligence — unified facade over tree-sitter and LSP services.
 *
 * Design principles:
 *   - Graceful degradation: never throw, always return safe defaults.
 *   - LSP is optional; falls back to tree-sitter where possible.
 *   - Tree-sitter is optional; falls back to empty arrays/null.
 *   - On initialization, language configs are read and registered with LspService
 *     so user/project overrides in the configured languages directory take effect.
 */
import { resolve } from 'path';
import { pathToFileURL, fileURLToPath } from 'url';
import { loadLanguageConfigs } from './config.js';
import { TreeSitterService } from './tree-sitter/service.js';
import { LspService } from './lsp/service.js';
import { detectLanguage } from './tree-sitter/languages.js';
import { extractSymbols, extractOutline, findEnclosingScope } from './tree-sitter/queries.js';
import { logger } from '../utils/logger.js';
import { summarizeError } from '../utils/error-display.js';
// ---------------------------------------------------------------------------
// URI helpers
// ---------------------------------------------------------------------------
/**
 * Convert a filesystem path to a file:// URI.
 * Correctly handles paths with spaces and special characters.
 */
export function pathToUri(filePath) {
    return pathToFileURL(resolve(filePath)).href;
}
/**
 * Convert a file:// URI back to a filesystem path.
 * Correctly handles percent-encoded URIs.
 */
export function uriToPath(uri) {
    return fileURLToPath(uri);
}
// ---------------------------------------------------------------------------
// CodeIntelligence
// ---------------------------------------------------------------------------
export class CodeIntelligence {
    treeSitter;
    lsp;
    shellPaths;
    constructor(options = {}) {
        this.shellPaths = options.shellPaths ?? null;
        this.treeSitter = options.treeSitter ?? new TreeSitterService();
        this.lsp = options.lsp ?? (this.shellPaths ? new LspService(this.shellPaths) : null);
    }
    // -------------------------------------------------------------------------
    // Lifecycle
    // -------------------------------------------------------------------------
    /**
     * Initialize all services. Call once at startup.
     * Loads language configs and registers LSP server commands with LspService
     * so that user/project overrides in the configured languages directory are respected.
     */
    async initialize() {
        if (!this.shellPaths) {
            try {
                await this.treeSitter.initialize();
            }
            catch (err) {
                logger.debug('CodeIntelligence: TreeSitterService init error', { error: summarizeError(err) });
            }
            return;
        }
        // Wire language configs into LspService (resolves config/facade disconnection).
        // Only register languages that have an LSP command defined — don't overwrite
        // configs that were already registered via registerServer().
        try {
            const lsp = this.lsp;
            if (!lsp) {
                await this.treeSitter.initialize();
                return;
            }
            const configs = loadLanguageConfigs(this.shellPaths);
            for (const [langId, cfg] of configs) {
                if (cfg.lsp) {
                    lsp.registerServer(langId, {
                        command: cfg.lsp.command,
                        args: cfg.lsp.args,
                        initializationOptions: cfg.lsp.initializationOptions,
                    });
                }
            }
        }
        catch (err) {
            logger.debug('CodeIntelligence: failed to load language configs', { error: summarizeError(err) });
        }
        // Initialize tree-sitter WASM loader.
        try {
            await this.treeSitter.initialize();
        }
        catch (err) {
            logger.debug('CodeIntelligence: TreeSitterService init error', { error: summarizeError(err) });
        }
        // LspService has no async init; LSP servers are started on demand.
    }
    /** Shutdown all owned services for this facade instance. */
    async dispose() {
        try {
            await this.lsp?.shutdown();
        }
        catch (err) {
            logger.debug('LSP shutdown error', { error: summarizeError(err) });
        }
        try {
            this.treeSitter.dispose();
        }
        catch (err) {
            logger.debug('TreeSitter dispose error', { error: summarizeError(err) });
        }
    }
    // -------------------------------------------------------------------------
    // Private helpers
    // -------------------------------------------------------------------------
    /**
     * Parse a file and load its tree-sitter language in one step.
     * Returns null if the language is unknown or grammar unavailable.
     */
    async _parseFile(filePath, content) {
        const lang = detectLanguage(filePath);
        if (!lang)
            return null;
        const tree = await this.treeSitter.parse(filePath, content, lang);
        if (!tree)
            return null;
        const language = await this.treeSitter.loadLanguage(lang);
        if (!language)
            return null;
        return { tree, language, lang };
    }
    // -------------------------------------------------------------------------
    // Tree-sitter operations
    // -------------------------------------------------------------------------
    /** Detect the language of a file by its extension. */
    detectLanguage(filePath) {
        return detectLanguage(filePath);
    }
    /** Check if a tree-sitter grammar is loaded for a file's language. */
    hasTreeSitter(filePath) {
        const lang = detectLanguage(filePath);
        if (!lang)
            return false;
        return this.treeSitter.loadedLanguages.includes(lang);
    }
    /**
     * Parse a file and extract symbols.
     * Returns empty array if no grammar is loaded for the language.
     */
    async getSymbols(filePath, content) {
        try {
            const parsed = await this._parseFile(filePath, content);
            if (!parsed)
                return [];
            return extractSymbols(parsed.tree, parsed.language, parsed.lang);
        }
        catch (err) {
            logger.debug('CodeIntelligence.getSymbols error', { filePath, error: summarizeError(err) });
            return [];
        }
    }
    /**
     * Parse a file and extract an outline (signatures only).
     * Returns empty array if no grammar is loaded for the language.
     */
    async getOutline(filePath, content) {
        try {
            const parsed = await this._parseFile(filePath, content);
            if (!parsed)
                return [];
            return extractOutline(parsed.tree, parsed.language, parsed.lang);
        }
        catch (err) {
            logger.debug('CodeIntelligence.getOutline error', { filePath, error: summarizeError(err) });
            return [];
        }
    }
    /**
     * Find the scope (function/class) enclosing a line.
     * Returns null if unavailable.
     */
    async getEnclosingScope(filePath, content, line) {
        try {
            const parsed = await this._parseFile(filePath, content);
            if (!parsed)
                return null;
            return findEnclosingScope(parsed.tree, parsed.language, parsed.lang, line);
        }
        catch (err) {
            logger.debug('CodeIntelligence.getEnclosingScope error', { filePath, error: summarizeError(err) });
            return null;
        }
    }
    // -------------------------------------------------------------------------
    // LSP operations
    // -------------------------------------------------------------------------
    /** Check if an LSP server is available for a file's language. */
    async hasLsp(filePath) {
        if (!this.lsp)
            return false;
        const lang = detectLanguage(filePath);
        if (!lang)
            return false;
        try {
            return await this.lsp.isAvailable(lang);
        }
        catch (err) {
            logger.debug('hasLsp error', { error: summarizeError(err) });
            return false;
        }
    }
    /**
     * Get definition location.
     * Returns null if LSP not available for the file's language.
     */
    async getDefinition(filePath, line, column) {
        const lang = detectLanguage(filePath);
        if (!lang)
            return null;
        try {
            const client = await this.lsp?.getClient(lang);
            if (!client)
                return null;
            const uri = pathToUri(filePath);
            const result = await client.request('textDocument/definition', {
                textDocument: { uri },
                position: { line, character: column },
            });
            if (!result)
                return null;
            if (Array.isArray(result))
                return result[0] ?? null;
            return result;
        }
        catch (err) {
            logger.debug('CodeIntelligence.getDefinition error', { filePath, error: summarizeError(err) });
            return null;
        }
    }
    /**
     * Get all references to a symbol.
     * Returns empty array if LSP not available.
     */
    async getReferences(filePath, line, column) {
        const lang = detectLanguage(filePath);
        if (!lang)
            return [];
        try {
            const client = await this.lsp?.getClient(lang);
            if (!client)
                return [];
            const uri = pathToUri(filePath);
            const result = await client.request('textDocument/references', {
                textDocument: { uri },
                position: { line, character: column },
                context: { includeDeclaration: true },
            });
            return result ?? [];
        }
        catch (err) {
            logger.debug('CodeIntelligence.getReferences error', { filePath, error: summarizeError(err) });
            return [];
        }
    }
    /**
     * Get document symbols.
     * Tries LSP first; falls back to tree-sitter if LSP is unavailable.
     */
    async getDocumentSymbols(filePath, content) {
        const lang = detectLanguage(filePath);
        // Try LSP first
        if (lang) {
            try {
                const client = await this.lsp?.getClient(lang);
                if (client) {
                    const uri = pathToUri(filePath);
                    const lspSymbols = await client.request('textDocument/documentSymbol', { textDocument: { uri } });
                    if (lspSymbols && lspSymbols.length > 0)
                        return lspSymbols;
                }
            }
            catch (err) {
                logger.debug('CodeIntelligence.getDocumentSymbols LSP error', { filePath, error: summarizeError(err) });
            }
        }
        // Fall back to tree-sitter
        return this.getSymbols(filePath, content);
    }
    /**
     * Get hover information.
     * Returns null if LSP not available.
     */
    async getHover(filePath, line, column) {
        const lang = detectLanguage(filePath);
        if (!lang)
            return null;
        try {
            const client = await this.lsp?.getClient(lang);
            if (!client)
                return null;
            const uri = pathToUri(filePath);
            return await client.request('textDocument/hover', {
                textDocument: { uri },
                position: { line, character: column },
            }) ?? null;
        }
        catch (err) {
            logger.debug('CodeIntelligence.getHover error', { filePath, error: summarizeError(err) });
            return null;
        }
    }
    /**
     * Get diagnostics for a file.
     * Returns empty array if LSP not available.
     * Note: LSP pushes diagnostics via textDocument/publishDiagnostics notifications;
     * this method is a best-effort pull (not all servers support textDocument/diagnostic).
     */
    async getDiagnostics(filePath) {
        const lang = detectLanguage(filePath);
        if (!lang)
            return [];
        try {
            const client = await this.lsp?.getClient(lang);
            if (!client)
                return [];
            const uri = pathToUri(filePath);
            const result = await client.request('textDocument/diagnostic', { textDocument: { uri } });
            return result?.items ?? [];
        }
        catch (err) {
            logger.debug('CodeIntelligence.getDiagnostics error', { filePath, error: summarizeError(err) });
            return [];
        }
    }
}
