// Runtime bundler pick (Phase D-2.B.5b → D-3.1 runtime-aware default).
//
// The engine is chosen by `shouldUseNative()` per host runtime:
//   - Node: npm `rolldown(opts).write(opts.output)`.
//   - GJS:  `@gjsify/rolldown-native` (the Vala/Rust prebuild) via
//           `bundleWithPlugins()`. npm `rolldown` is a Rust N-API addon
//           that CANNOT load under GJS, so the native engine is the GJS
//           DEFAULT — no env var needed (this is the Node-free build
//           chain's bundler). This writes the BundleOutput to disk
//           (`runNativeBundle`), replicating npm rolldown's `.write()`
//           incl. nested chunk/asset subdirectories.
//
// `GJSIFY_BUNDLER=native|npm` is an explicit override: `native` forces
// the native engine and throws if its prebuild isn't loadable for the
// running architecture (instead of silently switching engines); `npm`
// forces the npm crate (Node only).
//
// Plugin shape conversion: rolldown plugins may declare hooks either
// as bare functions (`load(id)`) or as `{filter, handler}` objects.
// Both shapes are translated to `NativePlugin` form. `filter.id`
// regex/string sources become `idFilter.<hook>` regex strings on the
// Rust side. `this.resolve()` / `this.warn()` / `this.error()` calls
// inside hook handlers route through the B.3 nested protocol.
//
// Plugins that depend on rolldown context methods we don't implement
// (`this.parse`, `this.emitFile`, `this.getModuleInfo`, …) will fail
// at hook-call time. The current gjsify plugin set doesn't use any
// of those, so we don't gate on it; future incompatibilities surface
// as build errors rather than silent wrong behavior.
import { promises as fs } from 'node:fs';
import * as path from 'node:path';
import { createRequire } from 'node:module';
import { pathToFileURL } from 'node:url';
import { resolveNpmPackage } from './utils/resolve-npm-package.js';
// npm `rolldown` is a Rust crate with platform-specific prebuilds; loading
// it eagerly at module init pulls musl-detection code that does
// `require('node:fs')` synchronously — fine on Node, but fatal under GJS
// where the createRequire polyfill rejects synchronous builtin loads.
// Dynamic import keeps it off the GJS code path entirely; the native
// branch (`@gjsify/rolldown-native`) handles bundling there.
async function loadNpmRolldown() {
    // Indirect specifier so Rolldown's static-analysis doesn't try to
    // bundle the npm crate into a GJS target build.
    const specifier = 'rolldown';
    const target = resolveImportTargetForGjs(specifier);
    const mod = (await import(/* @vite-ignore */ target));
    return mod.rolldown;
}
/**
 * Convert a bare npm specifier into something dynamic `import(...)` can
 * load regardless of host runtime:
 *
 *   - Node has a native node_modules resolver — return the specifier
 *     unchanged.
 *   - GJS's native ESM loader has no node_modules walker, so we resolve
 *     the specifier through multiple `createRequire` anchors (cwd,
 *     workspace root, bundle URL, parent-dir walk, `GJSIFY_NODE_PATH`)
 *     and dynamic-import the resulting `file://` URL.
 *
 * Falls back to the bare specifier when every anchor misses so the
 * host runtime's loader surfaces its native error path instead of a
 * silent synth from this helper.
 *
 * The bundle-URL anchor (`import.meta.url`) is critical for the case
 * where the install lives next to the bundle but the user invokes
 * `gjs -m <install>/dist/cli.gjs.mjs build …` from a completely
 * unrelated cwd — without it, the createRequire walk anchored at the
 * cwd's `node_modules` chain misses, and we'd throw `Module not found:
 * rolldown` even though the package is present under the install dir.
 */
function resolveImportTargetForGjs(specifier) {
    const isGjs = typeof globalThis.imports?.gi !== 'undefined';
    if (!isGjs)
        return specifier;
    const resolved = resolveNpmPackage(specifier, { bundleUrl: import.meta.url });
    if (resolved)
        return pathToFileURL(resolved).href;
    return specifier;
}
/**
 * In-memory bundle used by `--globals auto` for AST-driven detection.
 * Mirrors the shape of `AnalysisBundler` in
 * `@gjsify/rolldown-plugin-gjsify/utils/auto-globals`. Routes through the
 * same engine as the final build (npm rolldown on Node, native on GJS) so
 * the GJS-bundled CLI doesn't try to load the unloadable npm crate.
 */
