/**
 * Thin MCP client wrapper for code-review-graph.
 *
 * Connects via Stdio transport to the configured cr-graph binary.
 * Binary discovery order:
 *   1. NEXUS_CRG_BIN env var
 *   2. ~/.nexus-prime/cr-graph.json  { "bin": "/path/to/binary", "args": [...] }
 *   3. legacy <repo>/.mcp.json code-review-graph entry
 *   4. uvx code-review-graph serve default, scoped to the repo
 *
 * Failure mode: when the binary is not found or the process fails to start,
 * the client returns `available = false` and all methods return empty results.
 * The orchestrator falls back to the n-gram index. No regression.
 */
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import BetterSqlite3 from 'better-sqlite3';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { nexusEventBus } from './event-bus.js';
const MIN_CRG_CANDIDATES = 5;
const REQUEST_TIMEOUT_MS = 4_000;
const INIT_TIMEOUT_MS = 4_000;
const FAST_REQUEST_TIMEOUT_MS = 650;
export function discoverCrGraphConfig(repoRoot) {
    // 1. Env var
    const envBin = process.env['NEXUS_CRG_BIN'];
    if (envBin) {
        return { bin: envBin, args: [], cwd: repoRoot };
    }
    // 2. Hidden Nexus-owned config. User-facing MCP configs should expose only nexus-prime.
    try {
        const configPath = path.join(os.homedir(), '.nexus-prime', 'cr-graph.json');
        const raw = fs.readFileSync(configPath, 'utf8');
        const parsed = JSON.parse(raw);
        if (typeof parsed['bin'] === 'string') {
            return {
                bin: parsed['bin'],
                args: Array.isArray(parsed['args']) ? parsed['args'] : [],
                cwd: typeof parsed['cwd'] === 'string' ? parsed['cwd'] : undefined,
            };
        }
    }
    catch {
        // not found or malformed — unavailable
    }
    // 3. Legacy fallback for older workspaces that still have a public peer entry.
    const repoMcpConfig = repoRoot ? readMcpCrGraphConfig(path.join(repoRoot, '.mcp.json'), repoRoot) : null;
    if (repoMcpConfig)
        return repoMcpConfig;
    // 4. Default internal transport. If uvx or code-review-graph is unavailable,
    // initialize() degrades gracefully and Nexus falls back to the n-gram index.
    if (repoRoot) {
        return { bin: 'uvx', args: ['code-review-graph', 'serve'], cwd: repoRoot };
    }
    return null;
}
function readMcpCrGraphConfig(configPath, repoRoot) {
    try {
        const raw = fs.readFileSync(configPath, 'utf8');
        const parsed = JSON.parse(raw);
        const servers = parsed['mcpServers'];
        if (!servers || typeof servers !== 'object' || Array.isArray(servers))
            return null;
        const server = servers['code-review-graph'];
        if (!server || typeof server !== 'object' || Array.isArray(server))
            return null;
        const record = server;
        if (typeof record['command'] !== 'string' || !record['command'])
            return null;
        return {
            bin: record['command'],
            args: Array.isArray(record['args'])
                ? record['args'].filter((arg) => typeof arg === 'string')
                : [],
            cwd: repoRoot,
        };
    }
    catch {
        return null;
    }
}
export class CodeReviewGraphClient {
    client = null;
    transport = null;
    _available = false;
    _initialized = false;
    initPromise = null;
    repoRoot;
    closing = false;
    constructor(repoRoot) {
        this.repoRoot = repoRoot;
    }
    get available() {
        return this._available;
    }
    async init() {
        this.closing = false;
        if (this.initPromise)
            return this.initPromise;
        if (this._initialized)
            return;
        this.initPromise = this.initialize().finally(() => {
            this.initPromise = null;
        });
        return this.initPromise;
    }
    async initialize() {
        this._initialized = true;
        const config = discoverCrGraphConfig(this.repoRoot);
        if (!config)
            return;
        try {
            this.transport = new StdioClientTransport({
                command: config.bin,
                args: config.args ?? [],
                cwd: config.cwd ?? this.repoRoot,
                stderr: 'ignore',
            });
            this.client = new Client({ name: 'nexus-prime', version: '1.0.0' }, { capabilities: {} });
            await withTimeout(this.client.connect(this.transport), INIT_TIMEOUT_MS, 'cr-graph init timeout');
            unrefTransportProcess(this.transport);
            if (this.closing) {
                try {
                    await this.transport?.close();
                }
                catch { /* best-effort */ }
                this.client = null;
                this.transport = null;
                this._available = false;
                return;
            }
            this._available = true;
        }
        catch {
            // binary not found or failed to start — degrade gracefully
            try {
                await this.transport?.close();
            }
            catch { /* best-effort */ }
            this.client = null;
            this.transport = null;
            this._available = false;
        }
    }
    /**
     * Search for file nodes relevant to a task description.
     * Returns absolute file paths, clamped to `limit`.
     * Returns empty array when unavailable or on error.
     */
    async semanticSearchNodes(query, limit = 20, options = {}) {
        const waitForColdInit = options.waitForColdInit ?? true;
        if (!this._available && !waitForColdInit) {
            return [];
        }
        if (this.initPromise && !waitForColdInit)
            return [];
        await this.init();
        if (!this._available || !this.client)
            return [];
        const timeoutMs = options.timeoutMs ?? REQUEST_TIMEOUT_MS;
        try {
            const result = await withTimeout(this.client.callTool({
                name: 'semantic_search_nodes',
                arguments: { query, limit: limit * 2, node_types: ['file', 'function', 'class', 'interface'] },
            }), timeoutMs, 'cr-graph timeout');
            return extractFilePaths(result, this.repoRoot, limit);
        }
        catch {
            return [];
        }
    }
    /**
     * Returns true when results are sufficient to use as primary source.
     */
    static isSufficient(results) {
        return results.length >= MIN_CRG_CANDIDATES;
    }
    async semanticSearchNodesFast(query, limit = 20) {
        const localResults = this.searchLocalGraphDb(query, limit);
        if (localResults.length > 0)
            return localResults;
        if (!this._available)
            return [];
        return this.semanticSearchNodes(query, limit, {
            timeoutMs: FAST_REQUEST_TIMEOUT_MS,
            waitForColdInit: false,
        });
    }
    searchLocalGraphDb(query, limit) {
        const dbPath = path.join(this.repoRoot, '.code-review-graph', 'graph.db');
        if (!fs.existsSync(dbPath))
            return [];
        const terms = extractGraphSearchTerms(query);
        if (terms.length === 0)
            return [];
        let db;
        try {
            db = new BetterSqlite3(dbPath, { readonly: true, fileMustExist: true });
            const matchQuery = terms.map((term) => `${term}*`).join(' OR ');
            const rows = db.prepare(`
        SELECT n.file_path AS filePath, bm25(nodes_fts) AS score
        FROM nodes_fts
        JOIN nodes n ON n.rowid = nodes_fts.rowid
        WHERE nodes_fts MATCH ?
        ORDER BY score ASC
        LIMIT ?
      `).all(matchQuery, Math.max(limit * 4, limit));
            return dedupeGraphPaths(rows, this.repoRoot, limit);
        }
        catch {
            try {
                const likeTerms = terms.slice(0, 6).map((term) => `%${term}%`);
                const conditions = likeTerms.map(() => `
          lower(n.name || ' ' || n.qualified_name || ' ' || n.file_path || ' ' || coalesce(n.signature, '')) LIKE ?
        `).join(' OR ');
                if (!conditions)
                    return [];
                const rows = db?.prepare(`
          SELECT DISTINCT n.file_path AS filePath
          FROM nodes n
          WHERE ${conditions}
          LIMIT ?
        `).all(...likeTerms, Math.max(limit * 4, limit));
                return dedupeGraphPaths(rows ?? [], this.repoRoot, limit);
            }
            catch {
                return [];
            }
        }
        finally {
            try {
                db?.close();
            }
            catch { /* best-effort */ }
        }
    }
    async close() {
        this.closing = true;
        if (this.transport) {
            try {
                await this.transport.close();
            }
            catch { /* best-effort */ }
            this.transport = null;
        }
        this.client = null;
        this._available = false;
        this._initialized = false;
    }
}
// ─── Non-blocking graph build ─────────────────────────────────────────────────
const CRG_REBUILD_TTL_MS = 24 * 60 * 60 * 1_000; // 24 hours
const CRG_STAMP_NAME = 'cr-graph-built.json';
/**
 * Non-blocking cr-graph freshness check + build trigger.
 *
 * Called via `setImmediate` from `nexus_session_bootstrap` so the handler
 * returns immediately and the graph builds in the background.
 * - Checks a stamp file in ~/.nexus-prime to avoid rebuilding more than once
 *   per 24 hours per repo root.
 * - Emits `graph.cr.build.start` / `graph.cr.build.complete` / `graph.cr.build.failed`
 *   on the event bus so the dashboard can render a progress pill.
 * - All failures are silently swallowed — cr-graph is optional and the
 *   orchestrator falls back to the n-gram index if unavailable.
 */
