import path from 'node:path';
import { normalizePath } from './languages.js';
import { incomingEdges, moduleForRelativePath, nodeById } from './query.js';
import { renderInlineSourceBlock } from './source-inline.js';
function isExported(index, node) {
    const file = index.files.find(candidate => candidate.path === node.path);
    return (file?.symbols.some(symbol => symbol.exported &&
        symbol.name === node.name &&
        symbol.type === node.type &&
        symbol.line === (node.line ?? symbol.line)) ?? false);
}
function crossesModuleBoundary(index, node) {
    const relativeByAbsolute = new Map(index.files.map(file => [file.path, file.relativePath]));
    const targetRelative = relativeByAbsolute.get(node.path);
    if (!targetRelative)
        return false;
    const targetModule = moduleForRelativePath(targetRelative);
    return index.edges.some(edge => {
        if (edge.type !== 'imports' || edge.status !== 'resolved' || edge.toPath !== node.path)
            return false;
        const fromRelative = relativeByAbsolute.get(edge.fromPath);
        return fromRelative !== undefined && moduleForRelativePath(fromRelative) !== targetModule;
    });
}
function graphNote(index, node) {
    const importedBy = incomingEdges(index, 'imports', node.id).filter(edge => edge.status === 'resolved').length;
    if (importedBy > 0)
        return `imported-by ${importedBy}`;
    const calledBy = incomingEdges(index, 'calls', node.id).filter(edge => edge.status === 'resolved').length;
    if (calledBy > 0)
        return `called-by ${calledBy}`;
    if (isExported(index, node) && crossesModuleBoundary(index, node))
        return 'exported seam';
    return undefined;
}
function citationCoverage(index) {
    const diagnostics = [];
    if (index.truncated) {
        diagnostics.push(`${index.truncated.droppedFiles} discovered file(s) were not indexed, so absence claims are incomplete.`);
    }
    if (index.skippedFiles.length > 0) {
        diagnostics.push(`${index.skippedFiles.length} file(s) were skipped by parser guardrails.`);
    }
    return {
        coverage: diagnostics.length === 0 ? 'complete' : 'partial',
        diagnostics,
    };
}
function location(index, node) {
    const startLine = node.line ?? 1;
    return {
        relativePath: node.relativePath ?? normalizePath(path.relative(index.repoRoot, node.path)),
        startLine,
        endLine: Math.max(startLine, node.endLine ?? startLine),
    };
}
export function citationsForName(index, query, results) {
    const citations = results.map(result => ({
        ...location(index, result.node),
        kind: result.node.type,
        name: result.node.name,
        note: graphNote(index, result.node) ?? 'matched-name',
        ...(result.inlineContext ? { inlineContext: result.inlineContext } : {}),
        ...(result.inlineContextDiagnostic ? { inlineContextDiagnostic: result.inlineContextDiagnostic } : {}),
    }));
    return {
        query,
        citations,
        ...citationCoverage(index),
        identifierOnly: citations.length > 0 && citations.every(citation => citation.note === 'matched-name'),
    };
}
export function citationsForFused(index, query, results) {
    const nodes = nodeById(index);
    const citations = results.map((result, position) => {
        const node = nodes.get(result.nodeId);
        const startLine = result.line ?? node?.line ?? 1;
        const exactNameOnly = result.sources.length > 0 && result.sources.every(source => source.method === 'name');
        return {
            relativePath: result.relativePath ??
                node?.relativePath ??
                normalizePath(path.relative(index.repoRoot, result.path ?? node?.path ?? index.repoRoot)),
            startLine,
            endLine: Math.max(startLine, result.endLine ?? node?.endLine ?? startLine),
            kind: result.type,
            name: result.name,
            note: (node ? graphNote(index, node) : undefined) ??
                (exactNameOnly ? 'matched-name' : `rank #${position + 1}`),
            ...(result.inlineContext ? { inlineContext: result.inlineContext } : {}),
            ...(result.inlineContextDiagnostic ? { inlineContextDiagnostic: result.inlineContextDiagnostic } : {}),
        };
    });
    return {
        query,
        citations,
        ...citationCoverage(index),
        identifierOnly: citations.length > 0 && citations.every(citation => citation.note === 'matched-name'),
    };
}
function oneLine(value) {
    return value.replace(/\s+/gu, ' ').trim();
}
/** Plain, score-free citation output for agents and shell composition. */
export function formatCodeCitations(result) {
    const lines = [];
    for (const citation of result.citations) {
        lines.push(`${citation.relativePath}:${citation.startLine}-${citation.endLine} ${citation.kind} ${oneLine(citation.name)} (${citation.note})`);
        if (citation.inlineContext)
            lines.push(renderInlineSourceBlock(citation.inlineContext));
        if (citation.inlineContextDiagnostic) {
            lines.push(`note: [${citation.inlineContextDiagnostic.code}] ${citation.inlineContextDiagnostic.message}`);
        }
    }
    if (result.identifierOnly)
        lines.push('identifier-only query: grep would answer this');
    for (const diagnostic of result.diagnostics)
        lines.push(`note: ${diagnostic}`);
    const absence = result.citations.length === 0 ? `; no indexed matches for "${oneLine(result.query)}"` : '';
    lines.push(`${result.citations.length} citation(s); coverage=${result.coverage}${absence}`);
    return `${lines.join('\n')}\n`;
}
//# sourceMappingURL=citations.js.map