/**
 * CodeGraph backend — `StructuralPrepass` against `@colbymchenry/codegraph`.
 *
 * Uses CodeGraph's CLI for init/index, and queries its SQLite directly via
 * the `sqlite3` command-line tool (preinstalled on macOS / Linux / Windows).
 * Falls back to grep-only style scanning if CodeGraph isn't installed.
 *
 * Design rationale: CodeGraph ships a tree-sitter-based indexer for 19+
 * languages with cross-file resolution + call-graph + FTS, all stored in
 * SQLite. We get tree-sitter coverage for free; we just translate its
 * vocabulary (class / method / function / interface) into our SpecVerse
 * vocabulary downstream in the adapter layer.
 *
 * Relies on:
 *   - `codegraph` CLI on PATH (or installable via `npm i -g @colbymchenry/codegraph`)
 *   - `sqlite3` CLI on PATH (preinstalled on most Unix systems)
 */
import { execSync, execFileSync } from 'child_process';
import { existsSync, readFileSync } from 'fs';
import { join } from 'path';
import { walkSourceTree, detectLanguage, globToRegex } from './walk.js';
import { extractEmits, extractDbWrites, extractExternalCalls, extractThrows, extractAsyncBoundaries, countBranchPoints, sliceBody, } from './method-patterns.js';
/** Map CodeGraph's `nodes.kind` enum to our Symbol['kind']. */
function mapKind(cgKind) {
    switch (cgKind) {
        case 'class': return 'class';
        case 'function': return 'function';
        case 'method': return 'method';
        case 'interface': return 'interface';
        case 'type_alias': return 'type_alias';
        case 'constant': return 'constant';
        // CodeGraph doesn't always emit 'enum' as a separate kind; treat as type_alias
        default: return null;
    }
}
export class CodeGraphBackend {
    sourceDir = '';
    dbPath = '';
    codegraphPath;
    sqlite3Path;
    reuseExistingIndex;
    capabilities = {
        callGraph: true,
        crossFileResolution: true,
        componentClustering: false, // CodeGraph doesn't have Leiden
        fullTextSearch: true, // FTS5 in SQLite
        fileWatching: true, // OS file watchers + 2s debounce
        routeExtraction: false, // codegraph backend deprecated; not extending (engines 6.30.0+)
        implementsExtraction: false, // codegraph backend deprecated
        validationSchemaExtraction: false, // codegraph backend deprecated
        processExtraction: false, // requires call-graph analysis we don't expose
    };
    constructor(options = {}) {
        this.codegraphPath = options.codegraphPath ?? this.detectBinary('codegraph');
        this.sqlite3Path = options.sqlite3Path ?? this.detectBinary('sqlite3');
        this.reuseExistingIndex = options.reuseExistingIndex ?? false;
    }
    /**
     * Check if CodeGraph + sqlite3 are available. Use this from the adapter
     * selector to decide whether to fall back to grep-only.
     */
    static isAvailable() {
        try {
            execSync('codegraph --version', { stdio: 'ignore', timeout: 5000 });
            execSync('sqlite3 --version', { stdio: 'ignore', timeout: 5000 });
            return true;
        }
        catch {
            return false;
        }
    }
    async init(sourceDir) {
        this.sourceDir = sourceDir;
        this.dbPath = join(sourceDir, '.codegraph', 'codegraph.db');
        if (this.reuseExistingIndex && existsSync(this.dbPath))
            return;
        // CodeGraph's `init` is interactive (asks about Claude integration);
        // we feed "n" via stdin to skip the interactive setup.
        if (!existsSync(join(sourceDir, '.codegraph'))) {
            execSync(`echo "n" | ${this.codegraphPath} init`, {
                cwd: sourceDir,
                stdio: 'ignore',
                timeout: 30_000,
            });
        }
    }
    async index() {
        const start = Date.now();
        if (this.reuseExistingIndex && existsSync(this.dbPath)) {
            // Trust existing index. Stats from DB.
            const stats = this.queryOne(`SELECT (SELECT COUNT(*) FROM nodes WHERE kind='file') AS files,
                (SELECT COUNT(*) FROM nodes WHERE kind != 'file') AS symbols`);
            return { ...stats, durationMs: Date.now() - start };
        }
        execSync(`${this.codegraphPath} index`, {
            cwd: this.sourceDir,
            stdio: 'ignore',
            timeout: 600_000, // 10 min for large repos
        });
        const stats = this.queryOne(`SELECT (SELECT COUNT(*) FROM nodes WHERE kind='file') AS files,
              (SELECT COUNT(*) FROM nodes WHERE kind != 'file') AS symbols`);
        return { ...stats, durationMs: Date.now() - start };
    }
    async listFiles(filter) {
        // listFiles uses an FS walk (not the index) because adapters need to find
        // arbitrary file types (schema.prisma, package.json, config files) — and
        // CodeGraph only indexes its 19 supported languages, missing those.
        let result = walkSourceTree(this.sourceDir);
        if (filter?.dir)
            result = result.filter((f) => f.startsWith(filter.dir));
        if (filter?.lang)
            result = result.filter((f) => detectLanguage(f) === filter.lang);
        if (filter?.glob) {
            const globRegex = globToRegex(filter.glob);
            result = result.filter((f) => globRegex.test(f));
        }
        return result;
    }
    async listClasses(filter) {
        let sql = `SELECT * FROM nodes WHERE kind='class'`;
        if (filter?.dir)
            sql += ` AND file_path LIKE '${this.escape(filter.dir)}%'`;
        sql += ` ORDER BY file_path, start_line`;
        const rows = this.queryAll(sql);
        return rows
            .filter((r) => !filter?.namePattern || filter.namePattern.test(r.name))
            .map((r) => this.rowToSymbol(r));
    }
    async listMethods(filter) {
        let sql = `SELECT * FROM nodes WHERE kind='method'`;
        const conditions = [];
        if (filter?.class)
            conditions.push(`qualified_name LIKE '${this.escape(filter.class)}%'`);
        if (filter?.nameIn && filter.nameIn.length > 0) {
            const list = filter.nameIn.map((n) => `'${this.escape(n)}'`).join(',');
            conditions.push(`name IN (${list})`);
        }
        if (conditions.length)
            sql += ` AND ${conditions.join(' AND ')}`;
        sql += ` ORDER BY file_path, start_line`;
        const rows = this.queryAll(sql);
        return rows.map((r) => this.rowToSymbol(r));
    }
    async listInterfaces(filter) {
        let sql = `SELECT * FROM nodes WHERE kind='interface'`;
        if (filter?.dir)
            sql += ` AND file_path LIKE '${this.escape(filter.dir)}%'`;
        sql += ` ORDER BY file_path, start_line`;
        const rows = this.queryAll(sql);
        return rows
            .filter((r) => !filter?.namePattern || filter.namePattern.test(r.name))
            .map((r) => this.rowToSymbol(r));
    }
    async listImports(file) {
        // CodeGraph stores imports as nodes of kind='import' with edges to the imported module.
        // For v1, we do a simplified query: get all import nodes in this file.
        const rel = file.startsWith(this.sourceDir) ? file.slice(this.sourceDir.length).replace(/^\//, '') : file;
        const sql = `SELECT name, qualified_name FROM nodes WHERE kind='import' AND file_path='${this.escape(rel)}'`;
        const rows = this.queryAll(sql);
        return rows.map((r) => ({
            fromFile: file,
            // qualified_name may encode the module path; name encodes the imported symbol
            toModule: r.qualified_name.includes(':') ? r.qualified_name.split(':')[0] : r.qualified_name,
            symbols: [r.name],
        }));
    }
    async fileSourceText(file) {
        return readFileSync(file, 'utf8');
    }
    async callers(symbol) {
        const sql = `
      SELECT n.* FROM edges e
      JOIN nodes n ON e.from_id = n.id
      WHERE e.to_id = (SELECT id FROM nodes WHERE qualified_name='${this.escape(symbol)}' LIMIT 1)
        AND e.kind='call'
    `;
        const rows = this.queryAll(sql);
        return rows.map((r) => this.rowToSymbol(r));
    }
    async callees(symbol) {
        const sql = `
      SELECT n.* FROM edges e
      JOIN nodes n ON e.to_id = n.id
      WHERE e.from_id = (SELECT id FROM nodes WHERE qualified_name='${this.escape(symbol)}' LIMIT 1)
        AND e.kind='call'
    `;
        const rows = this.queryAll(sql);
        return rows.map((r) => this.rowToSymbol(r));
    }
    /**
     * Per-method fact sheet via SQL (for the call graph) + regex over the body
     * (for everything else: emits, dbWrites, externalCalls, throws, async, branches).
     *
     * The SQL piece is what justifies the CodeGraph backend over grep-only here —
     * tree-sitter resolved cross-file calls deterministically, while a regex over
     * a single method body would only see same-file references.
     */
    async getMethodDetails(qualifiedName) {
        const methodSql = `
      SELECT * FROM nodes
      WHERE qualified_name='${this.escape(qualifiedName)}'
        AND kind='method'
      LIMIT 1
    `;
        const rows = this.queryAll(methodSql);
        if (rows.length === 0)
            return null;
        const methodRow = rows[0];
        // Resolve absolute file path (consistent with rowToSymbol)
        const filePath = methodRow.file_path.startsWith('/')
            ? methodRow.file_path
            : join(this.sourceDir, methodRow.file_path);
        // Read the body — extract via brace-depth scan over the source file
        const fileText = readFileSync(filePath, 'utf8');
        const fileLines = fileText.split('\n');
        const bodyLines = fileLines.slice(methodRow.start_line - 1, methodRow.end_line);
        const body = bodyLines.join('\n');
        // Calls — use the call graph (CodeGraph's strength)
        const calls = await this.callees(qualifiedName);
        return {
            qualifiedName,
            signature: methodRow.signature ?? '',
            body,
            filePath,
            startLine: methodRow.start_line,
            endLine: methodRow.end_line,
            language: methodRow.language,
            calls,
            emits: extractEmits(body),
            dbWrites: extractDbWrites(body),
            externalCalls: extractExternalCalls(body),
            throws: extractThrows(body),
            asyncBoundaries: extractAsyncBoundaries(body),
            branchPoints: countBranchPoints(body),
            bodyTextSliced: sliceBody(body),
        };
    }
    // ────────────────────────────────────────────────────────────────────
    // Internals
    // ────────────────────────────────────────────────────────────────────
    /** Run a SQL query; expect 0 or 1 row. Throws if more. */
    queryOne(sql) {
        const rows = this.queryAll(sql);
        if (rows.length === 0)
            return {};
        return rows[0];
    }
    /** Run a SQL query; return all rows as objects (parsed from JSON). */
    queryAll(sql) {
        const stdout = execFileSync(this.sqlite3Path, ['-json', this.dbPath, sql], {
            encoding: 'utf8',
            timeout: 30_000,
            maxBuffer: 50 * 1024 * 1024, // 50MB — large repos can produce big result sets
        });
        if (!stdout.trim())
            return [];
        try {
            return JSON.parse(stdout);
        }
        catch (err) {
            throw new Error(`CodeGraph backend: failed to parse sqlite3 -json output: ${err}\n--- stdout ---\n${stdout.slice(0, 500)}`);
        }
    }
    rowToSymbol(row) {
        let decorators;
        if (row.decorators) {
            try {
                decorators = JSON.parse(row.decorators);
            }
            catch {
                // Ignore malformed decorator JSON
            }
        }
        const filePath = row.file_path.startsWith('/')
            ? row.file_path
            : join(this.sourceDir, row.file_path);
        return {
            kind: mapKind(row.kind) ?? 'function',
            name: row.name,
            qualifiedName: row.qualified_name,
            filePath,
            startLine: row.start_line,
            endLine: row.end_line,
            signature: row.signature ?? undefined,
            docstring: row.docstring ?? undefined,
            isExported: !!row.is_exported,
            decorators,
            language: row.language,
        };
    }
    escape(s) {
        // Single-quote escaping for SQL string literals.
        return s.replace(/'/g, "''");
    }
    detectBinary(name) {
        try {
            const path = execSync(`which ${name}`, { encoding: 'utf8', timeout: 3000 }).trim();
            if (path)
                return path;
        }
        catch {
            // Fall through
        }
        return name; // Hope it's on PATH
    }
}
//# sourceMappingURL=codegraph.js.map