import { Repository, SqliteRepository } from '../../db/index.js';
import { isTestFile } from '../../infrastructure/test-filter.js';
import { normalizeSymbol, toSymbolRef } from '../../shared/normalize.js';
import { paginateResult } from '../../shared/paginate.js';
import { resolveAnalysisOpts, withRepo } from './query-helpers.js';
import { findMatchingNodes } from './symbol-lookup.js';
/** Cache so repeated raw-db calls reuse the same SqliteRepository (preserves per-instance memoization). */
const repoCache = new WeakMap();
/** Coerce a raw db handle or Repository into a Repository instance. */
function toRepo(dbOrRepo) {
    if (dbOrRepo instanceof Repository)
        return dbOrRepo;
    const db = dbOrRepo;
    let repo = repoCache.get(db);
    if (!repo) {
        repo = new SqliteRepository(db);
        repoCache.set(db, repo);
    }
    return repo;
}
// --- Shared BFS: transitive callers ---
const INTERFACE_LIKE_KINDS = new Set(['interface', 'trait']);
/** Record an implementor node at the given depth, adding to frontier and levels. */
function recordImplementor(impl, parentId, depth, visited, frontier, levels, noTests, onVisit) {
    if (visited.has(impl.id) || (noTests && isTestFile(impl.file)))
        return;
    visited.add(impl.id);
    frontier.push(impl.id);
    if (!levels[depth])
        levels[depth] = [];
    levels[depth].push({
        name: impl.name,
        kind: impl.kind,
        file: impl.file,
        line: impl.line,
        viaImplements: true,
    });
    if (onVisit)
        onVisit({ ...impl, viaImplements: true }, parentId, depth);
}
/** Expand implementors for an interface/trait node into the BFS frontier. */
function expandImplementors(repo, nodeId, depth, visited, frontier, levels, noTests, onVisit) {
    const impls = repo.findImplementors(nodeId);
    for (const impl of impls) {
        recordImplementor(impl, nodeId, depth, visited, frontier, levels, noTests, onVisit);
    }
}
/** Record a caller node at depth `d`, adding to frontier and levels. */
function recordCaller(caller, parentId, depth, visited, nextFrontier, levels, noTests, onVisit) {
    if (visited.has(caller.id) || (noTests && isTestFile(caller.file)))
        return;
    visited.add(caller.id);
    nextFrontier.push(caller.id);
    if (!levels[depth])
        levels[depth] = [];
    levels[depth].push(toSymbolRef(caller));
    if (onVisit)
        onVisit(caller, parentId, depth);
}
/** Process all callers of one frontier node, recording new nodes and expanding implementors. */
function processFrontierNode(repo, fid, depth, visited, nextFrontier, levels, noTests, resolveImplementors, onVisit) {
    const callers = repo.findDistinctCallers(fid);
    for (const c of callers) {
        recordCaller(c, fid, depth, visited, nextFrontier, levels, noTests, onVisit);
        if (resolveImplementors && INTERFACE_LIKE_KINDS.has(c.kind)) {
            expandImplementors(repo, c.id, depth + 1, visited, nextFrontier, levels, noTests, onVisit);
        }
    }
}
/** Seed BFS with implementors of the start node when it is an interface/trait. */
function seedInterfaceImplementors(repo, startId, visited, levels, noTests, onVisit) {
    const implNextFrontier = [];
    const startNode = repo.findNodeById(startId);
    if (startNode && INTERFACE_LIKE_KINDS.has(startNode.kind)) {
        expandImplementors(repo, startId, 1, visited, implNextFrontier, levels, noTests, onVisit);
    }
    return implNextFrontier;
}
export function bfsTransitiveCallers(dbOrRepo, startId, { noTests = false, maxDepth = 3, includeImplementors = true, onVisit, } = {}) {
    const repo = toRepo(dbOrRepo);
    const resolveImplementors = includeImplementors && repo.hasImplementsEdges();
    const visited = new Set([startId]);
    const levels = {};
    let frontier = [startId];
    // Seed: if start node is an interface/trait, include its implementors at depth 1
    const implNextFrontier = resolveImplementors
        ? seedInterfaceImplementors(repo, startId, visited, levels, noTests, onVisit)
        : [];
    for (let d = 1; d <= maxDepth; d++) {
        if (d === 1 && implNextFrontier.length > 0) {
            frontier = [...frontier, ...implNextFrontier];
        }
        const nextFrontier = [];
        for (const fid of frontier) {
            processFrontierNode(repo, fid, d, visited, nextFrontier, levels, noTests, resolveImplementors, onVisit);
        }
        frontier = nextFrontier;
        if (frontier.length === 0)
            break;
    }
    return { totalDependents: visited.size - 1, levels };
}
/** BFS over import dependents, returning visited node IDs and depth-per-id map. */
function bfsImportDependents(repo, seedNodes, noTests) {
    const visited = new Set();
    const queue = [];
    const levels = new Map();
    for (const fn of seedNodes) {
        visited.add(fn.id);
        queue.push(fn.id);
        levels.set(fn.id, 0);
    }
    while (queue.length > 0) {
        const current = queue.shift();
        const level = levels.get(current);
        const dependents = repo.findImportDependents(current);
        for (const dep of dependents) {
            if (visited.has(dep.id))
                continue;
            if (noTests && isTestFile(dep.file))
                continue;
            visited.add(dep.id);
            queue.push(dep.id);
            levels.set(dep.id, level + 1);
        }
    }
    return { visited, levels };
}
/** Group visited dependents by depth (excluding seed depth 0). */
function groupDependentsByLevel(repo, levels) {
    const byLevel = {};
    for (const [id, level] of levels) {
        if (level === 0)
            continue;
        if (!byLevel[level])
            byLevel[level] = [];
        const node = repo.findNodeById(id);
        if (node)
            byLevel[level].push({ file: node.file });
    }
    return byLevel;
}
export function impactAnalysisData(file, customDbPath, opts = {}) {
    return withRepo(customDbPath, (repo) => {
        const noTests = opts.noTests || false;
        const fileNodes = repo.findFileNodes(`%${file}%`);
        if (fileNodes.length === 0) {
            return { file, sources: [], levels: {}, totalDependents: 0 };
        }
        const { visited, levels } = bfsImportDependents(repo, fileNodes, noTests);
        const byLevel = groupDependentsByLevel(repo, levels);
        return {
            file,
            sources: fileNodes.map((f) => f.file),
            levels: byLevel,
            totalDependents: visited.size - fileNodes.length,
        };
    });
}
export function fnImpactData(name, customDbPath, opts = {}) {
    return withRepo(customDbPath, (repo) => {
        const { noTests, config } = resolveAnalysisOpts(opts);
        const maxDepth = opts.depth || config.analysis?.fnImpactDepth || 5;
        const hc = new Map();
        const nodes = findMatchingNodes(repo, name, { noTests, file: opts.file, kind: opts.kind });
        if (nodes.length === 0) {
            return { name, results: [] };
        }
        const includeImplementors = opts.includeImplementors !== false;
        const results = nodes.map((node) => {
            const { levels, totalDependents } = bfsTransitiveCallers(repo, node.id, {
                noTests,
                maxDepth,
                includeImplementors,
            });
            return {
                ...normalizeSymbol(node, repo, hc),
                levels,
                totalDependents,
            };
        });
        const base = { name, results };
        return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
    });
}
//# sourceMappingURL=fn-impact.js.map