export async function ensureCrGraphBuilt(repoRoot) {
    try {
        const stampPath = path.join(os.homedir(), '.nexus-prime', CRG_STAMP_NAME);
        // Mtime gate: skip if already built for this repo root within the TTL.
        try {
            const raw = fs.readFileSync(stampPath, 'utf8');
            const stamp = JSON.parse(raw);
            if (stamp.repoRoot === repoRoot &&
                typeof stamp.builtAt === 'number' &&
                Date.now() - stamp.builtAt < CRG_REBUILD_TTL_MS) {
                return;
            }
        }
        catch { /* stamp missing or corrupt — proceed */ }
        const config = discoverCrGraphConfig(repoRoot);
        if (!config)
            return; // binary not configured — nothing to do
        const transport = new StdioClientTransport({
            command: config.bin,
            args: config.args ?? [],
            cwd: config.cwd ?? repoRoot,
            stderr: 'ignore',
        });
        const client = new Client({ name: 'nexus-prime', version: '1.0.0' }, { capabilities: {} });
        await client.connect(transport);
        unrefTransportProcess(transport);
        nexusEventBus.emit('graph.cr.build.start', { repoRoot });
        await withTimeout(client.callTool({ name: 'build_or_update_graph_tool', arguments: { repo_root: repoRoot } }), 180_000, 'cr-graph build timeout');
        nexusEventBus.emit('graph.cr.build.complete', { repoRoot });
        // Persist stamp so the next boot skips the rebuild.
        try {
            fs.mkdirSync(path.join(os.homedir(), '.nexus-prime'), { recursive: true });
            fs.writeFileSync(stampPath, JSON.stringify({ builtAt: Date.now(), repoRoot }));
        }
        catch { /* best-effort */ }
        await transport.close().catch(() => { });
    }
    catch {
        nexusEventBus.emit('graph.cr.build.failed', { repoRoot });
    }
}
// ─────────────────────────────────────────────────────────────────────────────
/** Parse tool result content and extract file paths. */
function extractFilePaths(result, repoRoot, limit) {
    const content = result?.content;
    if (!Array.isArray(content))
        return [];
    const paths = [];
    for (const block of content) {
        if (block.type !== 'text' || !block.text)
            continue;
        let parsed;
        try {
            parsed = JSON.parse(block.text);
        }
        catch {
            continue;
        }
        const nodes = Array.isArray(parsed) ? parsed : parsed?.['nodes'];
        if (!Array.isArray(nodes))
            continue;
        for (const node of nodes) {
            const filePath = node?.['file_path']
                ?? node?.['path']
                ?? node?.['filePath'];
            if (typeof filePath !== 'string' || !filePath)
                continue;
            const abs = filePath.startsWith('/') ? filePath : path.resolve(repoRoot, filePath);
            paths.push(abs);
            if (paths.length >= limit)
                return paths;
        }
    }
    return paths;
}
function extractGraphSearchTerms(query) {
    return [...new Set(String(query || '')
            .toLowerCase()
            .replace(/[^a-z0-9_\s/-]/g, ' ')
            .split(/\s+/)
            .map((term) => term.replace(/^[/-]+|[/-]+$/g, ''))
            .filter((term) => term.length >= 3 && /^[a-z0-9_/-]+$/.test(term))
            .slice(0, 12))];
}
function dedupeGraphPaths(rows, repoRoot, limit) {
    const seen = new Set();
    const paths = [];
    for (const row of rows) {
        if (typeof row.filePath !== 'string' || !row.filePath)
            continue;
        const abs = row.filePath.startsWith('/') ? row.filePath : path.resolve(repoRoot, row.filePath);
        if (seen.has(abs))
            continue;
        seen.add(abs);
        paths.push(abs);
        if (paths.length >= limit)
            break;
    }
    return paths;
}
async function withTimeout(promise, timeoutMs, message) {
    let timeout;
    const timeoutPromise = new Promise((_, reject) => {
        timeout = setTimeout(() => reject(new Error(message)), timeoutMs);
        timeout.unref?.();
    });
    try {
        return await Promise.race([promise, timeoutPromise]);
    }
    finally {
        if (timeout)
            clearTimeout(timeout);
    }
}
function unrefTransportProcess(transport) {
    const child = transport._process;
    child?.unref?.();
    child?.stdin?.unref?.();
    child?.stdout?.unref?.();
    child?.stderr?.unref?.();
}
