import path from 'node:path';
import { readFile } from 'node:fs/promises';
import { sliceUtf8Span } from './source-span.js';
function isSourceNode(node) {
    return node.type !== 'file' && node.type !== 'module' && node.external !== true;
}
export function candidateFromNode(node) {
    return {
        nodeId: node.id,
        name: node.name,
        type: node.type,
        path: node.path,
        relativePath: node.relativePath,
        line: node.line,
        endLine: node.endLine,
        span: node.span,
        containerName: node.containerName,
    };
}
/**
 * Confine a path to the repository, or say why it does not belong.
 *
 * Returns a message rather than throwing, because every caller — source reads,
 * entity queries, the MCP root guard — treats an escape as a typed refusal to
 * answer rather than as an exception.
 */
export function resolveWithinRepo(repoRoot, targetPath) {
    const root = path.resolve(repoRoot);
    const resolved = path.isAbsolute(targetPath) ? path.resolve(targetPath) : path.resolve(root, targetPath);
    const relativePath = path.relative(root, resolved);
    if (relativePath === '' || relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
        return `Resolved path "${targetPath}" is outside repo root "${root}".`;
    }
    return {
        path: resolved,
        relativePath: relativePath.split(path.sep).join('/'),
    };
}
function findParsedFile(index, node) {
    const resolvedNodePath = path.resolve(node.path);
    return index.files.find(file => path.resolve(file.path) === resolvedNodePath);
}
async function readSourceSnapshot(file, filePath, needsRawSource) {
    if (file?.content != null) {
        const contentLines = file.content.split(/\r?\n/);
        return {
            lines: needsRawSource ? contentLines : (file.lines ?? contentLines),
            raw: file.content,
        };
    }
    if (file?.lines && !needsRawSource)
        return { lines: file.lines };
    // An exact result and its frozen line-slice source must describe one read of
    // the file. Reading separately permits a concurrent edit to create a tuple
    // whose bodyHash and exactSpanHash came from different snapshots.
    const raw = await readFile(filePath, 'utf8');
    return { lines: raw.split(/\r?\n/), raw };
}
/**
 * Candidate matching shared with `hashCodeSymbols` (src/code/hash.ts): returns
 * the matching source nodes, or an error message when the query's file path
 * escapes the repo root. Exported so key-based resolution cannot diverge from
 * `readCodeSource`'s.
 */
export function selectByQuery(index, query) {
    if ('nodeId' in query) {
        // Stable-layout JSON maps relativize the checkout path inside node ids so
        // the document is relocatable; accept that repo-relative form as an alias
        // by re-anchoring it to this index's root (the same aliasing role
        // `legacyNodeId` already plays for format migrations).
        const separator = index.repoRoot.includes('\\') ? '\\' : '/';
        const reanchored = query.nodeId.replace(/^(node\d+:[^:]+:)(?![\\/])/, `$1${index.repoRoot}${separator}`);
        return index.nodes.filter(node => (node.id === query.nodeId ||
            node.legacyNodeId === query.nodeId ||
            node.id === reanchored ||
            node.legacyNodeId === reanchored) &&
            isSourceNode(node));
    }
    let resolvedFile;
    if (query.file) {
        const resolved = resolveWithinRepo(index.repoRoot, query.file);
        if (typeof resolved === 'string')
            return resolved;
        resolvedFile = resolved.path;
    }
    return index.nodes.filter(node => {
        if (!isSourceNode(node))
            return false;
        if (node.name !== query.name)
            return false;
        if (query.type && node.type !== query.type)
            return false;
        return !resolvedFile || path.resolve(node.path) === resolvedFile;
    });
}
export async function readCodeSource(index, query, _options = {}) {
    const selected = selectByQuery(index, query);
    if (typeof selected === 'string') {
        return { status: 'error', message: selected };
    }
    if (selected.length === 0) {
        return { status: 'not_found', candidates: [], message: 'No matching source node was found.' };
    }
    if (selected.length > 1) {
        return {
            status: 'ambiguous',
            candidates: selected.map(candidateFromNode),
            message: `Found ${selected.length} matching source nodes.`,
        };
    }
    const node = selected[0];
    if (node.line == null) {
        return { status: 'error', message: `Node "${node.id}" does not have a source line.` };
    }
    const resolved = resolveWithinRepo(index.repoRoot, node.path);
    if (typeof resolved === 'string') {
        return { status: 'error', message: resolved };
    }
    const endLine = node.endLine ?? node.line;
    const file = findParsedFile(index, node);
    const needsExactSource = node.span?.exact === true && node.span.startByte != null && node.span.endByte != null;
    const snapshot = await readSourceSnapshot(file, resolved.path, needsExactSource);
    const lines = snapshot.lines;
    const source = lines.slice(node.line - 1, endLine).join('\n');
    const exactSource = needsExactSource && snapshot.raw != null ? sliceUtf8Span(snapshot.raw, node.span) : undefined;
    return {
        status: 'resolved',
        nodeId: node.id,
        name: node.name,
        type: node.type,
        path: resolved.path,
        relativePath: node.relativePath ?? resolved.relativePath,
        line: node.line,
        endLine,
        span: node.span,
        containerName: node.containerName,
        source,
        exactSource,
    };
}
//# sourceMappingURL=source.js.map