export async function bundleToChunks(input) {
    if (await shouldUseNative()) {
        const native = await tryLoadNative();
        if (!native)
            throw new Error('@gjsify/rolldown-native not loadable');
        const rawPlugins = (input.rolldownInput.plugins ?? []);
        const nativePlugins = [];
        for (const p of rawPlugins) {
            if (isPluginObject(p))
                nativePlugins.push(toNativePlugin(p));
        }
        const opts = liftTransformExtras(stripUnserializable({
            ...input.rolldownInput,
            input: normalizeInputForNative(input.rolldownInput.input),
            format: input.format,
        }));
        delete opts.plugins;
        const result = await native.bundleWithPlugins(opts, nativePlugins);
        const codes = [];
        for (const item of result.output) {
            if (item.type === 'chunk')
                codes.push(item.code);
        }
        return codes;
    }
    const rolldown = await loadNpmRolldown();
    const build = await rolldown(input.rolldownInput);
    try {
        const result = await build.generate({ format: input.format, minify: false, sourcemap: false });
        const codes = [];
        for (const entry of result.output) {
            if (entry.type === 'chunk')
                codes.push(entry.code);
        }
        return codes;
    }
    finally {
        await build.close();
    }
}
/**
 * Watch source files and rebuild on change. Only npm rolldown supports
 * this path — `@gjsify/rolldown-native` does not surface a watcher API yet.
 * Returns the watcher; the caller registers `event` / `close` listeners
 * and is responsible for invoking `watcher.close()` on shutdown.
 */
export async function runWatch(finalOpts) {
    if (await shouldUseNative()) {
        throw new Error('`gjsify build --watch` requires the npm `rolldown` engine. The native engine ' +
            '(`@gjsify/rolldown-native`) does not expose a watcher API. Run the watch loop ' +
            'under Node (`node lib/index.js build … --watch`) or set `GJSIFY_BUNDLER=npm`.');
    }
    const specifier = 'rolldown';
    const target = resolveImportTargetForGjs(specifier);
    const mod = (await import(/* @vite-ignore */ target));
    const output = finalOpts.output ?? {};
    return mod.watch({ ...finalOpts, output });
}
/**
 * Run a bundle with the picked engine. Drop-in replacement for the
 * `rolldown(opts).write(opts.output)` flow used directly in build.ts.
 */
