import { createHash } from 'node:crypto';
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import { abortIfNeeded } from '../utils.js';
import { normalizePath } from './languages.js';
export { SourceIdentitySchema, SourceInlineBlockSchema, SourceInlineDiagnosticSchema, SourceInlineProjectionSchema, SourceInlineRangeSchema, } from './source-inline-contract.js';
const manifests = new WeakMap();
const diskLines = new WeakMap();
export function sourceIdentityForBytes(bytes) {
    return {
        size: bytes.byteLength,
        sha256: createHash('sha256').update(bytes).digest('hex'),
    };
}
/** Bind disk-readable source to the exact byte identity used to build this index. */
export function attachSourceInlineManifest(index, entries) {
    manifests.set(index, new Map(entries.map(entry => [normalizePath(entry.relativePath), { ...entry }])));
    // A refreshed manifest is a new verification snapshot. Do not retain an
    // earlier read (or an earlier refusal) against it.
    diskLines.delete(index);
}
export function getSourceInlineManifestEntry(index, relativePath) {
    return manifests.get(index)?.get(normalizePath(relativePath));
}
function stale(relativePath, reason) {
    return {
        code: 'inline-context-stale',
        message: `Inline context for "${relativePath}" was omitted: ${reason}`,
        relativePath,
    };
}
export function inlineSourceFromLines(relativePath, lines, start, end, context) {
    const normalized = normalizePath(relativePath);
    const focusStartLine = Math.max(1, Math.trunc(start));
    const focusEndLine = Math.max(focusStartLine, Math.trunc(end));
    const contextLines = Math.max(0, Math.trunc(context));
    if (focusStartLine > lines.length) {
        return {
            diagnostic: stale(normalized, `the indexed start line ${focusStartLine} is beyond the verified ${lines.length}-line source`),
        };
    }
    const boundedFocusEnd = Math.min(lines.length, focusEndLine);
    const startLine = Math.max(1, focusStartLine - contextLines);
    const endLine = Math.min(lines.length, boundedFocusEnd + contextLines);
    return {
        block: {
            relativePath: normalized,
            startLine,
            endLine,
            focusStartLine,
            focusEndLine: boundedFocusEnd,
            source: lines.slice(startLine - 1, endLine).join('\n'),
        },
    };
}
export function inlineSourceFromText(relativePath, source, startLine, endLine, contextLines) {
    return inlineSourceFromLines(relativePath, source.split(/\r?\n/u), startLine, endLine, contextLines);
}
export function toSourceInlineProjection(result) {
    return {
        ...(result.block ? { inlineContext: result.block } : {}),
        ...(result.diagnostic ? { inlineContextDiagnostic: result.diagnostic } : {}),
    };
}
export function renderInlineSourceBlock(block, indent = '  ') {
    const width = String(block.endLine).length;
    return block.source
        .split('\n')
        .map((line, offset) => `${indent}${String(block.startLine + offset).padStart(width)} | ${line}`)
        .join('\n');
}
function resolveRepositoryFile(repoRoot, relativePath) {
    const absolute = path.resolve(repoRoot, relativePath);
    const back = path.relative(repoRoot, absolute);
    if (back === '' || back === '..' || back.startsWith(`..${path.sep}`) || path.isAbsolute(back))
        return undefined;
    return absolute;
}
async function loadVerifiedLines(index, relativePath) {
    const normalized = normalizePath(relativePath);
    const held = index.files.find(file => file.relativePath === normalized);
    const heldLines = held?.lines ?? held?.content?.split(/\r?\n/u);
    if (heldLines)
        return { lines: heldLines };
    const manifest = manifests.get(index)?.get(normalized);
    if (!manifest?.sha256) {
        return { diagnostic: stale(normalized, 'the indexed byte size and SHA-256 are unavailable') };
    }
    const absolute = resolveRepositoryFile(index.repoRoot, normalized);
    if (!absolute)
        return { diagnostic: stale(normalized, 'the path does not resolve inside the repository') };
    try {
        const bytes = await readFile(absolute);
        const identity = sourceIdentityForBytes(bytes);
        if (identity.size !== manifest.size || identity.sha256 !== manifest.sha256) {
            return {
                diagnostic: stale(normalized, `working-tree bytes do not match the indexed manifest (size ${identity.size}/${manifest.size}, SHA-256 ${identity.sha256 === manifest.sha256 ? 'matches' : 'differs'})`),
            };
        }
        return { lines: bytes.toString('utf8').split(/\r?\n/u) };
    }
    catch (error) {
        return {
            diagnostic: stale(normalized, `the verified source could not be read (${error.message})`),
        };
    }
}
async function sourceLines(index, relativePath, signal) {
    abortIfNeeded(signal);
    const normalized = normalizePath(relativePath);
    const held = index.files.find(file => file.relativePath === normalized);
    if (held?.lines || held?.content !== undefined) {
        const loaded = await loadVerifiedLines(index, normalized);
        abortIfNeeded(signal);
        return loaded;
    }
    let byPath = diskLines.get(index);
    if (!byPath) {
        byPath = new Map();
        diskLines.set(index, byPath);
    }
    let pending = byPath.get(normalized);
    if (!pending) {
        // The shared read must not inherit one caller's cancellation signal: an
        // aborted request must not poison this index's verified-source cache for
        // later operations or for concurrent callers with a live signal.
        pending = loadVerifiedLines(index, normalized);
        byPath.set(normalized, pending);
    }
    const loaded = await pending;
    abortIfNeeded(signal);
    return loaded;
}
/**
 * Read one indexed span plus `contextLines` on either side.
 *
 * Full-content indexes are their own immutable source snapshot. Reduced
 * indexes may consult the working tree only when both byte size and SHA-256
 * match the manifest captured from the bytes that were actually parsed.
 */
export async function readInlineSource(index, request) {
    const relativePath = normalizePath(request.relativePath);
    const loaded = await sourceLines(index, relativePath, request.signal);
    if (!loaded.lines)
        return { diagnostic: loaded.diagnostic ?? stale(relativePath, 'source is unavailable') };
    return inlineSourceFromLines(relativePath, loaded.lines, request.startLine, request.endLine, request.contextLines);
}
//# sourceMappingURL=source-inline.js.map