import { readFile } from 'node:fs/promises';
import path from 'node:path';
import { abortIfNeeded, compareCodeUnits } from './utils.js';
import { CODE_EXTENSIONS, getLanguageSpec, LANGUAGE_SPECS } from './code/languages.js';
import { resolveTreeSitterAsset, loadTreeSitterLanguage } from './code/tree-sitter.js';
import { structuralLanguages } from './code/structural.js';
import { resolveEntityCacheDir } from './code/entity/store.js';
import { gitVersion, preflightRepo } from './git/client.js';
async function probeGit(repoRoot, signal) {
    const version = await gitVersion(repoRoot, { signal }).catch(() => undefined);
    if (!version)
        return { available: false, isRepository: false, shallow: false };
    try {
        const info = await preflightRepo(repoRoot, { signal });
        const git = {
            available: true,
            version,
            isRepository: Boolean(info.head || info.currentBranch),
            shallow: info.shallow,
            isDirty: info.isDirty,
        };
        if (info.head)
            git.head = info.head;
        if (info.currentBranch)
            git.currentBranch = info.currentBranch;
        return git;
    }
    catch {
        return { available: true, version, isRepository: false, shallow: false };
    }
}
/**
 * The cache entry for this repository, without opening a store.
 *
 * A store open would resolve the repository identity and the options
 * fingerprint, which is most of what makes a session construction expensive.
 * A probe instead reads the entries the directory holds and reports the newest
 * one *belonging to this repository* — enough to answer "is there a warm cache
 * here", which is the question, without pretending to answer "would *my*
 * options hit it".
 *
 * Scoping by `repoRoot` is not optional. The default cache directory is global
 * (`~/.cache/occ/entity`) and shared by every repository on the machine, so
 * ranking every entry by `builtAt` answers with whichever repository was
 * indexed most recently — usually a different one.
 */
