import { fork } from 'node:child_process';
import { existsSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { OccAbortError } from '../../errors.js';
import { abortIfNeeded, sanitizeForkExecArgv } from '../../utils.js';
import { createEntitySession } from './session.js';
function resolveRunnerPath() {
    const directory = path.dirname(fileURLToPath(import.meta.url));
    const built = path.join(directory, 'isolated-runner.js');
    if (existsSync(built))
        return built;
    return path.join(directory, 'isolated-runner.ts');
}
/**
 * Warm this repository's caches in a child process, then open a session on them.
 *
 * What is isolated is the **build**, not the session. Nothing about a query
 * session is transferable — it is a graph of maps over shared string identity —
 * so the child does the part that actually costs: the commit walk, blob reads,
 * tree-sitter parsing, and lineage matching, all of which are CPU- and
 * git-bound and would otherwise block the caller's event loop for as long as a
 * cold index takes. It persists both the lineage cache and the code index, then
 * exits; the parent opens a normal session, which finds both warm.
 *
 * That leaves the parent paying deserialization, and only for what an unchanged
 * index actually reads — the lineage heads and event log, never the parsed-blob
 * cache. It is a real cost, not zero, and this function does not pretend
 * otherwise; it is a fraction of a cold build.
 *
 * A cache directory is therefore required. With `cacheDir: false` the child
 * would build in memory and take every result to the grave, so that
 * combination is refused rather than silently degraded to an in-process build.
 */
export async function createEntitySessionIsolated(options, onProgress) {
    if (options.cacheDir === false) {
        throw new Error('createEntitySessionIsolated requires a cache directory: the child process builds the index and exits, ' +
            'so an in-memory index would be discarded with it. Pass a cacheDir, or use createEntitySession.');
    }
    abortIfNeeded(options.signal);
    const { signal, ...buildOptions } = options;
    await warmInChild(buildOptions, signal, onProgress);
    abortIfNeeded(signal);
    return createEntitySession({ ...buildOptions, signal, onProgress });
}
/** Run one build to completion in a forked child, forwarding progress and cancellation. */
function warmInChild(options, signal, onProgress) {
    const runnerPath = resolveRunnerPath();
    return new Promise((resolve, reject) => {
        let settled = false;
        let abortTimer;
        const child = fork(runnerPath, [], {
            stdio: ['ignore', 'ignore', 'pipe', 'ipc'],
            execArgv: sanitizeForkExecArgv(),
        });
        const cleanup = () => {
            if (abortTimer)
                clearTimeout(abortTimer);
            signal?.removeEventListener('abort', handleAbort);
        };
        const handleAbort = () => {
            if (settled)
                return;
            child.send({ type: 'abort' });
            // A cooperative abort has to reach a checkpoint; a build wedged in a
            // syscall never will, and the caller asked to stop either way.
            abortTimer = setTimeout(() => {
                if (!settled)
                    child.kill();
            }, 1000);
        };
        signal?.addEventListener('abort', handleAbort, { once: true });
        let stderr = '';
        child.stderr?.on('data', (chunk) => {
            stderr += chunk.toString();
        });
        child.on('message', (msg) => {
            if (msg.type === 'progress' && msg.event) {
                onProgress?.(msg.event);
            }
            else if (msg.type === 'ready') {
                settled = true;
                cleanup();
                resolve();
            }
            else if (msg.type === 'aborted') {
                settled = true;
                cleanup();
                reject(new OccAbortError());
            }
            else if (msg.type === 'error') {
                settled = true;
                cleanup();
                const error = new Error(msg.message ?? 'Isolated entity index build failed');
                if (msg.name)
                    error.name = msg.name;
                reject(error);
            }
        });
        child.on('error', error => {
            settled = true;
            cleanup();
            reject(error);
        });
        child.on('close', code => {
            if (settled)
                return;
            cleanup();
            if (signal?.aborted) {
                reject(new OccAbortError());
                return;
            }
            reject(code !== 0
                ? new Error(`Isolated entity index runner exited with code ${code}: ${stderr.trim()}`)
                : new Error('Isolated entity index runner exited without warming the cache'));
        });
        child.send({ type: 'start', options });
    });
}
//# sourceMappingURL=isolated.js.map