import path from 'node:path';
import { findCallees, findCallers, findCrossFileCallTargets, findDbPath, findFileNodes, findImplementors, findImportSources, findImportTargets, findInterfaces, findIntraFileCallEdges, findNodeChildren, findNodesByFile, getComplexityForNode, getLineCountForNode, getMaxEndLineForFile, } from '../../db/index.js';
import { cachedStmt } from '../../db/repository/cached-stmt.js';
import { debug } from '../../infrastructure/logger.js';
import { isTestFile } from '../../infrastructure/test-filter.js';
import { createFileLinesReader, extractSignature, extractSummary, isFileLikeTarget, readSourceRange, } from '../../shared/file-utils.js';
import { resolveMethodViaHierarchy } from '../../shared/hierarchy.js';
import { normalizeSymbol, toSymbolRef } from '../../shared/normalize.js';
import { paginateResult } from '../../shared/paginate.js';
import { resolveAnalysisOpts, withReadonlyDb } from './query-helpers.js';
import { findMatchingNodes } from './symbol-lookup.js';
/** Format a callee row into the output shape with summary and source. */
function formatCalleeRow(c, repoRoot, getFileLines, displayOpts, includeSource) {
    const cLines = getFileLines(c.file);
    return {
        name: c.name,
        kind: c.kind,
        file: c.file,
        line: c.line,
        endLine: c.end_line || null,
        summary: cLines ? extractSummary(cLines, c.line, displayOpts) : null,
        source: includeSource
            ? readSourceRange(repoRoot, c.file, c.line, c.end_line ?? undefined, displayOpts)
            : null,
    };
}
/** BFS to collect deeper callees beyond the first level. */
function collectDeeperCallees(db, startIds, rootId, repoRoot, getFileLines, opts) {
    const { noTests, maxDepth, displayOpts } = opts;
    const visited = new Set(startIds);
    visited.add(rootId);
    let frontier = [...startIds];
    const result = [];
    for (let d = 2; d <= maxDepth; d++) {
        const nextFrontier = [];
        for (const fid of frontier) {
            const deeper = findCallees(db, fid);
            for (const c of deeper) {
                if (visited.has(c.id) || (noTests && isTestFile(c.file)))
                    continue;
                visited.add(c.id);
                nextFrontier.push(c.id);
                result.push(formatCalleeRow(c, repoRoot, getFileLines, displayOpts, true));
            }
        }
        frontier = nextFrontier;
        if (frontier.length === 0)
            break;
    }
    return result;
}
function buildCallees(db, node, repoRoot, getFileLines, opts) {
    const { noTests, depth, displayOpts } = opts;
    const calleeRows = findCallees(db, node.id);
    const filteredCallees = noTests ? calleeRows.filter((c) => !isTestFile(c.file)) : calleeRows;
    const callees = filteredCallees.map((c) => formatCalleeRow(c, repoRoot, getFileLines, displayOpts, depth >= 1));
    if (depth > 1) {
        const deeper = collectDeeperCallees(db, filteredCallees.map((c) => c.id), node.id, repoRoot, getFileLines, { noTests, maxDepth: Math.min(depth, 5), displayOpts });
        callees.push(...deeper);
    }
    return callees;
}
function fetchCallerRows(db, node) {
    const callerRows = findCallers(db, node.id);
    if (node.kind === 'method' && node.name.includes('.')) {
        const methodName = node.name.split('.').pop() ?? '';
        const relatedMethods = resolveMethodViaHierarchy(db, methodName);
        for (const rm of relatedMethods) {
            if (rm.id === node.id)
                continue;
            const extraCallers = findCallers(db, rm.id);
            callerRows.push(...extraCallers.map((c) => ({ ...c, viaHierarchy: rm.name })));
        }
    }
    return callerRows;
}
function buildCallers(callerRows, noTests) {
    const filtered = noTests ? callerRows.filter((c) => !isTestFile(c.file)) : callerRows;
    return filtered.map((c) => ({
        name: c.name,
        kind: c.kind,
        file: c.file,
        line: c.line,
        viaHierarchy: c.viaHierarchy || undefined,
    }));
}
const INTERFACE_LIKE_KINDS = new Set(['interface', 'trait']);
const IMPLEMENTOR_KINDS = new Set(['class', 'struct', 'record', 'enum']);
function buildImplementationInfo(db, node, noTests) {
    // For interfaces/traits: show who implements them
    if (INTERFACE_LIKE_KINDS.has(node.kind)) {
        let impls = findImplementors(db, node.id);
        if (noTests)
            impls = impls.filter((n) => !isTestFile(n.file));
        return {
            implementors: impls.map(toSymbolRef),
        };
    }
    // For classes/structs: show what they implement
    if (IMPLEMENTOR_KINDS.has(node.kind)) {
        let ifaces = findInterfaces(db, node.id);
        if (noTests)
            ifaces = ifaces.filter((n) => !isTestFile(n.file));
        if (ifaces.length > 0) {
            return {
                implements: ifaces.map(toSymbolRef),
            };
        }
    }
    return {};
}
function buildRelatedTests(callerRows, getFileLines, includeTests) {
    const testCallers = callerRows.filter((c) => isTestFile(c.file));
    const testsByFile = new Map();
    for (const tc of testCallers) {
        if (!testsByFile.has(tc.file))
            testsByFile.set(tc.file, []);
        testsByFile.get(tc.file).push(tc);
    }
    const relatedTests = [];
    for (const [file] of testsByFile) {
        const tLines = getFileLines(file);
        const testNames = [];
        if (tLines) {
            for (const tl of tLines) {
                const tm = tl.match(/(?:it|test|describe)\s*\(\s*['"`]([^'"`]+)['"`]/);
                if (tm)
                    testNames.push(tm[1]);
            }
        }
        const testSource = includeTests && tLines ? tLines.join('\n') : undefined;
        relatedTests.push({
            file,
            testCount: testNames.length,
            testNames,
            source: testSource,
        });
    }
    return relatedTests;
}
function getComplexityMetrics(db, nodeId) {
    try {
        const cRow = getComplexityForNode(db, nodeId);
        if (!cRow)
            return null;
        return {
            cognitive: cRow.cognitive,
            cyclomatic: cRow.cyclomatic,
            maxNesting: cRow.max_nesting,
            maintainabilityIndex: cRow.maintainability_index || 0,
            halsteadVolume: cRow.halstead_volume || 0,
        };
    }
    catch (e) {
        debug(`complexity lookup failed for node ${nodeId}: ${e.message}`);
        return null;
    }
}
function getNodeChildrenSafe(db, nodeId) {
    try {
        return findNodeChildren(db, nodeId).map((c) => ({
            name: c.name,
            kind: c.kind,
            line: c.line,
            endLine: c.end_line || null,
        }));
    }
    catch (e) {
        debug(`findNodeChildren failed for node ${nodeId}: ${e.message}`);
        return [];
    }
}
function buildIntraFileDataFlow(db, file) {
    const intraEdges = findIntraFileCallEdges(db, file);
    const dataFlowMap = new Map();
    for (const edge of intraEdges) {
        if (!dataFlowMap.has(edge.caller_name))
            dataFlowMap.set(edge.caller_name, []);
        dataFlowMap.get(edge.caller_name).push(edge.callee_name);
    }
    return [...dataFlowMap.entries()].map(([caller, callees]) => ({
        caller,
        callees,
    }));
}
function explainFileImpl(db, target, getFileLines, displayOpts) {
    const fileNodes = findFileNodes(db, `%${target}%`);
    if (fileNodes.length === 0)
        return [];
    return fileNodes.map((fn) => {
        const symbols = findNodesByFile(db, fn.file);
        // IDs of symbols that have incoming calls from other files (public)
        const publicIds = findCrossFileCallTargets(db, fn.file);
        const fileLines = getFileLines(fn.file);
        const mapSymbol = (s) => ({
            name: s.name,
            kind: s.kind,
            line: s.line,
            role: s.role || null,
            summary: fileLines ? extractSummary(fileLines, s.line, displayOpts) : null,
            signature: fileLines ? extractSignature(fileLines, s.line, displayOpts) : null,
        });
        const publicApi = symbols.filter((s) => publicIds.has(s.id)).map(mapSymbol);
        const internal = symbols.filter((s) => !publicIds.has(s.id)).map(mapSymbol);
        const imports = findImportTargets(db, fn.id).map((r) => ({
            file: r.file,
        }));
        const importedBy = findImportSources(db, fn.id).map((r) => ({
            file: r.file,
        }));
        const dataFlow = buildIntraFileDataFlow(db, fn.file);
        const metric = getLineCountForNode(db, fn.id);
        let lineCount = metric?.line_count || null;
        if (!lineCount) {
            const maxLine = getMaxEndLineForFile(db, fn.file);
            lineCount = maxLine?.max_end || null;
        }
        return {
            file: fn.file,
            lineCount,
            symbolCount: symbols.length,
            publicApi,
            internal,
            imports,
            importedBy,
            dataFlow,
        };
    });
}
const _explainNodeStmtCache = new WeakMap();
const _EXPLAIN_NODE_SQL = `SELECT * FROM nodes WHERE name LIKE ? AND kind IN ('function','method','class','interface','type','struct','enum','trait','record','module','constant') ORDER BY file, line`;
function explainFunctionImpl(db, target, noTests, getFileLines, displayOpts) {
    const stmt = cachedStmt(_explainNodeStmtCache, db, _EXPLAIN_NODE_SQL);
    let nodes = stmt.all(`%${target}%`);
    if (noTests)
        nodes = nodes.filter((n) => !isTestFile(n.file));
    if (nodes.length === 0)
        return [];
    const hc = new Map();
    return nodes.slice(0, 10).map((node) => {
        const fileLines = getFileLines(node.file);
        const lineCount = node.end_line ? node.end_line - node.line + 1 : null;
        const summary = fileLines ? extractSummary(fileLines, node.line, displayOpts) : null;
        const signature = fileLines ? extractSignature(fileLines, node.line, displayOpts) : null;
        const callees = findCallees(db, node.id).map(toSymbolRef);
        const allCallerRows = findCallers(db, node.id);
        let callers = allCallerRows.map(toSymbolRef);
        if (noTests)
            callers = callers.filter((c) => !isTestFile(c.file));
        const seenFiles = new Set();
        const relatedTests = allCallerRows
            .filter((r) => isTestFile(r.file) && !seenFiles.has(r.file) && seenFiles.add(r.file))
            .map((r) => ({ file: r.file }));
        return {
            ...normalizeSymbol(node, db, hc),
            lineCount,
            summary,
            signature,
            complexity: getComplexityMetrics(db, node.id),
            callees,
            callers,
            relatedTests,
        };
    });
}
function explainCallees(parentResults, currentDepth, visited, db, noTests, getFileLines, displayOpts) {
    if (currentDepth <= 0)
        return;
    for (const r of parentResults) {
        const newCallees = [];
        for (const callee of r.callees) {
            const key = `${callee.name}:${callee.file}:${callee.line}`;
            if (visited.has(key))
                continue;
            visited.add(key);
            const calleeResults = explainFunctionImpl(db, callee.name, noTests, getFileLines, displayOpts);
            const exact = calleeResults.find((cr) => cr.file === callee.file && cr.line === callee.line);
            if (exact) {
                exact._depth =
                    (r._depth || 0) + 1;
                newCallees.push(exact);
            }
        }
        if (newCallees.length > 0) {
            r.depDetails = newCallees;
            explainCallees(newCallees, currentDepth - 1, visited, db, noTests, getFileLines, displayOpts);
        }
    }
}
// --- Exported functions ---
export function contextData(name, customDbPath, opts = {}) {
    return withReadonlyDb(customDbPath, (db) => {
        const depth = opts.depth || 0;
        const noSource = opts.noSource || false;
        const includeTests = opts.includeTests || false;
        const { noTests, displayOpts } = resolveAnalysisOpts(opts);
        const dbPath = findDbPath(customDbPath);
        const repoRoot = path.resolve(path.dirname(dbPath), '..');
        const nodes = findMatchingNodes(db, name, { noTests, file: opts.file, kind: opts.kind });
        if (nodes.length === 0) {
            return { name, results: [] };
        }
        const getFileLines = createFileLinesReader(repoRoot);
        const results = nodes.map((node) => {
            const fileLines = getFileLines(node.file);
            const source = noSource
                ? null
                : readSourceRange(repoRoot, node.file, node.line, node.end_line ?? undefined, displayOpts);
            const signature = fileLines ? extractSignature(fileLines, node.line, displayOpts) : null;
            const callees = buildCallees(db, node, repoRoot, getFileLines, {
                noTests,
                depth,
                displayOpts,
            });
            const allCallerRows = fetchCallerRows(db, node);
            const callers = buildCallers(allCallerRows, noTests);
            const relatedTests = buildRelatedTests(allCallerRows, getFileLines, includeTests);
            const complexityMetrics = getComplexityMetrics(db, node.id);
            const nodeChildren = getNodeChildrenSafe(db, node.id);
            const implInfo = buildImplementationInfo(db, node, noTests);
            return {
                name: node.name,
                kind: node.kind,
                file: node.file,
                line: node.line,
                role: node.role || null,
                endLine: node.end_line || null,
                source,
                signature,
                complexity: complexityMetrics,
                children: nodeChildren.length > 0 ? nodeChildren : undefined,
                callees,
                callers,
                relatedTests,
                ...implInfo,
            };
        });
        const base = { name, results };
        return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
    });
}
export function explainData(target, customDbPath, opts = {}) {
    return withReadonlyDb(customDbPath, (db) => {
        const depth = opts.depth || 0;
        const kind = isFileLikeTarget(target) ? 'file' : 'function';
        const { noTests, displayOpts } = resolveAnalysisOpts(opts);
        const dbPath = findDbPath(customDbPath);
        const repoRoot = path.resolve(path.dirname(dbPath), '..');
        const getFileLines = createFileLinesReader(repoRoot);
        const results = kind === 'file'
            ? explainFileImpl(db, target, getFileLines, displayOpts)
            : explainFunctionImpl(db, target, noTests, getFileLines, displayOpts);
        if (kind === 'function' && depth > 0 && results.length > 0) {
            const visited = new Set(results.map((r) => `${r.name}:${r.file}:${r.line ?? ''}`));
            explainCallees(results, depth, visited, db, noTests, getFileLines, displayOpts);
        }
        const base = { target, kind, results };
        return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
    });
}
//# sourceMappingURL=context.js.map