/**
 * Per-file focus scoring for query- and path-focused repo maps.
 *
 * `computeFocusScores` produces an additive relevance score per file — keyed by
 * ABSOLUTE path so it shares the key space of `ParsedFile.path` and
 * `RankedFile.path` — from three signals:
 *   - a free-text `query` matched against paths, symbol names, signatures,
 *     jsdoc, and (capped) excerpt/content,
 *   - explicit focus `paths` (exact file or directory prefix), and
 *   - graph neighbors of the path seeds, walked over the import/call/inherit
 *     graph up to `graphDepth`.
 *
 * It makes no ranking or budgeting decisions of its own — `buildRepoMap` blends
 * the score with the global structural rank to decide admission order. Kept
 * dependency-free (types + `normalizePath` + `node:path`) so it stays trivially
 * testable and within the `code` module boundary.
 */
import path from 'node:path';
import { normalizePath } from './languages.js';
/** Same relationship edges that `rank.ts` ranks over — keeps the two consistent. */
const RELATIONSHIP_EDGE_TYPES = new Set(['imports', 'calls', 'uses', 'inherits', 'implements']);
// Scoring weights. Bounded per file so no single huge or barrel file runs away.
const W_NAME_EXACT = 4; // exported symbol whose name equals a query term — the file IS the target
const W_NAME_SUBSTRING = 2; // symbol name contains a term
const W_PATH_SEGMENT = 3; // a path segment contains a term
const W_SIGNATURE = 1; // any signature/fullSignature contains a term
const W_JSDOC = 1; // any jsdoc contains a term
const W_CONTENT = 0.5; // term present in the (capped) excerpt/content
const NAME_EXACT_CAP = 8; // per-term cap on exact-name bonus
const NAME_SUBSTRING_CAP = 5; // per-term cap on counted substring symbols
const W_PATH_EXACT = 10; // relativePath equals a focus path
const W_PATH_PREFIX = 6; // file lives under a focus directory
const CONTENT_SCAN_CHARS = 4_000; // never scan more than this much content per file
function posix(value) {
    return normalizePath(value);
}
/** Compute per-file focus relevance, keyed by absolute file path. */
export function computeFocusScores(index, focus) {
    const result = new Map();
    const repoRoot = index.repoRoot;
    const add = (absPath, score, reasons = []) => {
        let entry = result.get(absPath);
        if (!entry) {
            entry = { score: 0, reasons: [] };
            result.set(absPath, entry);
        }
        entry.score += score;
        for (const reason of reasons) {
            if (!entry.reasons.includes(reason))
                entry.reasons.push(reason);
        }
    };
    const relByPath = new Map();
    for (const file of index.files)
        relByPath.set(file.path, posix(file.relativePath));
    // ── Query signal ──────────────────────────────────────────────────────────
    const terms = (focus.query ?? '')
        .toLowerCase()
        .split(/\s+/)
        .map(term => term.trim())
        .filter(Boolean);
    const contentAvailable = index.contentMode !== 'none';
    if (terms.length > 0) {
        for (const file of index.files) {
            let fileScore = 0;
            const reasons = [];
            const segments = posix(file.relativePath).toLowerCase().split(/[/.]+/).filter(Boolean);
            const haystack = contentAvailable
                ? (file.excerpt?.content ?? file.content ?? '').slice(0, CONTENT_SCAN_CHARS).toLowerCase()
                : '';
            // Iterate the symbols once, lowercasing each symbol's name/signature/jsdoc
            // a single time and testing every term against it, instead of re-scanning
            // (and re-lowercasing) the full symbol list once per term. Per-term tallies
            // are folded back into the score/reasons afterwards, preserving order.
            const exactBonus = new Array(terms.length).fill(0);
            const substringHits = new Array(terms.length).fill(0);
            const sawSignature = new Array(terms.length).fill(false);
            const sawJsdoc = new Array(terms.length).fill(false);
            for (const symbol of file.symbols) {
                const name = symbol.name.toLowerCase();
                const sig = (symbol.fullSignature ?? symbol.signature ?? '').toLowerCase();
                const jsdoc = symbol.jsdoc != null ? symbol.jsdoc.toLowerCase() : undefined;
                for (let t = 0; t < terms.length; t++) {
                    const term = terms[t];
                    if (name === term) {
                        exactBonus[t] = Math.min(NAME_EXACT_CAP, exactBonus[t] + W_NAME_EXACT);
                    }
                    else if (name.includes(term) && substringHits[t] < NAME_SUBSTRING_CAP) {
                        substringHits[t]++;
                    }
                    if (!sawSignature[t] && sig.includes(term))
                        sawSignature[t] = true;
                    if (!sawJsdoc[t] && jsdoc != null && jsdoc.includes(term))
                        sawJsdoc[t] = true;
                }
            }
            for (let t = 0; t < terms.length; t++) {
                const term = terms[t];
                if (segments.some(segment => segment.includes(term))) {
                    fileScore += W_PATH_SEGMENT;
                    reasons.push(`path mentions "${term}"`);
                }
                if (exactBonus[t] > 0) {
                    fileScore += exactBonus[t];
                    reasons.push(`defines "${term}"`);
                }
                if (substringHits[t] > 0) {
                    fileScore += substringHits[t] * W_NAME_SUBSTRING;
                    reasons.push(`symbol matches "${term}"`);
                }
                if (sawSignature[t])
                    fileScore += W_SIGNATURE;
                if (sawJsdoc[t])
                    fileScore += W_JSDOC;
                if (haystack.includes(term))
                    fileScore += W_CONTENT;
            }
            if (fileScore > 0)
                add(file.path, fileScore, reasons);
        }
    }
    // ── Path signal + graph neighbors ─────────────────────────────────────────
    const rawPaths = (focus.paths ?? []).map(value => value.trim()).filter(Boolean);
    if (rawPaths.length === 0)
        return result;
    // Normalize each focus path to a repo-relative, forward-slash form (no trailing slash).
    const focusRels = rawPaths.map(raw => {
        const rel = path.isAbsolute(raw) ? path.relative(repoRoot, raw) : path.normalize(raw);
        return posix(rel).replace(/\/+$/, '');
    });
    const seeds = new Set();
    for (const file of index.files) {
        const fileRel = posix(file.relativePath);
        for (let i = 0; i < focusRels.length; i++) {
            const prefix = focusRels[i];
            const original = rawPaths[i];
            if (prefix === '' || prefix === '.') {
                add(file.path, W_PATH_PREFIX, [`under focus path: ${original}`]);
                seeds.add(file.path);
            }
            else if (fileRel === prefix) {
                add(file.path, W_PATH_EXACT, [`path match: ${original}`]);
                seeds.add(file.path);
            }
            else if (fileRel.startsWith(`${prefix}/`)) {
                add(file.path, W_PATH_PREFIX, [`under focus path: ${original}`]);
                seeds.add(file.path);
            }
        }
    }
    const graphDepth = focus.graphDepth ?? 1;
    if (graphDepth < 1 || seeds.size === 0)
        return result;
    // Undirected file adjacency over relationship edges, restricted to indexed files.
    const known = new Set(index.files.map(file => file.path));
    const adjacency = new Map();
    const link = (a, b) => {
        if (a === b)
            return;
        let set = adjacency.get(a);
        if (!set) {
            set = new Set();
            adjacency.set(a, set);
        }
        set.add(b);
    };
    for (const edge of index.edges) {
        if (!RELATIONSHIP_EDGE_TYPES.has(edge.type))
            continue;
        const to = edge.toPath;
        if (to == null || !known.has(to) || !known.has(edge.fromPath))
            continue;
        link(edge.fromPath, to);
        link(to, edge.fromPath);
    }
    // BFS from all seeds; record the shallowest depth per file (never downgrade).
    const depthOf = new Map();
    for (const seed of seeds)
        depthOf.set(seed, 0);
    let frontier = [...seeds];
    for (let depth = 1; depth <= graphDepth && frontier.length > 0; depth++) {
        const next = [];
        for (const node of frontier) {
            const neighbors = adjacency.get(node);
            if (!neighbors)
                continue;
            const fromRel = relByPath.get(node) ?? posix(path.relative(repoRoot, node));
            for (const neighbor of neighbors) {
                if (depthOf.has(neighbor))
                    continue;
                depthOf.set(neighbor, depth);
                next.push(neighbor);
                add(neighbor, Math.max(1, 4 - depth), [`graph neighbor (depth ${depth}) of ${fromRel}`]);
            }
        }
        frontier = next;
    }
    return result;
}
//# sourceMappingURL=focus.js.map