export async function runBundle(finalOpts) {
    if (await shouldUseNative()) {
        return await runNativeBundle(finalOpts);
    }
    const rolldown = await loadNpmRolldown();
    const build = await rolldown(finalOpts);
    try {
        return await build.write(finalOpts.output ?? {});
    }
    finally {
        await build.close();
    }
}
let _nativeProbe = null;
export async function shouldUseNative() {
    const env = globalThis.process?.env ?? {};
    const choice = env.GJSIFY_BUNDLER;
    // Explicit env-var override always wins.
    if (choice === 'npm')
        return false;
    if (choice === 'native') {
        const native = await tryLoadNative();
        if (!native) {
            throw new Error('GJSIFY_BUNDLER=native but @gjsify/rolldown-native is not loadable (no prebuild for this architecture, or not running under GJS).');
        }
        return true;
    }
    // Phase D-3.1 — runtime-aware default.
    //   Node:    use npm rolldown (no FFI loading cost at install time).
    //   GJS:     try @gjsify/rolldown-native — it's the only engine
    //            that can actually run here (npm rolldown is a Rust
    //            crate). Fall back to npm only on Node.
    const isGjs = typeof globalThis.imports?.gi !== 'undefined';
    if (!isGjs)
        return false;
    const native = await tryLoadNative();
    return native !== null;
}
async function tryLoadNative() {
    if (_nativeProbe)
        return _nativeProbe;
    _nativeProbe = (async () => {
        const isGjs = typeof globalThis.imports?.gi !== 'undefined';
        if (!isGjs)
            return null;
        try {
            // Under GJS the ESM loader has no node_modules resolver — a bare
            // `import('@gjsify/rolldown-native')` would throw `Module not
            // found`. Resolve via createRequire (PnP+node_modules-aware) to
            // a real path, then dynamic-import the resulting file:// URL.
            // Under Node a bare specifier import works directly, so we keep
            // the simpler form there.
            //
            // `createRequire` + `pathToFileURL` are statically imported at
            // the top of this file so the GJS bundle inlines them via
            // `@gjsify/module` / `@gjsify/url`. A *dynamic* `import('node:…')`
            // would instead hit the GJS native ESM loader which doesn't
            // know the `node:` URI scheme and throws — silently swallowed
            // by the surrounding catch, leaving the caller to fall back
            // to npm rolldown (which then throws ImportError for `rolldown`).
            const specifier = '@gjsify/rolldown-native';
            let target = specifier;
            if (isGjs) {
                // Same multi-anchor resolution as `loadNpmRolldown` —
                // when the bundle is invoked from a cwd outside the
                // install dir, anchoring solely at `import.meta.url`
                // misses node_modules layouts where the user's cwd
                // or the workspace root carries the package instead.
                const resolved = resolveNpmPackage(specifier, { bundleUrl: import.meta.url }) ??
                    createRequire(import.meta.url).resolve(specifier);
                target = pathToFileURL(resolved).href;
            }
            const mod = (await import(/* @vite-ignore */ target));
            if (!mod.hasNativeRolldown())
                return null;
            return mod;
        }
        catch {
            return null;
        }
    })();
    return _nativeProbe;
}
async function runNativeBundle(finalOpts) {
    const native = await tryLoadNative();
    if (!native) {
        throw new Error('@gjsify/rolldown-native not loadable');
    }
    const rawPlugins = (finalOpts.plugins ?? []);
    const nativePlugins = [];
    for (const p of rawPlugins) {
        if (isPluginObject(p))
            nativePlugins.push(toNativePlugin(p));
    }
    // Strip plugins from opts — bundleWithPlugins gets them as a
    // separate argument and the Rust side wires them into rolldown's
    // own plugin chain. Normalize `input` to the Rust deserializer's
    // expected `InputItem[]` shape, and flatten `output: { … }` (npm
    // rolldown's JS-side shape) into the top-level keys the Rust
    // BundlerOptions deserializer expects.
    const { output: outputOpts, plugins: _droppedPlugins, ...rest } = finalOpts;
    void _droppedPlugins;
    const bundlerOpts = liftTransformExtras(stripUnserializable({
        ...rest,
        ...outputOpts,
        input: normalizeInputForNative(finalOpts.input),
    }));
    if (globalThis.process?.env
        ?.GJSIFY_DEBUG_NATIVE_OPTS) {
        // Debug switch to inspect the shape we ship to the native facade —
        // mismatches surface as Rust serde parse errors that point at column
        // numbers in this JSON.
        console.error('[gjsify-bundler-pick] native opts JSON:', JSON.stringify(bundlerOpts));
    }
    const result = await native.bundleWithPlugins(bundlerOpts, nativePlugins);
    // The native facade returns the BundleOutput shape but doesn't
    // write files — replicate `.write()` here so callers see the same
    // on-disk layout whether they used npm rolldown or native.
    const outputCfg = (finalOpts.output ?? {});
    const outDir = outputCfg.dir ?? (outputCfg.file ? path.dirname(outputCfg.file) : process.cwd());
    if (outDir)
        await fs.mkdir(outDir, { recursive: true });
    for (const item of result.output) {
        // A single `--outfile` build collapses to that exact path; otherwise
        // each chunk/asset lands at `outDir/<fileName>`. Nested fileNames
        // (e.g. `_virtual/_rolldown/runtime.js` from a `--library` /
        // code-split build) live in subdirectories npm rolldown's `.write()`
        // creates implicitly — replicate that here, or `writeFile` throws
        // ENOENT on the missing parent (regression: `--library` under the
        // native GJS engine). Also write text assets for `.write()` parity.
        const target = outputCfg.file && item.type === 'chunk' && result.output.length === 1
            ? outputCfg.file
            : path.join(outDir, item.fileName);
        await fs.mkdir(path.dirname(target), { recursive: true });
        if (item.type === 'chunk') {
            await fs.writeFile(target, item.code, 'utf8');
        }
        else if (item.sourceText !== undefined) {
            await fs.writeFile(target, item.sourceText, 'utf8');
        }
    }
    // Synthesize the RolldownOutput shape downstream code touches.
    return synthRolldownOutput(result);
}
/**
 * Drop fields the JSON encoder can't ship to the Rust deserializer.
 * `external` may be a function predicate (npm rolldown supports this);
 * functions vanish under `JSON.stringify` and surface as parse errors on
 * the Rust side. Strip top-level function values so the option object
 * is JSON-clean. (The actual external behavior under native rolldown
 * comes from arrays/regex strings, set elsewhere by the orchestrator.)
 *
 * Also drops `sourcemap: false` — the JS API accepts a boolean while
 * the Rust deserializer expects an enum (`'File' | 'Inline' | 'Hidden'`).
 * Omitting the field has the same effect as `false`.
 */