async function probeEntityCache(directory, repoRoot, head, signal) {
    const cache = { directory, present: false, fresh: false };
    const { readdir } = await import('node:fs/promises');
    let newest;
    try {
        const versions = await readdir(directory, { withFileTypes: true });
        for (const version of versions) {
            if (!version.isDirectory())
                continue;
            const entries = await readdir(path.join(directory, version.name), { withFileTypes: true }).catch(() => []);
            for (const entry of entries) {
                if (signal?.aborted)
                    return cache;
                if (!entry.isDirectory())
                    continue;
                const raw = await readFile(path.join(directory, version.name, entry.name, 'meta.json'), 'utf8').catch(() => undefined);
                if (!raw)
                    continue;
                // Every step from parse to comparison sits inside this try, not just
                // the parse: `null` and `{"repoRoot": 5}` are both valid JSON that
                // throw one line later. Letting any of it reach the outer catch would
                // abandon the scan and report "no cache" for a directory that is in
                // fact warm — the failure this guard exists to prevent, moved down a
                // line.
                try {
                    const parsed = JSON.parse(raw);
                    if (typeof parsed?.repoRoot !== 'string' || path.resolve(parsed.repoRoot) !== repoRoot)
                        continue;
                    if (!newest || (parsed.builtAt ?? '') > (newest.builtAt ?? ''))
                        newest = parsed;
                }
                catch {
                    continue;
                }
            }
        }
    }
    catch {
        return cache;
    }
    if (!newest)
        return cache;
    cache.present = true;
    cache.fresh = Boolean(head && newest.indexedHead === head);
    if (newest.indexedHead)
        cache.indexedHead = newest.indexedHead;
    if (newest.builtAt)
        cache.builtAt = newest.builtAt;
    if (newest.commitCount !== undefined)
        cache.commitCount = newest.commitCount;
    if (newest.truncated !== undefined)
        cache.truncated = newest.truncated;
    if (newest.schemaVersion !== undefined)
        cache.schemaVersion = newest.schemaVersion;
    if (newest.occVersion)
        cache.occVersion = newest.occVersion;
    return cache;
}
async function probeGrammars() {
    const languagesByGrammar = new Map();
    const note = (grammar, label) => {
        const bucket = languagesByGrammar.get(grammar);
        if (!bucket)
            languagesByGrammar.set(grammar, [label]);
        else if (!bucket.includes(label))
            bucket.push(label);
    };
    for (const spec of LANGUAGE_SPECS) {
        // The compiler and SFC tiers need no grammar, so their absence from this
        // map is correct rather than a gap: they cannot fail to load one.
        if (spec.treeSitter?.grammar)
            note(spec.treeSitter.grammar, spec.name);
    }
    // Most grammars are attached per *extension* at lookup time rather than
    // declared on a spec, so walking `LANGUAGE_SPECS` alone sees three of the
    // extraction grammars OCC ships. Asking `getLanguageSpec` about each known extension is
    // the only enumeration that matches what the parser will actually dispatch.
    for (const extension of CODE_EXTENSIONS) {
        const spec = getLanguageSpec(`probe.${extension}`);
        const grammar = spec?.treeSitter?.grammar;
        if (!grammar)
            continue;
        // A named spec already contributed its language above; only the generic
        // tier has no name of its own, and there the extension is what a reader
        // recognises. This keeps `problems` readable — "so .kt, .kts yield no
        // entities" — instead of listing "generic" a dozen times.
        note(grammar, spec.name === 'generic' ? `.${extension}` : spec.name);
    }
    // JS/TS/TSX deliberately stay on the compiler parser for symbol extraction.
    // Their grammars exist for the explicit structural-query surface, so they
    // must be probed independently of LANGUAGE_SPECS.
    for (const language of structuralLanguages()) {
        note(language.grammar, `${language.name} structural queries`);
    }
    for (const languages of languagesByGrammar.values())
        languages.sort();
    const loaded = [];
    const failed = [];
    const versions = [];
    for (const [grammar, languages] of languagesByGrammar) {
        if (!resolveTreeSitterAsset(grammar, 'grammar.wasm')) {
            failed.push({ grammar, languages, reason: 'missing-asset' });
            continue;
        }
        const language = await loadTreeSitterLanguage(grammar).catch(() => null);
        if (!language) {
            failed.push({ grammar, languages, reason: 'load-failed' });
            continue;
        }
        loaded.push(grammar);
        const metadata = language.metadata;
        const entry = { grammar, abiVersion: language.abiVersion };
        if (metadata)
            entry.version = `${metadata.major_version}.${metadata.minor_version}.${metadata.patch_version}`;
        versions.push(entry);
    }
    loaded.sort();
    failed.sort((a, b) => compareCodeUnits(a.grammar, b.grammar));
    versions.sort((a, b) => compareCodeUnits(a.grammar, b.grammar));
    return { loaded, failed, versions };
}
const DOCUMENT_PARSER_FORMATS = [
    { format: 'docx', dependencies: ['mammoth'] },
    { format: 'pdf', dependencies: ['pdf-parse'] },
    { format: 'xlsx', dependencies: ['xlsx'] },
    { format: 'pptx', dependencies: ['officeparser', 'jszip'] },
    { format: 'odt', dependencies: ['officeparser', 'jszip'] },
    { format: 'ods', dependencies: ['officeparser', 'jszip', 'xlsx'] },
    { format: 'odp', dependencies: ['officeparser', 'jszip'] },
];
/** Load-only readiness check; it never opens or parses a document. */
export async function probeDocumentParsers(loadModule = specifier => import(specifier), signal) {
    const results = new Map();
    const dependencies = [...new Set(DOCUMENT_PARSER_FORMATS.flatMap(item => item.dependencies))].sort();
    for (const dependency of dependencies) {
        abortIfNeeded(signal);
        try {
            await loadModule(dependency);
            results.set(dependency, undefined);
        }
        catch (error) {
            const raw = error instanceof Error ? error.message : String(error);
            results.set(dependency, raw.split(/\r?\n/, 1)[0]?.slice(0, 500) || 'Module load failed.');
        }
    }
    abortIfNeeded(signal);
    return {
        checked: true,
        formats: DOCUMENT_PARSER_FORMATS.map(item => {
            const failures = item.dependencies.flatMap(module => {
                const message = results.get(module);
                return message ? [{ module, message }] : [];
            });
            return {
                format: item.format,
                ready: failures.length === 0,
                dependencies: [...item.dependencies],
                failures,
            };
        }),
    };
}
export async function probe(options = {}) {
    const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
    const git = await probeGit(repoRoot, options.signal);
    const directory = options.cacheDir === false ? '' : resolveEntityCacheDir(options.cacheDir);
    const entityCache = options.cacheDir === false
        ? { directory: '', present: false, fresh: false }
        : await probeEntityCache(directory, repoRoot, git.head, options.signal);
    const grammars = options.grammars === false ? { loaded: [], failed: [], versions: [] } : await probeGrammars();
    const documentParsers = options.documentParsers === false
        ? { checked: false, formats: [] }
        : await probeDocumentParsers(undefined, options.signal);
    const problems = [];
    if (!git.available)
        problems.push('git is not available, so no history-derived answer is possible.');
    else if (!git.isRepository)
        problems.push(`${repoRoot} is not a git repository.`);
    else if (!git.head)
        problems.push('The repository has no commits, so there is no history to index.');
    if (git.shallow)
        problems.push('History is shallow; entities older than the graft point cannot be attributed.');
    if (git.head && !entityCache.present && options.cacheDir !== false) {
        problems.push('No lineage cache for this repository yet, so the first session will pay a cold build.');
    }
    else if (entityCache.present && !entityCache.fresh) {
        problems.push('The lineage cache does not cover the current head, so the next session will extend it.');
    }
    if (entityCache.truncated) {
        problems.push('The cached index hit a bound; empty or unattributed answers may be a limit rather than a fact.');
    }
    for (const entry of grammars.failed) {
        problems.push(`Grammar "${entry.grammar}" ${entry.reason === 'missing-asset' ? 'is not installed' : 'failed to load'}, ` +
            `so ${entry.languages.join(', ')} yield no entities.`);
    }
    for (const entry of grammars.versions) {
        // ABI 13 is the library's compatibility floor and is being phased out
        // upstream; 14 and 15 both load on every web-tree-sitter OCC supports.
        if (entry.abiVersion <= 13) {
            problems.push(`Grammar "${entry.grammar}" targets parser ABI ${entry.abiVersion}, the floor tree-sitter is phasing out; ` +
                `it should be regenerated before a runtime upgrade strands it.`);
        }
    }
    for (const entry of documentParsers.formats) {
        if (entry.ready)
            continue;
        const modules = entry.failures.map(failure => failure.module).join(', ');
        problems.push(`The ${entry.format} parser is unavailable because ${modules} could not load.`);
    }
    return { repoRoot, git, entityCache, grammars, documentParsers, problems };
}
//# sourceMappingURL=probe.js.map