import { createHash } from 'node:crypto';
import { existsSync } from 'node:fs';
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { initializeTreeSitterRuntime, resolveTreeSitterAsset } from './tree-sitter.js';
/**
 * The vendored ast-grep WASM matching engine.
 *
 * The engine ships no grammars and no native code: it delegates parsing to the
 * same web-tree-sitter module instance and vendored grammar wasm files OCC
 * already uses, so an ast-grep match and a tsquery capture describe the same
 * parse tree. The artifact under vendor/ast-grep/ is self-built from a pinned
 * upstream tag (see vendor/ast-grep/README.md) and is content-addressed here:
 * both files are re-hashed before the engine is instantiated, and a mismatch
 * refuses to run rather than executing unpinned code.
 */
export const AST_GREP_ENGINE = { name: 'ast-grep', version: '0.45.1' };
const ENGINE_ASSETS = [
    { fileName: 'wasm.js', sha256: '5bd8bd4e999dd2f566c3f0b328ee61871918925820728411c2fac612084b50cb' },
    { fileName: 'wasm_bg.wasm', sha256: '532c5fb8f1a5e41de6041edd12236db5f6f202361da47a462834585b5b3b5995' },
];
/** A vendored engine artifact failed its content-addressed integrity check. */
export class AstGrepIntegrityError extends Error {
    code = 'ast-grep-integrity-failed';
    constructor(message) {
        super(message);
        this.name = 'AstGrepIntegrityError';
    }
}
/**
 * Languages where `$` is a valid identifier character need a different
 * metavariable sigil internally. These are ast-grep's own per-language
 * assignments; the remaining OCC languages (java, javascript, scala, tsx,
 * typescript) use the `$` default.
 */
const EXPANDO_BY_LANGUAGE = {
    c: '𐀀',
    cpp: '𐀀',
    csharp: 'µ',
    go: 'µ',
    kotlin: 'µ',
    php: 'µ',
    python: 'µ',
    ruby: 'µ',
    rust: 'µ',
    swift: 'µ',
};
let enginePromise;
const languageRegistrations = new Map();
export function resolveAstGrepAsset(fileName) {
    const here = path.dirname(fileURLToPath(import.meta.url));
    const candidates = [
        path.resolve(here, '../../vendor/ast-grep', fileName),
        path.resolve(here, '../../../vendor/ast-grep', fileName),
    ];
    return candidates.find(candidate => existsSync(candidate)) ?? null;
}
/** The engine artifact is present on disk. Says nothing about digest validity. */
export function astGrepAssetsPresent() {
    return ENGINE_ASSETS.every(asset => resolveAstGrepAsset(asset.fileName) !== null);
}
/** Verify bytes before they can reach either the JS or WASM evaluator. */
export function verifyAstGrepAssetBytes(fileName, bytes) {
    const asset = ENGINE_ASSETS.find(candidate => candidate.fileName === fileName);
    if (!asset) {
        throw new AstGrepIntegrityError(`Unknown ast-grep engine asset "${fileName}".`);
    }
    const digest = createHash('sha256').update(bytes).digest('hex');
    if (digest !== asset.sha256) {
        throw new AstGrepIntegrityError(`The vendored ast-grep engine asset "${asset.fileName}" does not match its pinned digest; ` +
            `refusing to run an unpinned engine. Expected ${asset.sha256}, found ${digest}.`);
    }
}
async function readVerifiedEngineAssets() {
    const verified = new Map();
    for (const asset of ENGINE_ASSETS) {
        const resolved = resolveAstGrepAsset(asset.fileName);
        if (!resolved) {
            throw new AstGrepIntegrityError(`The vendored ast-grep engine asset "${asset.fileName}" is missing from vendor/ast-grep/.`);
        }
        const bytes = await readFile(resolved);
        verifyAstGrepAssetBytes(asset.fileName, bytes);
        verified.set(asset.fileName, bytes);
    }
    const glue = verified.get('wasm.js');
    const wasm = verified.get('wasm_bg.wasm');
    if (!glue || !wasm) {
        throw new AstGrepIntegrityError('The vendored ast-grep engine layout is incomplete.');
    }
    return { glue, wasm };
}
/**
 * Turn the verified glue bytes into an importable module without reading the
 * vendor file again. A data URL has no package-resolution base, so the one
 * bare dependency specifier is replaced with Node's resolved ESM URL. The
 * executable body otherwise comes exclusively from the bytes just hashed.
 */
function verifiedGlueDataUrl(bytes) {
    let source;
    try {
        source = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
    }
    catch (error) {
        const detail = error instanceof Error ? ` ${error.message}` : '';
        throw new AstGrepIntegrityError(`The vendored ast-grep JS glue is not valid UTF-8.${detail}`);
    }
    const dependencyImport = "from 'web-tree-sitter'";
    const first = source.indexOf(dependencyImport);
    const last = source.lastIndexOf(dependencyImport);
    if (first < 0 || first !== last) {
        throw new AstGrepIntegrityError('The vendored ast-grep JS glue has an unexpected dependency-import layout; refusing to transform it.');
    }
    const resolvedDependency = import.meta.resolve('web-tree-sitter');
    const executableSource = source.slice(0, first) +
        `from ${JSON.stringify(resolvedDependency)}` +
        source.slice(first + dependencyImport.length);
    return `data:text/javascript;base64,${Buffer.from(executableSource, 'utf8').toString('base64')}`;
}
async function loadEngineModule() {
    const assets = await readVerifiedEngineAssets();
    // web-tree-sitter's one-time init is awaited first so the engine's own init
    // observes an already-initialized runtime instead of racing it.
    await initializeTreeSitterRuntime();
    const module = (await import(verifiedGlueDataUrl(assets.glue)));
    await module.default({ module_or_path: assets.wasm });
    await module.initializeTreeSitter();
    return module;
}
async function loadEngine() {
    if (!enginePromise) {
        enginePromise = loadEngineModule().catch((error) => {
            enginePromise = undefined;
            throw error;
        });
    }
    return enginePromise;
}
async function ensureLanguageRegistered(module, language) {
    const cached = languageRegistrations.get(language.name);
    if (cached)
        return cached;
    const promise = (async () => {
        const grammarPath = resolveTreeSitterAsset(language.grammar, 'grammar.wasm');
        if (!grammarPath) {
            throw new Error(`The vendored ${language.name} tree-sitter grammar is unavailable.`);
        }
        const expandoChar = EXPANDO_BY_LANGUAGE[language.name];
        await module.registerDynamicLanguage({
            [language.name]: { libraryPath: grammarPath, ...(expandoChar ? { expandoChar } : {}) },
        });
    })().catch((error) => {
        languageRegistrations.delete(language.name);
        throw error;
    });
    languageRegistrations.set(language.name, promise);
    return promise;
}
/**
 * Parse one source text with the ast-grep engine.
 *
 * The returned handle owns WASM-side memory in two runtimes: the caller must
 * `free()` every SgNode it obtained, then `free()` the root, and finally
 * `delete()` the inner web-tree-sitter tree — web-tree-sitter 0.26 has no
 * finalizer, so a skipped delete leaks the parse for the process lifetime.
 */
export async function parseWithAstGrep(language, source) {
    const module = await loadEngine();
    await ensureLanguageRegistered(module, language);
    return module.parse(language.name, source);
}
//# sourceMappingURL=ast-grep.js.map