import { fork } from 'node:child_process';
import { existsSync } from 'node:fs';
import { rm } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { readNdjsonFile, readSectionedCodeIndex } from './index-io.js';
import { CodeIndexBudgetExceededError } from './build.js';
import { OccAbortError } from '../errors.js';
import { abortIfNeeded, isCooperative, sanitizeForkExecArgv } from '../utils.js';
function resolveRunnerPath() {
    const directory = path.dirname(fileURLToPath(import.meta.url));
    const builtRunner = path.join(directory, 'isolated-runner.js');
    if (existsSync(builtRunner))
        return builtRunner;
    return path.join(directory, 'isolated-runner.ts');
}
function resolveChunkRunnerPath() {
    const directory = path.dirname(fileURLToPath(import.meta.url));
    const builtRunner = path.join(directory, 'isolated-chunk-runner.js');
    if (existsSync(builtRunner))
        return builtRunner;
    return path.join(directory, 'isolated-chunk-runner.ts');
}
function serializableCooperative(option) {
    if (!isCooperative(option))
        return undefined;
    if (option === true)
        return true;
    return { yieldEveryMs: option.yieldEveryMs };
}
/**
 * Rebuild a typed error from the fields the runner transmits so callers keep the
 * same `instanceof`/`.code`/`.budget` surface they get from the in-process build.
 */
function rehydrateRunnerError(msg, fallbackMessage) {
    if (msg.code === 'OCC_CODE_INDEX_BUDGET_EXCEEDED' && msg.budget) {
        return new CodeIndexBudgetExceededError(msg.budget);
    }
    const error = new Error(msg.message ?? fallbackMessage);
    if (msg.name)
        error.name = msg.name;
    if (msg.code)
        error.code = msg.code;
    return error;
}
export async function buildCodebaseIndexIsolated(options, onProgress) {
    abortIfNeeded(options.signal);
    const runnerPath = resolveRunnerPath();
    const { signal, slim, ...requestedBuildOptions } = options;
    const buildOptions = {
        ...requestedBuildOptions,
        typescriptChecker: options.typescriptChecker ?? 'preferred',
    };
    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 rejectAbort = () => {
            cleanup();
            reject(new OccAbortError());
        };
        const handleAbort = () => {
            if (settled)
                return;
            child.send({ type: 'abort' });
            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 === 'aborted') {
                settled = true;
                rejectAbort();
            }
            else if (msg.type === 'complete' && msg.resultPath) {
                settled = true;
                const resultPath = msg.resultPath;
                void (async () => {
                    try {
                        const { index } = await readSectionedCodeIndex(resultPath, signal);
                        cleanup();
                        resolve(index);
                    }
                    catch (err) {
                        cleanup();
                        reject(err instanceof Error ? err : new Error(String(err)));
                    }
                    finally {
                        // Always remove the artifact once its path is known, even when the
                        // read fails or is interrupted by an aborting signal.
                        await rm(resultPath, { recursive: true, force: true }).catch(() => undefined);
                    }
                })();
            }
            else if (msg.type === 'error') {
                settled = true;
                cleanup();
                reject(rehydrateRunnerError(msg, 'Isolated code index build failed'));
            }
        });
        child.on('error', err => {
            settled = true;
            cleanup();
            reject(err);
        });
        child.on('close', code => {
            if (settled)
                return;
            cleanup();
            if (signal?.aborted) {
                rejectAbort();
                return;
            }
            if (code !== 0) {
                reject(new Error(`Isolated code index runner exited with code ${code}: ${stderr.trim()}`));
            }
            else {
                reject(new Error('Isolated code index runner exited without sending a result'));
            }
        });
        child.send({ type: 'start', options: buildOptions, slim });
    });
}
export async function chunkFromStoreIsolated(options, onProgress) {
    abortIfNeeded(options.signal);
    const runnerPath = resolveChunkRunnerPath();
    const { signal, strategy, chunk, cooperative, ...storeOptions } = options;
    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 rejectAbort = () => {
            cleanup();
            reject(new OccAbortError());
        };
        const handleAbort = () => {
            if (settled)
                return;
            child.send({ type: 'abort' });
            abortTimer = setTimeout(() => {
                if (!settled)
                    child.kill();
            }, 1000);
        };
        signal?.addEventListener('abort', handleAbort, { once: true });
        let stderr = '';
        child.stderr?.on('data', (chunkData) => {
            stderr += chunkData.toString();
        });
        child.on('message', (msg) => {
            if (msg.type === 'progress' && msg.event) {
                onProgress?.(msg.event);
            }
            else if (msg.type === 'aborted') {
                settled = true;
                rejectAbort();
            }
            else if (msg.type === 'complete' && msg.resultPath) {
                settled = true;
                const resultPath = msg.resultPath;
                void (async () => {
                    try {
                        const chunks = await readNdjsonFile(resultPath, signal);
                        cleanup();
                        resolve(chunks);
                    }
                    catch (err) {
                        cleanup();
                        reject(err instanceof Error ? err : new Error(String(err)));
                    }
                    finally {
                        // Always remove the artifact once its path is known, even when the
                        // read fails or is interrupted by an aborting signal.
                        await rm(resultPath, { force: true }).catch(() => undefined);
                    }
                })();
            }
            else if (msg.type === 'error') {
                settled = true;
                cleanup();
                reject(rehydrateRunnerError(msg, 'Isolated code chunking failed'));
            }
        });
        child.on('error', err => {
            settled = true;
            cleanup();
            reject(err);
        });
        child.on('close', code => {
            if (settled)
                return;
            cleanup();
            if (signal?.aborted) {
                rejectAbort();
                return;
            }
            if (code !== 0) {
                reject(new Error(`Isolated code chunk runner exited with code ${code}: ${stderr.trim()}`));
            }
            else {
                reject(new Error('Isolated code chunk runner exited without sending a result'));
            }
        });
        child.send({
            type: 'start',
            options: {
                store: storeOptions,
                strategy,
                chunk,
                cooperative: serializableCooperative(cooperative),
            },
        });
    });
}
//# sourceMappingURL=isolated.js.map