import { z } from 'zod';
import { abortIfNeeded, compareCodeUnits } from '../../utils.js';
import { scanMarkdownLines } from '../formats/markdown.js';
import { countEntityAmbiguityEvidence, summarizeEntityEvidenceGaps } from './evidence.js';
import { matchesPathScope, normalizePathScope, resolveEntity } from './resolve.js';
import { TRUNCATION_CODES } from './store.js';
const INLINE_CODE = /(?<!`)`([^`\n]+)`(?!`)/gu;
const QUALIFIED_IDENTIFIER = /^[A-Za-z_$][\w$]*(?:(?:\.|::)[A-Za-z_$][\w$]*)*$/u;
const PATH_SYMBOL = /^(?<relativePath>(?:\.{0,2}\/)?[A-Za-z0-9_.@/-]+\.[A-Za-z0-9]+)#(?<name>[A-Za-z_$][\w$]*(?:(?:\.|::)[A-Za-z_$][\w$]*)*)$/u;
const PLAIN_PATH_SYMBOL = /(?<![\w./-])((?:\.{0,2}\/)?[A-Za-z0-9_.@-]+(?:\/[A-Za-z0-9_.@-]+)*\.[A-Za-z0-9]+#[A-Za-z_$][\w$]*(?:(?:\.|::)[A-Za-z_$][\w$]*)*)/gu;
const CONSTANT_NAME = /^[A-Z][A-Z0-9_]*$/u;
const MARKDOWN_EXTENSIONS = ['.md', '.markdown', '.mdx'];
const DIAGNOSTIC_LIMIT = 50;
const ALL_REFUSED_DOCUMENT_LIMIT = 50;
export const ENTITY_DRIFT_DEFAULT_DIRS = ['docs', 'README.md'];
export const EntityDriftOptionsSchema = z.object({
    /** Repository-relative Markdown files or directory subtrees. */
    dirs: z.array(z.string().trim().min(1)).min(1).max(100).optional(),
});
function push(map, key, entity) {
    const bucket = map.get(key);
    if (bucket)
        bucket.push(entity);
    else
        map.set(key, [entity]);
}
export function filteredDriftLiveIndex(live, predicate) {
    const all = live.all.filter(predicate);
    const result = {
        all,
        byNodeId: new Map(),
        byLineageId: new Map(),
        byName: new Map(),
        byQualifiedName: new Map(),
        byPath: new Map(),
    };
    for (const entity of all) {
        if (entity.ref.nodeId)
            result.byNodeId.set(entity.ref.nodeId, entity);
        if (!result.byLineageId.has(entity.ref.lineageId))
            result.byLineageId.set(entity.ref.lineageId, entity);
        push(result.byName, entity.record.name, entity);
        push(result.byQualifiedName, entity.record.qualifiedName, entity);
        push(result.byPath, entity.record.relativePath, entity);
    }
    return result;
}
export function isDriftDeclarationTarget(entity) {
    if (['function', 'method', 'class', 'interface', 'type', 'enum'].includes(entity.record.kind))
        return true;
    return entity.record.kind === 'variable' && (entity.record.isExported || CONSTANT_NAME.test(entity.record.name));
}
function normalizeLinkPath(value) {
    const normalized = value.replaceAll('\\', '/').replace(/^\.\//u, '');
    if (normalized.startsWith('/') || normalized.split('/').includes('..'))
        return undefined;
    return normalized;
}
function parseReference(raw) {
    const trimmed = raw.trim().replace(/\(\)$/u, '');
    const pathSymbol = PATH_SYMBOL.exec(trimmed);
    if (pathSymbol?.groups) {
        const relativePath = normalizeLinkPath(pathSymbol.groups.relativePath);
        if (!relativePath)
            return undefined;
        return { name: pathSymbol.groups.name, relativePath };
    }
    if (!QUALIFIED_IDENTIFIER.test(trimmed))
        return undefined;
    return { name: trimmed };
}
function deepestSectionAtLine(sections, line) {
    let deepest;
    for (const section of sections) {
        if (section.startLine > line || section.endLine < line)
            continue;
        if (!deepest ||
            section.startLine > deepest.startLine ||
            (section.startLine === deepest.startLine && section.endLine < deepest.endLine)) {
            deepest = section;
        }
    }
    return deepest;
}
/**
 * Scan one Markdown document with the exact reference grammar used by
 * repository drift. Callers supply section spans, which lets generated
 * documents share this scanner without pretending they are repository
 * entities.
 */
export function scanDriftDocumentLinks(content, sections) {
    const links = [];
    const seen = new Set();
    for (const line of scanMarkdownLines(content)) {
        if (line.fenced)
            continue;
        const section = deepestSectionAtLine(sections, line.lineNumber);
        if (!section)
            continue;
        const add = (raw) => {
            const parsed = parseReference(raw);
            if (!parsed)
                return;
            const key = [section.key, parsed.relativePath ?? '', parsed.name].join('\0');
            if (seen.has(key))
                return;
            seen.add(key);
            links.push({ section, line: line.lineNumber, raw, ...parsed });
        };
        const prose = line.text.replace(INLINE_CODE, (_match, raw) => {
            add(raw);
            return ' ';
        });
        for (const match of prose.matchAll(PLAIN_PATH_SYMBOL)) {
            if (match[1])
                add(match[1]);
        }
    }
    return links;
}
function isMarkdownPath(relativePath) {
    const lower = relativePath.toLowerCase();
    return MARKDOWN_EXTENSIONS.some(extension => lower.endsWith(extension));
}
export function strongestDriftEvidence(events) {
    if (events.some(event => event.flags.semanticBasis === 'nontrivia-syntax-change'))
        return 'cst-proved';
    if (events.some(event => event.flags.bodyChanged === true ||
        (event.previousBodyHash !== undefined &&
            event.currentBodyHash !== undefined &&
            event.previousBodyHash !== event.currentBodyHash))) {
        return 'body-hash';
    }
    return 'unknown';
}
function rowOrder(a, b) {
    return (compareCodeUnits(a.docPath, b.docPath) ||
        a.docSpan.startLine - b.docSpan.startLine ||
        a.docSpan.endLine - b.docSpan.endLine ||
        compareCodeUnits(a.entityRef.relativePath, b.entityRef.relativePath) ||
        a.entityRef.startLine - b.entityRef.startLine ||
        compareCodeUnits(a.entityRef.qualifiedName ?? a.entityRef.name, b.entityRef.qualifiedName ?? b.entityRef.name) ||
        compareCodeUnits(a.entityRef.lineageId, b.entityRef.lineageId));
}
export function compareDriftCandidateRefs(a, b) {
    return (compareCodeUnits(a.relativePath, b.relativePath) ||
        a.startLine - b.startLine ||
        compareCodeUnits(a.qualifiedName ?? a.name, b.qualifiedName ?? b.name) ||
        compareCodeUnits(a.lineageId, b.lineageId));
}
/**
 * Join Markdown section references to current declarations, then compare their
 * commit order. Every uncertain name is counted and refused; this function
 * never chooses a candidate by iteration order.
 */
export function entityDrift(options, context) {
    const parsed = EntityDriftOptionsSchema.safeParse(options);
    if (!parsed.success) {
        return {
            status: 'error',
            code: 'invalid-options',
            message: parsed.error.issues[0]?.message ?? 'Invalid entity drift options.',
        };
    }
    let dirs;
    try {
        dirs = [...new Set((parsed.data.dirs ?? ENTITY_DRIFT_DEFAULT_DIRS).map(normalizePathScope))].sort(compareCodeUnits);
    }
    catch (error) {
        return {
            status: 'error',
            code: 'invalid-options',
            message: error instanceof Error ? error.message : String(error),
        };
    }
    const declarationTargets = filteredDriftLiveIndex(context.live, isDriftDeclarationTarget);
    const bareTargets = filteredDriftLiveIndex(declarationTargets, entity => entity.record.isExported && !entity.ref.isTest);
    const documentPaths = [...context.formatFiles.keys()]
        .filter(relativePath => isMarkdownPath(relativePath) && dirs.some(dir => matchesPathScope(relativePath, dir)))
        .sort(compareCodeUnits);
    const diagnostics = [];
    let diagnosticsOmitted = 0;
    const addDiagnostic = (diagnostic) => {
        if (diagnostics.length < DIAGNOSTIC_LIMIT)
            diagnostics.push(diagnostic);
        else
            diagnosticsOmitted++;
    };
    const byDocument = new Map();
    const rowsByPair = new Map();
    let linksExtracted = 0;
    let linksResolved = 0;
    let linksAmbiguous = 0;
    let linksNotFound = 0;
    let linksWithoutDocHistory = 0;
    for (const docPath of documentPaths) {
        abortIfNeeded(options.signal);
        const content = context.formatFiles.get(docPath);
        if (content === undefined)
            continue;
        const entities = (context.live.byPath.get(docPath) ?? [])
            .filter(entity => entity.record.kind === 'heading')
            .map(entity => ({
            key: entity.ref.lineageId,
            startLine: entity.ref.startLine,
            endLine: entity.ref.endLine,
            value: entity,
        }));
        const coverage = { candidates: 0, resolved: 0 };
        byDocument.set(docPath, coverage);
        for (const link of scanDriftDocumentLinks(content, entities)) {
            abortIfNeeded(options.signal);
            coverage.candidates++;
            linksExtracted++;
            const outcome = resolveEntity({ name: link.name, ...(link.relativePath ? { relativePath: link.relativePath } : {}) }, link.relativePath ? declarationTargets : bareTargets);
            const doc = link.section.value;
            const docSpan = { startLine: link.section.startLine, endLine: link.section.endLine };
            if (outcome.status !== 'resolved') {
                if (outcome.status === 'ambiguous')
                    linksAmbiguous++;
                else
                    linksNotFound++;
                const candidates = [...outcome.candidates].sort(compareDriftCandidateRefs).slice(0, 5);
                addDiagnostic({
                    code: outcome.status === 'ambiguous' ? 'link-ambiguous' : 'link-not-found',
                    message: outcome.status === 'ambiguous'
                        ? `Refused "${link.raw}": it matches ${outcome.candidates.length} eligible declarations.`
                        : `Refused "${link.raw}": no eligible declaration resolved uniquely.`,
                    docPath,
                    docSpan,
                    line: link.line,
                    reference: link.raw,
                    ...(candidates.length > 0 ? { candidates } : {}),
                });
                continue;
            }
            coverage.resolved++;
            linksResolved++;
            const docHead = context.entityIndex.state.byLineageId.get(doc.ref.lineageId);
            const docCommit = docHead ? context.entityIndex.commitBySha.get(docHead.lastCommit) : undefined;
            if (!docHead || !docCommit || doc.ref.provisional || doc.ref.indeterminate) {
                linksWithoutDocHistory++;
                addDiagnostic({
                    code: 'doc-history-unavailable',
                    message: `Resolved "${link.raw}", but the document section has no comparable committed last-touch event.`,
                    docPath,
                    docSpan,
                    line: link.line,
                    reference: link.raw,
                });
                continue;
            }
            const subsequent = (context.entityIndex.eventsByLineage.get(outcome.entity.ref.lineageId) ?? []).filter(event => event.commitOrder > docHead.lastCommitOrder && event.flags.logicChanged === true);
            const pairKey = `${doc.ref.lineageId}\0${outcome.entity.ref.lineageId}`;
            if (!rowsByPair.has(pairKey)) {
                rowsByPair.set(pairKey, {
                    docPath,
                    docSpan,
                    entityRef: outcome.entity.ref,
                    docLastTouch: {
                        commit: docHead.lastCommit,
                        commitOrder: docHead.lastCommitOrder,
                        committedAt: docCommit.committedAt,
                    },
                    changesSince: {
                        count: subsequent.length,
                        strongestEvidence: strongestDriftEvidence(subsequent),
                    },
                });
            }
        }
    }
    const allRefused = [...byDocument.entries()]
        .filter(([, coverage]) => coverage.candidates > 0 && coverage.resolved === 0)
        .map(([docPath]) => docPath)
        .sort(compareCodeUnits);
    const shownAllRefused = allRefused.slice(0, ALL_REFUSED_DOCUMENT_LIMIT);
    const diagnosticsFromIndex = context.entityIndex.status.diagnostics;
    const rows = [...rowsByPair.values()].sort(rowOrder);
    return {
        status: 'resolved',
        dirs,
        rows,
        diagnostics,
        honesty: {
            freshness: {
                fresh: context.freshness.fresh,
                reasons: context.freshness.reasons,
                workingTreeMeasured: context.freshness.workingTree !== undefined,
            },
            truncation: {
                truncated: context.entityIndex.status.truncated,
                reasons: diagnosticsFromIndex.filter(diagnostic => TRUNCATION_CODES.has(diagnostic.code)),
                omitted: 0,
            },
            coverage: {
                documentsConsidered: documentPaths.length,
                documentsWithCandidates: [...byDocument.values()].filter(coverage => coverage.candidates > 0).length,
                documentsAllLinksRefused: allRefused.length,
                allRefusedDocuments: shownAllRefused,
                allRefusedDocumentsOmitted: Math.max(0, allRefused.length - shownAllRefused.length),
                linksExtracted,
                linksResolved,
                linksRefused: linksAmbiguous + linksNotFound,
                linksAmbiguous,
                linksNotFound,
                linksWithoutDocHistory,
                rowsEmitted: rows.length,
                eligibleCodeTargets: declarationTargets.all.length,
                diagnosticsOmitted,
            },
            ambiguityCount: countEntityAmbiguityEvidence(context.entityIndex.counters.ambiguitiesRefused, diagnosticsFromIndex) +
                linksAmbiguous,
            evidenceGaps: summarizeEntityEvidenceGaps(diagnosticsFromIndex),
        },
    };
}
//# sourceMappingURL=drift.js.map