import { readFile } from 'node:fs/promises';
import { z } from 'zod';
import { createTokenizer, resolveTokenizerName } from '../tokens.js';
import { getLanguageSpec } from './languages.js';
import { parseCodeContent } from './parsers.js';
import { candidateFromNode, resolveWithinRepo, selectByQuery } from './source.js';
import { collectTreeSitterTagCaptures, openTreeSitterParseCache, } from './tree-sitter.js';
export const CodeDocKindSchema = z.enum(['function', 'class', 'interface', 'type-alias', 'enum', 'variable']);
export const CodeDocQuerySchema = z.union([
    z.object({ nodeId: z.string().min(1) }),
    z.object({
        name: z.string().min(1),
        relativePath: z.string().min(1).optional(),
        kind: CodeDocKindSchema.optional(),
        containerName: z.string().min(1).optional(),
    }),
]);
function emptyObservation() {
    return { candidatesConsidered: 0, symbolsMatched: 0, fullSourceRead: false, ambiguityCount: 0 };
}
function deriveHonesty(observation) {
    return {
        freshness: 'mixed-index-and-working-tree',
        stalenessReason: 'resolution came from the index; source was reread without rechecking index freshness',
        truncation: { truncated: false },
        coverage: {
            candidatesConsidered: observation.candidatesConsidered,
            symbolsMatched: observation.symbolsMatched,
            fullSourceRead: observation.fullSourceRead,
        },
        ambiguityCount: observation.ambiguityCount,
    };
}
function honestyKey(value) {
    return [
        value.freshness,
        value.stalenessReason,
        value.truncation.truncated,
        value.coverage.candidatesConsidered,
        value.coverage.symbolsMatched,
        value.coverage.fullSourceRead,
        value.ambiguityCount,
    ].join('\u0000');
}
function honestyDelta(base, effective) {
    const delta = {};
    if (effective.freshness !== base.freshness)
        delta.freshness = effective.freshness;
    if (effective.stalenessReason !== base.stalenessReason)
        delta.stalenessReason = effective.stalenessReason;
    if (effective.truncation.truncated !== base.truncation.truncated)
        delta.truncation = effective.truncation;
    const coverage = {};
    if (effective.coverage.candidatesConsidered !== base.coverage.candidatesConsidered) {
        coverage.candidatesConsidered = effective.coverage.candidatesConsidered;
    }
    if (effective.coverage.symbolsMatched !== base.coverage.symbolsMatched) {
        coverage.symbolsMatched = effective.coverage.symbolsMatched;
    }
    if (effective.coverage.fullSourceRead !== base.coverage.fullSourceRead) {
        coverage.fullSourceRead = effective.coverage.fullSourceRead;
    }
    if (Object.keys(coverage).length > 0)
        delta.coverage = coverage;
    if (effective.ambiguityCount !== base.ambiguityCount)
        delta.ambiguityCount = effective.ambiguityCount;
    return Object.keys(delta).length > 0 ? delta : undefined;
}
/** Merge an envelope baseline with one result's optional honesty delta. */
export function mergeCodeDocHonesty(base, delta) {
    return {
        freshness: delta?.freshness ?? base.freshness,
        stalenessReason: delta?.stalenessReason ?? base.stalenessReason,
        truncation: delta?.truncation ?? base.truncation,
        coverage: { ...base.coverage, ...delta?.coverage },
        ambiguityCount: delta?.ambiguityCount ?? base.ambiguityCount,
    };
}
/**
 * Hoist the modal effective honesty value into one batch envelope. Positional
 * ties keep the first observed value, and every differing result carries the
 * exact delta required to reconstruct its original effective honesty.
 */
