import { appConfig } from '../../config/app.js';
import { ErrorFactory } from '../../exceptions/ZintrustError.js';
import { spawn } from '../../node-singletons/child-process.js';
import { existsSync } from '../../node-singletons/fs.js';
import * as path from '../../node-singletons/path.js';
const getExitCode = (exitCode, signal) => {
    if (typeof exitCode === 'number')
        return exitCode;
    if (signal === 'SIGINT' || signal === 'SIGTERM')
        return 0;
    return 1;
};
const resolveLocalBin = (command, cwd) => {
    // If command is already a path, leave it alone.
    if (command.includes('/') || command.includes('\\'))
        return command;
    const binDir = path.join(cwd, 'node_modules', '.bin');
    const candidates = process.platform === 'win32'
        ? [
            path.join(binDir, `${command}.cmd`),
            path.join(binDir, `${command}.exe`),
            path.join(binDir, `${command}.bat`),
            path.join(binDir, command),
        ]
        : [path.join(binDir, command)];
    for (const candidate of candidates) {
        if (existsSync(candidate))
            return candidate;
    }
    return command;
};
const buildCommandNotFoundMessage = (command) => {
    if (command === 'tsx') {
        return [
            "Error: 'tsx' not found on PATH.",
            'Install it in the project with "npm install -D tsx".',
            'If you want a machine-wide fallback, install it globally with "npm install -g tsx".',
        ].join(' ');
    }
    return `Error: '${command}' not found on PATH.`;
};
export const SpawnUtil = Object.freeze({
    async spawnAndWait(input) {
        const cwd = input.cwd ?? process.cwd();
        const resolvedCommand = resolveLocalBin(input.command, cwd);
        const child = spawn(resolvedCommand, input.args, {
            cwd,
            env: input.env ?? appConfig.getSafeEnv(),
            stdio: 'inherit',
        });
        // In interactive shells, the foreground process group already receives SIGINT
        // (and often SIGTERM) so forwarding can cause duplicates. `tsx watch` is
        // especially sensitive here and can print "Previous process hasn't exited yet. Force killing...".
        const forwardSignals = typeof input.forwardSignals === 'boolean' ? input.forwardSignals : !process.stdin.isTTY;
        const forwardSignal = (signal) => {
            try {
                child.kill(signal);
            }
            catch (error) {
                const wrapped = ErrorFactory.createTryCatchError('Failed to forward signal to child process', error);
                // Best-effort logging; then rethrow (tests/assertions rely on this behavior).
                try {
                    process.stderr.write(`${String(wrapped.message)}\n`);
                }
                catch {
                    // ignore
                }
                throw wrapped;
            }
        };
        const onSigint = () => {
            if (forwardSignals) {
                forwardSignal('SIGINT');
            }
            // If not forwarding, handle to prevent the parent from exiting before the child
            // finishes its graceful shutdown (child receives SIGINT directly in TTY).
        };
        const onSigterm = () => {
            if (forwardSignals) {
                forwardSignal('SIGTERM');
            }
            // Same rationale as SIGINT: keep parent alive while waiting for child to exit.
        };
        process.on('SIGINT', onSigint);
        process.on('SIGTERM', onSigterm);
        try {
            const result = await new Promise((resolve, reject) => {
                child.once('error', (error) => {
                    reject(error);
                });
                child.once('close', (code, signal) => {
                    resolve({ exitCode: code, signal });
                });
            });
            return getExitCode(result.exitCode, result.signal);
        }
        catch (error) {
            const code = error?.code;
            if (code === 'ENOENT') {
                throw ErrorFactory.createCliError(buildCommandNotFoundMessage(input.command));
            }
            throw ErrorFactory.createTryCatchError('Failed to spawn child process', error);
        }
        finally {
            process.off('SIGINT', onSigint);
            process.off('SIGTERM', onSigterm);
        }
    },
});