function stripUnserializable(opts) {
    const out = {};
    for (const [k, v] of Object.entries(opts)) {
        if (typeof v === 'function')
            continue;
        if (k === 'sourcemap' && typeof v === 'boolean')
            continue;
        out[k] = v;
    }
    return out;
}
/**
 * The Rust deserializer in the native facade treats `define` and `inject`
 * as top-level fields on `BundlerOptions`, while npm rolldown's JS API
 * groups them under `transform.{define,inject}`. Lift them out so the
 * orchestrator's shape (built around the JS API) is accepted by the Rust
 * side too. Other transform sub-options (`target`, `dropLabels`, …) stay
 * where they are because they are real TransformOptions fields.
 */
function liftTransformExtras(opts) {
    const transform = opts['transform'];
    if (!transform)
        return opts;
    const lift = {};
    const remaining = { ...transform };
    for (const key of ['define', 'inject']) {
        if (key in remaining) {
            lift[key] = remaining[key];
            delete remaining[key];
        }
    }
    // `inject` shape diverges between engines: npm rolldown accepts a map
    // `{ alias: 'module' | [module, named] }`; the native Rust deserializer
    // expects an array of named-import descriptors. Convert if needed.
    if (lift['inject'] !== undefined && !Array.isArray(lift['inject'])) {
        lift['inject'] = mapToInjectArray(lift['inject']);
    }
    return {
        ...opts,
        transform: Object.keys(remaining).length === 0 ? undefined : remaining,
        ...lift,
    };
}
function mapToInjectArray(map) {
    const out = [];
    for (const [alias, value] of Object.entries(map)) {
        if (typeof value === 'string') {
            // Default-import binding: `import alias from 'module'`
            out.push({ type: 'default', from: value, alias });
        }
        else {
            // Named-import binding: `import { imported as alias } from 'from'`
            const [from, imported] = value;
            out.push({ type: 'named', from, imported, alias });
        }
    }
    return out;
}
/**
 * The native rolldown facade's Rust deserializer requires
 * `input: BundleInputItem[]` (i.e. `[{ import, name? }]`). Normalize the
 * other shapes npm rolldown accepts (string, string[], record) into that
 * shape so callers can pass either engine the same options object.
 */
