import { execFile } from 'node:child_process';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import { promisify } from 'node:util';
import { ensureInit, extensionToLanguage, loadGrammar } from './ast-classifier.js';
import { rethrowAsParseError } from './errors.js';
const execFileAsync = promisify(execFile);
// ─── Constants ──────────────────────────────────────
const TREE_SITTER_HINT = 'Check the S-expression query syntax. If valid, the source file may contain syntax that crashes tree-sitter.';
// ─── File reading ───────────────────────────────────
/**
 * Read file content — try `git show :path` first (staged content), fall back to disk.
 * Fully async — does not block the event loop.
 */
async function readFileContent(filePath, cwd) {
    try {
        const { stdout } = await execFileAsync('git', ['show', `:${filePath}`], {
            cwd,
            encoding: 'utf-8',
            maxBuffer: 10 * 1024 * 1024, // 10MB safety cap
        });
        return stdout;
    }
    catch {
        // Fall back to disk
    }
    try {
        const fullPath = path.resolve(cwd, filePath);
        return await fs.readFile(fullPath, 'utf-8');
    }
    catch {
        return null;
    }
}
// ─── Query execution ────────────────────────────────
/**
 * Run a single S-expression query against a parsed tree.
 * Returns matches that overlap with added line numbers.
 */
function runQuery(QueryClass, grammar, rootNode, lines, astQuery, addedLineNumbers) {
    let query = null;
    try {
        query = new QueryClass(grammar, astQuery);
        const matches = query.matches(rootNode);
        const results = [];
        for (const match of matches) {
            // Find the @violation capture, or use the first capture
            let targetNode = null;
            for (const capture of match.captures) {
                if (capture.name === 'violation') {
                    targetNode = capture.node;
                    break;
                }
            }
            if (!targetNode && match.captures.length > 0) {
                targetNode = match.captures[0].node;
            }
            if (!targetNode)
                continue;
            const startLine = targetNode.startPosition.row + 1;
            const endLine = targetNode.endPosition.row + 1;
            for (let lineNum = startLine; lineNum <= endLine; lineNum++) {
                if (addedLineNumbers.has(lineNum)) {
                    results.push({
                        lineNumber: lineNum,
                        lineText: lines[lineNum - 1] ?? '',
                        // Suppression anchor — the construct's start line, which may differ
                        // from the reported (first-added) line under diff scope (#2214).
                        startLineText: lines[startLine - 1] ?? '',
                        startPrecedingLineText: startLine > 1 ? (lines[startLine - 2] ?? null) : null,
                    });
                    break;
                }
            }
        }
        return results;
    }
    catch (err) {
        /* c8 ignore next – rethrowAsParseError always throws; return satisfies TS2366 */
        return rethrowAsParseError('AST query failed', err, TREE_SITTER_HINT);
    }
    finally {
        query?.delete();
    }
}
// ─── Public API ─────────────────────────────────────
/**
 * Convenience wrapper: read + parse + query in one call.
 * For batch operations, use `matchAstQueriesBatch` instead.
 */
export async function matchAstQuery(filePath, astQuery, addedLineNumbers, cwd) {
    if (addedLineNumbers.length === 0)
        return [];
    const ext = path.extname(filePath);
    const lang = extensionToLanguage(ext);
    if (!lang)
        return [];
    const content = await readFileContent(filePath, cwd);
    if (!content)
        return [];
    try {
        await ensureInit();
        const grammar = await loadGrammar(lang);
        const TreeSitter = await import('web-tree-sitter');
        const ParserClass = TreeSitter.default?.Parser ?? TreeSitter.Parser;
        const QueryClass = TreeSitter.default?.Query ?? TreeSitter.Query;
        const parser = new ParserClass();
        try {
            parser.setLanguage(grammar);
            const tree = parser.parse(content);
            if (!tree)
                return [];
            try {
                return runQuery(QueryClass, grammar, tree.rootNode, content.split('\n'), astQuery, new Set(addedLineNumbers));
            }
            finally {
                tree.delete();
            }
        }
        finally {
            parser.delete();
        }
    }
    catch (err) {
        rethrowAsParseError('AST parse failed', err, TREE_SITTER_HINT);
    }
}
/**
 * Parse a file once and run multiple AST queries against it efficiently.
 * O(M + N) instead of O(M * N) — file is read and parsed exactly once.
 * Returns results indexed by position in the input array.
 */
export async function matchAstQueriesBatch(filePath, queries, workingDirectory, onWarn = console.warn, // totem-context: fallback ensures query failures are never silently swallowed
readStrategy) {
    if (queries.length === 0)
        return [];
    const ext = path.extname(filePath);
    const lang = extensionToLanguage(ext);
    if (!lang) {
        return queries.map(() => []);
    }
    let content = null;
    if (readStrategy) {
        content = await readStrategy(filePath);
    }
    else {
        // Default disk read fallback with path containment check.
        // Uses path.relative() instead of startsWith() to avoid sibling-directory bypass
        // (e.g., /app-secrets bypassing a /app base).
        try {
            const normalizedBase = path.resolve(workingDirectory);
            const fullPath = path.join(normalizedBase, filePath);
            const relative = path.relative(normalizedBase, fullPath);
            if (relative.startsWith('..') || path.isAbsolute(relative)) {
                content = null;
            }
            else {
                content = await fs.readFile(fullPath, 'utf-8');
            }
        }
        catch {
            content = null;
        }
    }
    if (!content) {
        return queries.map(() => []);
    }
    try {
        await ensureInit();
        const grammar = await loadGrammar(lang);
        const TreeSitter = await import('web-tree-sitter');
        const ParserClass = TreeSitter.default?.Parser ?? TreeSitter.Parser;
        const QueryClass = TreeSitter.default?.Query ?? TreeSitter.Query;
        const parser = new ParserClass();
        try {
            parser.setLanguage(grammar);
            const tree = parser.parse(content);
            if (!tree) {
                return queries.map(() => []);
            }
            const lines = content.split('\n');
            try {
                return queries.map(({ astQuery, addedLineNumbers }) => {
                    try {
                        return runQuery(QueryClass, grammar, tree.rootNode, lines, astQuery, new Set(addedLineNumbers));
                    }
                    catch (err) {
                        // Per-query degradation: TS-specific node names crash on JS files, etc.
                        // Warn (not silent) so typos in user queries are visible (#988)
                        onWarn?.(`AST query skipped for ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
                        return [];
                    }
                });
            }
            finally {
                tree.delete();
            }
        }
        finally {
            parser.delete();
        }
    }
    catch (err) {
        rethrowAsParseError('AST batch parse failed', err, TREE_SITTER_HINT);
    }
}
//# sourceMappingURL=ast-query.js.map