export function compactCodeDocHonesty(results) {
    const baseline = (() => {
        if (results.length === 0)
            return deriveHonesty(emptyObservation());
        const counts = new Map();
        results.forEach((result, index) => {
            const key = honestyKey(result.honesty);
            const current = counts.get(key);
            if (current)
                current.count++;
            else
                counts.set(key, { honesty: result.honesty, count: 1, first: index });
        });
        return [...counts.values()].sort((left, right) => right.count - left.count || left.first - right.first)[0].honesty;
    })();
    const compact = results.map(result => {
        const { honesty: effective, ...outcome } = result;
        const delta = honestyDelta(baseline, effective);
        return delta ? { ...outcome, honestyDelta: delta } : outcome;
    });
    return { honesty: baseline, results: compact };
}
function selectedNodes(index, query) {
    if ('nodeId' in query)
        return selectByQuery(index, query);
    const selected = selectByQuery(index, {
        name: query.name,
        file: query.relativePath,
        type: query.kind,
    });
    if (typeof selected === 'string' || query.containerName == null)
        return selected;
    return selected.filter(node => node.containerName === query.containerName);
}
function displaySignature(node, parsed) {
    if (parsed.fullSignature)
        return parsed.fullSignature;
    if (parsed.signature)
        return parsed.signature;
    if (node.signature)
        return node.signature;
    const container = node.containerName ? `${node.containerName}.` : '';
    return `${node.type} ${container}${node.name}`;
}
function matchingCloseParen(value, open) {
    let depth = 0;
    let quote;
    let escaped = false;
    for (let index = open; index < value.length; index++) {
        const char = value[index];
        if (quote) {
            if (escaped)
                escaped = false;
            else if (char === '\\')
                escaped = true;
            else if (char === quote)
                quote = undefined;
            continue;
        }
        if (char === "'" || char === '"' || char === '`') {
            quote = char;
            continue;
        }
        if (char === '(')
            depth++;
        else if (char === ')' && --depth === 0)
            return index;
    }
    return -1;
}
function splitParameters(signature) {
    const open = signature.indexOf('(');
    if (open < 0)
        return [];
    const close = matchingCloseParen(signature, open);
    if (close < 0)
        return [];
    const body = signature.slice(open + 1, close).trim();
    if (!body)
        return [];
    const parts = [];
    let start = 0;
    let quote;
    let escaped = false;
    const stack = [];
    const closing = { '(': ')', '[': ']', '{': '}', '<': '>' };
    for (let index = 0; index < body.length; index++) {
        const char = body[index];
        if (quote) {
            if (escaped)
                escaped = false;
            else if (char === '\\')
                escaped = true;
            else if (char === quote)
                quote = undefined;
            continue;
        }
        if (char === "'" || char === '"' || char === '`') {
            quote = char;
            continue;
        }
        if (char in closing) {
            stack.push(closing[char]);
            continue;
        }
        if (stack.at(-1) === char) {
            stack.pop();
            continue;
        }
        if (char === ',' && stack.length === 0) {
            parts.push(body.slice(start, index).trim());
            start = index + 1;
        }
    }
    parts.push(body.slice(start).trim());
    return parts.filter(Boolean);
}
function sameParsedSymbol(candidate, node) {
    return candidate.name === node.name && candidate.type === node.type && candidate.containerName === node.containerName;
}
function cleanComment(raw) {
    const cleaned = raw
        .replace(/^\s*\/\*+/, '')
        .replace(/\*\/\s*$/, '')
        .split(/\r?\n/)
        .map(line => line.replace(/^\s*(?:\*+|\/\/\/?|#|--)\s?/, '').trimEnd())
        .join('\n')
        .trim();
    return cleaned || undefined;
}
function isComment(node) {
    return node != null && node.type.toLowerCase().includes('comment');
}
function precedingComment(node) {
    const comments = [];
    let previous = node.previousNamedSibling ?? node.previousSibling;
    let nextStartRow = node.startPosition.row;
    while (isComment(previous) && nextStartRow - previous.endPosition.row <= 1) {
        comments.unshift(previous);
        nextStartRow = previous.startPosition.row;
        previous = previous.previousNamedSibling ?? previous.previousSibling;
    }
    return cleanComment(comments.map(comment => comment.text).join('\n'));
}
function nodeSpanMatches(node, line, endLine) {
    return node.startPosition.row + 1 === line && node.endPosition.row + 1 === endLine;
}
async function treeSitterDocComment(grammar, tree, line, endLine, declarationTypes) {
    const captures = await collectTreeSitterTagCaptures(grammar, tree.rootNode);
    const tagged = (captures ?? [])
        .filter(capture => capture.name.startsWith('definition.') && nodeSpanMatches(capture.node, line, endLine))
        .map(capture => capture.node);
    if (tagged.length > 0)
        return precedingComment(tagged[0]);
    let matched;
    const visit = (node) => {
        if (matched)
            return;
        if (declarationTypes.has(node.type) && nodeSpanMatches(node, line, endLine)) {
            matched = node;
            return;
        }
        for (const child of node.namedChildren)
            visit(child);
    };
    visit(tree.rootNode);
    return matched ? precedingComment(matched) : undefined;
}
function readContractFile(context, filePath, repoRoot) {
    const cached = context.files.get(filePath);
    if (cached)
        return cached;
    const pending = (async () => {
        const content = await readFile(filePath, 'utf8');
        const parsed = await parseCodeContent(filePath, content, {
            repoRoot,
            resolveImports: false,
            treeSitterParseCache: context.treeCache,
        });
        return { content, parsed };
    })();
    context.files.set(filePath, pending);
    return pending;
}
function readContractTree(context, grammar, content) {
    return context.treeCache.acquire(grammar, content);
}
async function readOne(index, rawQuery, countTokens, tokenizer, batch, observation) {
    const parsedQuery = CodeDocQuerySchema.safeParse(rawQuery);
    if (!parsedQuery.success) {
        return {
            status: 'error',
            message: `Invalid code doc query: ${parsedQuery.error.message}`,
        };
    }
    const query = parsedQuery.data;
    try {
        const selected = selectedNodes(index, query);
        if (typeof selected === 'string') {
            return { status: 'error', query, message: selected };
        }
        observation.candidatesConsidered = selected.length;
        if (selected.length === 0) {
            return {
                status: 'not_found',
                query,
                candidates: [],
                message: 'No matching source node was found.',
            };
        }
        if (selected.length > 1) {
            observation.ambiguityCount = selected.length;
            return {
                status: 'ambiguous',
                query,
                candidates: selected.map(candidateFromNode),
                message: `Found ${selected.length} matching source nodes.`,
            };
        }
        const node = selected[0];
        if (node.line == null) {
            return {
                status: 'error',
                query,
                message: `Node "${node.id}" does not have a source line.`,
            };
        }
        const resolved = resolveWithinRepo(index.repoRoot, node.path);
        if (typeof resolved === 'string') {
            return { status: 'error', query, message: resolved };
        }
        const language = getLanguageSpec(resolved.path);
        if (!language) {
            return {
                status: 'unsupported',
                query,
                candidate: candidateFromNode(node),
                message: `No contract parser is available for "${resolved.relativePath}".`,
            };
        }
        if (language.parser === 'generic' && !language.treeSitter) {
            return {
                status: 'unsupported',
                query,
                candidate: candidateFromNode(node),
                message: `Contract reads are unsupported for the regex fallback used by "${resolved.relativePath}".`,
            };
        }
        const { content, parsed: reparsed } = await readContractFile(batch, resolved.path, index.repoRoot);
        observation.fullSourceRead = true;
        if (!reparsed || reparsed.kind !== 'parsed') {
            return {
                status: 'unsupported',
                query,
                candidate: candidateFromNode(node),
                message: `The active parser could not produce a contract for "${resolved.relativePath}".`,
            };
        }
        const matching = reparsed.file.symbols.filter(symbol => sameParsedSymbol(symbol, node));
        observation.symbolsMatched = matching.length;
        const atIndexedLine = matching.filter(symbol => symbol.line === node.line);
        const parsedSymbol = atIndexedLine.length === 1 ? atIndexedLine[0] : matching.length === 1 ? matching[0] : undefined;
        if (!parsedSymbol) {
            observation.ambiguityCount = matching.length > 1 ? matching.length : 0;
            return {
                status: 'error',
                query,
                message: `The indexed symbol no longer resolves uniquely in "${resolved.relativePath}".`,
            };
        }
        let docComment = parsedSymbol.jsdoc;
        if (language.treeSitter) {
            const declarationTypes = new Set([
                ...language.treeSitter.functionNodeTypes,
                ...language.treeSitter.classNodeTypes,
            ]);
            const treeLease = await readContractTree(batch, language.treeSitter.grammar, content);
            if (!treeLease) {
                return {
                    status: 'unsupported',
                    query,
                    candidate: candidateFromNode(node),
                    message: `The tree-sitter grammar for "${resolved.relativePath}" is unavailable; regex fallback has no contract reads.`,
                };
            }
            try {
                docComment = await treeSitterDocComment(language.treeSitter.grammar, treeLease.tree, parsedSymbol.line, parsedSymbol.endLine ?? parsedSymbol.line, declarationTypes);
            }
            finally {
                treeLease.release();
            }
        }
        const endLine = parsedSymbol.endLine ?? parsedSymbol.line;
        const source = content
            .split(/\r?\n/)
            .slice(parsedSymbol.line - 1, endLine)
            .join('\n');
        const signature = displaySignature(node, parsedSymbol);
        return {
            status: 'resolved',
            query,
            nodeId: node.id,
            name: node.name,
            type: node.type,
            path: resolved.path,
            relativePath: resolved.relativePath,
            line: parsedSymbol.line,
            endLine,
            containerName: node.containerName,
            signature,
            params: splitParameters(signature),
            docComment,
            bodyTokens: countTokens(source),
            tokenizer,
            bodySpanExact: endLine > parsedSymbol.line,
        };
    }
    catch (error) {
        return {
            status: 'error',
            query,
            message: error instanceof Error ? error.message : String(error),
        };
    }
}
/**
 * Read symbol contracts in a positionally aligned batch. Source is reread only
 * to derive the contract and body price; no result contains the body itself.
 */
export async function readCodeDocs(index, queries, options = {}) {
    const active = options.countTokens
        ? { name: options.tokenizer ?? 'custom', count: options.countTokens }
        : await createTokenizer(resolveTokenizerName(options.tokenizer));
    const batch = { files: new Map(), treeCache: openTreeSitterParseCache(index) };
    const observed = [];
    try {
        for (const query of queries) {
            const observation = emptyObservation();
            const outcome = await readOne(index, query, text => active.count(text), active.name, batch, observation);
            observed.push({ outcome, observation });
        }
        return observed.map(({ outcome, observation }) => ({ ...outcome, honesty: deriveHonesty(observation) }));
    }
    finally {
        batch.treeCache.close();
    }
}
//# sourceMappingURL=doc.js.map