function normalizeInputForNative(input) {
    if (input === undefined)
        return [];
    if (typeof input === 'string')
        return [{ import: input }];
    if (Array.isArray(input)) {
        return input.map((v) => (typeof v === 'string' ? { import: v } : v));
    }
    return Object.entries(input).map(([name, file]) => ({ name, import: file }));
}
export function isPluginObject(p) {
    return p !== null && typeof p === 'object' && !Array.isArray(p);
}
function pickHook(raw) {
    if (typeof raw === 'function')
        return { fn: raw };
    if (raw !== null && typeof raw === 'object' && 'handler' in raw) {
        const obj = raw;
        const idFilter = filterToRegexSource(obj.filter?.id);
        const out = { fn: obj.handler };
        if (idFilter !== undefined)
            out.idFilter = idFilter;
        return out;
    }
    return undefined;
}
function filterToRegexSource(f) {
    if (f === undefined)
        return undefined;
    if (Array.isArray(f)) {
        const sources = f.map(oneToSource).filter((x) => x !== undefined);
        return sources.length === 0 ? undefined : sources.length === 1 ? sources[0] : `(?:${sources.join('|')})`;
    }
    return oneToSource(f);
}
function oneToSource(f) {
    if (f instanceof RegExp)
        return f.source;
    if (typeof f === 'string') {
        // Treat plain strings as substring match. Escape regex metas
        // so glob-like inputs don't accidentally turn into wild
        // regexes.
        return f.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    }
    return undefined;
}
export function toNativePlugin(p) {
    const name = typeof p.name === 'string' ? p.name : 'unnamed-plugin';
    const out = { name };
    const idFilter = {};
    const load = pickHook(p.load);
    if (load) {
        out.load = load.fn;
        if (load.idFilter)
            idFilter.load = load.idFilter;
    }
    const transform = pickHook(p.transform);
    if (transform) {
        out.transform = transform.fn;
        if (transform.idFilter)
            idFilter.transform = transform.idFilter;
    }
    const resolveId = pickHook(p.resolveId);
    if (resolveId) {
        out.resolveId = resolveId.fn;
        if (resolveId.idFilter)
            idFilter.resolveId = resolveId.idFilter;
    }
    const renderChunk = pickHook(p.renderChunk);
    if (renderChunk)
        out.renderChunk = renderChunk.fn;
    const banner = pickHook(p.banner);
    if (banner)
        out.banner = banner.fn;
    const footer = pickHook(p.footer);
    if (footer)
        out.footer = footer.fn;
    const intro = pickHook(p.intro);
    if (intro)
        out.intro = intro.fn;
    const outro = pickHook(p.outro);
    if (outro)
        out.outro = outro.fn;
    const buildStart = pickHook(p.buildStart);
    if (buildStart)
        out.buildStart = buildStart.fn;
    const buildEnd = pickHook(p.buildEnd);
    if (buildEnd)
        out.buildEnd = buildEnd.fn;
    const generateBundle = pickHook(p.generateBundle);
    if (generateBundle)
        out.generateBundle = generateBundle.fn;
    const writeBundle = pickHook(p.writeBundle);
    if (writeBundle)
        out.writeBundle = writeBundle.fn;
    const closeBundle = pickHook(p.closeBundle);
    if (closeBundle)
        out.closeBundle = closeBundle.fn;
    if (Object.keys(idFilter).length > 0)
        out.idFilter = idFilter;
    return out;
}
function synthRolldownOutput(result) {
    // RolldownOutput's exact shape in the npm types is rich; we fill
    // in what downstream code in `build.ts` actually touches (none of
    // it inspects the output beyond logging the file count today).
    // Cast through unknown to satisfy the structural type without
    // pulling in every internal field.
    return {
        output: result.output.map((item) => {
            if (item.type === 'chunk') {
                return {
                    type: 'chunk',
                    fileName: item.fileName,
                    code: item.code,
                    name: item.name,
                    isEntry: item.isEntry,
                    isDynamicEntry: item.isDynamicEntry,
                    map: item.map ?? null,
                    sourcemapFileName: item.sourcemapFilename ?? null,
                    imports: item.imports,
                    dynamicImports: item.dynamicImports,
                    facadeModuleId: null,
                    moduleIds: [],
                    modules: {},
                    exports: [],
                    referencedFiles: [],
                    importedBindings: {},
                };
            }
            return {
                type: 'asset',
                fileName: item.fileName,
                names: item.names,
                originalFileNames: item.originalFileNames,
                source: item.sourceText ?? '',
                needsCodeReference: false,
            };
        }),
    };
}
