/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { exec, execSync, spawn } from 'node:child_process';
import os from 'node:os';
import path from 'node:path';
import fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import { quote, parse } from 'shell-quote';
import { USER_SETTINGS_DIR, SETTINGS_DIRECTORY_NAME, } from '../config/settings.js';
import { promisify } from 'node:util';
import { FatalSandboxError } from '@hoptrendy/hopcode-core';
import { randomBytes } from 'node:crypto';
import { writeStderrLine } from './stdioHelpers.js';
const execAsync = promisify(exec);
function getContainerPath(hostPath) {
    if (os.platform() !== 'win32') {
        return hostPath;
    }
    const withForwardSlashes = hostPath.replace(/\\/g, '/');
    const match = withForwardSlashes.match(/^([A-Z]):\/(.*)/i);
    if (match) {
        return `/${match[1].toLowerCase()}/${match[2]}`;
    }
    return hostPath;
}
const LOCAL_DEV_SANDBOX_IMAGE_NAME = 'hopcode-sandbox';
const SANDBOX_NETWORK_NAME = 'hopcode-sandbox';
const SANDBOX_PROXY_NAME = 'hopcode-sandbox-proxy';
const BUILTIN_SEATBELT_PROFILES = [
    'permissive-open',
    'permissive-closed',
    'permissive-proxied',
    'restrictive-open',
    'restrictive-closed',
    'restrictive-proxied',
];
/**
 * Determines whether the sandbox container should be run with the current user's UID and GID.
 * This is often necessary on Linux systems when using rootful Docker without userns-remap
 * configured, to avoid permission issues with
 * mounted volumes.
 *
 * The behavior is controlled by the `SANDBOX_SET_UID_GID` environment variable:
 * - If `SANDBOX_SET_UID_GID` is "1" or "true", this function returns `true`.
 * - If `SANDBOX_SET_UID_GID` is "0" or "false", this function returns `false`.
 * - If `SANDBOX_SET_UID_GID` is not set:
 *   - On Linux, it defaults to `true`.
 *   - On other OSes, it defaults to `false`.
 *
 * For more context on running Docker containers as non-root, see:
 * https://medium.com/redbubble/running-a-docker-container-as-a-non-root-user-7d2e00f8ee15
 *
 * @returns {Promise<boolean>} A promise that resolves to true if the current user's UID/GID should be used, false otherwise.
 */
async function shouldUseCurrentUserInSandbox() {
    const envVar = process.env['SANDBOX_SET_UID_GID']?.toLowerCase().trim();
    if (envVar === '1' || envVar === 'true') {
        return true;
    }
    if (envVar === '0' || envVar === 'false') {
        return false;
    }
    if (os.platform() === 'linux') {
        const debugEnv = [process.env['DEBUG'], process.env['DEBUG_MODE']].some((v) => v === 'true' || v === '1');
        if (debugEnv) {
            // Use stderr so it doesn't clutter normal STDOUT output (e.g. in `--prompt` runs).
            writeStderrLine('INFO: Using current user UID/GID in Linux sandbox. Set SANDBOX_SET_UID_GID=false to disable.');
        }
        return true;
    }
    return false;
}
// docker does not allow container names to contain ':' or '/', so we
// parse those out to shorten the name
function parseImageName(image) {
    const [fullName, tag] = image.split(':');
    const name = fullName.split('/').at(-1) ?? 'unknown-image';
    return tag ? `${name}-${tag}` : name;
}
function ports() {
    return (process.env['SANDBOX_PORTS'] ?? '')
        .split(',')
        .filter((p) => p.trim())
        .map((p) => p.trim());
}
function entrypoint(workdir, cliArgs) {
    const isWindows = os.platform() === 'win32';
    const containerWorkdir = getContainerPath(workdir);
    const shellCmds = [];
    const pathSeparator = isWindows ? ';' : ':';
    let pathSuffix = '';
    if (process.env['PATH']) {
        const paths = process.env['PATH'].split(pathSeparator);
        for (const p of paths) {
            const containerPath = getContainerPath(p);
            if (containerPath.toLowerCase().startsWith(containerWorkdir.toLowerCase())) {
                pathSuffix += `:${containerPath}`;
            }
        }
    }
    if (pathSuffix) {
        shellCmds.push(`export PATH="$PATH${pathSuffix}";`);
    }
    let pythonPathSuffix = '';
    if (process.env['PYTHONPATH']) {
        const paths = process.env['PYTHONPATH'].split(pathSeparator);
        for (const p of paths) {
            const containerPath = getContainerPath(p);
            if (containerPath.toLowerCase().startsWith(containerWorkdir.toLowerCase())) {
                pythonPathSuffix += `:${containerPath}`;
            }
        }
    }
    if (pythonPathSuffix) {
        shellCmds.push(`export PYTHONPATH="$PYTHONPATH${pythonPathSuffix}";`);
    }
    const projectSandboxBashrc = path.join(SETTINGS_DIRECTORY_NAME, 'sandbox.bashrc');
    if (fs.existsSync(projectSandboxBashrc)) {
        shellCmds.push(`source ${getContainerPath(projectSandboxBashrc)};`);
    }
    ports().forEach((p) => shellCmds.push(`socat TCP4-LISTEN:${p},bind=$(hostname -i),fork,reuseaddr TCP4:127.0.0.1:${p} 2> /dev/null &`));
    const quotedCliArgs = cliArgs.slice(2).map((arg) => quote([arg]));
    const cliCmd = process.env['NODE_ENV'] === 'development'
        ? process.env['DEBUG']
            ? 'npm run debug --'
            : 'npm rebuild && npm run start --'
        : process.env['DEBUG']
            ? `node --inspect-brk=0.0.0.0:${process.env['DEBUG_PORT'] || '9229'} $(which qwen)`
            : 'qwen';
    const args = [...shellCmds, cliCmd, ...quotedCliArgs];
    return ['bash', '-c', args.join(' ')];
}
export async function start_sandbox(config, nodeArgs = [], cliConfig, cliArgs = []) {
    if (config.command === 'sandbox-exec') {
        // disallow BUILD_SANDBOX
        if (process.env['BUILD_SANDBOX']) {
            throw new FatalSandboxError('Cannot BUILD_SANDBOX when using macOS Seatbelt');
        }
        const profile = (process.env['SEATBELT_PROFILE'] ??= 'permissive-open');
        let profileFile = fileURLToPath(new URL(`sandbox-macos-${profile}.sb`, import.meta.url));
        // if profile name is not recognized, then look for file under project settings directory
        if (!BUILTIN_SEATBELT_PROFILES.includes(profile)) {
            profileFile = path.join(SETTINGS_DIRECTORY_NAME, `sandbox-macos-${profile}.sb`);
        }
        if (!fs.existsSync(profileFile)) {
            throw new FatalSandboxError(`Missing macos seatbelt profile file '${profileFile}'`);
        }
        // Log on STDERR so it doesn't clutter the output on STDOUT
        writeStderrLine(`using macos seatbelt (profile: ${profile}) ...`);
        // if DEBUG is set, convert to --inspect-brk in NODE_OPTIONS
        const nodeOptions = [
            ...(process.env['DEBUG'] ? ['--inspect-brk'] : []),
            ...nodeArgs,
        ].join(' ');
        const args = [
            '-D',
            `TARGET_DIR=${fs.realpathSync(process.cwd())}`,
            '-D',
            `TMP_DIR=${fs.realpathSync(os.tmpdir())}`,
            '-D',
            `HOME_DIR=${fs.realpathSync(os.homedir())}`,
            '-D',
            `CACHE_DIR=${fs.realpathSync(execSync(`getconf DARWIN_USER_CACHE_DIR`).toString().trim())}`,
        ];
        // Add included directories from the workspace context
        // Always add 5 INCLUDE_DIR parameters to ensure .sb files can reference them
        const MAX_INCLUDE_DIRS = 5;
        const targetDir = fs.realpathSync(cliConfig?.getTargetDir() || '');
        const includedDirs = [];
        if (cliConfig) {
            const workspaceContext = cliConfig.getWorkspaceContext();
            const directories = workspaceContext.getDirectories();
            // Filter out TARGET_DIR
            for (const dir of directories) {
                const realDir = fs.realpathSync(dir);
                if (realDir !== targetDir) {
                    includedDirs.push(realDir);
                }
            }
        }
        for (let i = 0; i < MAX_INCLUDE_DIRS; i++) {
            let dirPath = '/dev/null'; // Default to a safe path that won't cause issues
            if (i < includedDirs.length) {
                dirPath = includedDirs[i];
            }
            args.push('-D', `INCLUDE_DIR_${i}=${dirPath}`);
        }
        const finalArgv = cliArgs;
        args.push('-f', profileFile, 'sh', '-c', [
            `SANDBOX=sandbox-exec`,
            `NODE_OPTIONS="${nodeOptions}"`,
            ...finalArgv.map((arg) => quote([arg])),
        ].join(' '));
        // start and set up proxy if HOPCODE_SANDBOX_PROXY_COMMAND is set
        const proxyCommand = process.env['HOPCODE_SANDBOX_PROXY_COMMAND'];
        let proxyProcess = undefined;
        let sandboxProcess = undefined;
        const sandboxEnv = { ...process.env };
        if (proxyCommand) {
            const proxy = process.env['HTTPS_PROXY'] ||
                process.env['https_proxy'] ||
                process.env['HTTP_PROXY'] ||
                process.env['http_proxy'] ||
                'http://localhost:8877';
            sandboxEnv['HTTPS_PROXY'] = proxy;
            sandboxEnv['https_proxy'] = proxy; // lower-case can be required, e.g. for curl
            sandboxEnv['HTTP_PROXY'] = proxy;
            sandboxEnv['http_proxy'] = proxy;
            const noProxy = process.env['NO_PROXY'] || process.env['no_proxy'];
            if (noProxy) {
                sandboxEnv['NO_PROXY'] = noProxy;
                sandboxEnv['no_proxy'] = noProxy;
            }
            // Note: CodeQL flags this as js/shell-command-injection-from-environment.
            // This is intentional - CLI tool executes user-provided proxy commands.
            proxyProcess = spawn('bash', ['-c', proxyCommand], {
                stdio: ['ignore', 'pipe', 'pipe'],
                detached: true,
            });
            // install handlers to stop proxy on exit/signal
            const stopProxy = () => {
                writeStderrLine('stopping proxy ...');
                if (proxyProcess?.pid) {
                    process.kill(-proxyProcess.pid, 'SIGTERM');
                }
            };
            process.on('exit', stopProxy);
            process.on('SIGINT', stopProxy);
            process.on('SIGTERM', stopProxy);
            // commented out as it disrupts ink rendering
            // proxyProcess.stdout?.on('data', (data) => {
            //   console.info(data.toString());
            // });
            proxyProcess.stderr?.on('data', (data) => {
                writeStderrLine(data.toString());
            });
            proxyProcess.on('close', (code, signal) => {
                if (sandboxProcess?.pid) {
                    process.kill(-sandboxProcess.pid, 'SIGTERM');
                }
                throw new FatalSandboxError(`Proxy command '${proxyCommand}' exited with code ${code}, signal ${signal}`);
            });
            writeStderrLine('waiting for proxy to start ...');
            await execAsync(`until timeout 0.25 curl -s http://localhost:8877; do sleep 0.25; done`);
        }
        // spawn child and let it inherit stdio
        process.stdin.pause();
        sandboxProcess = spawn(config.command, args, {
            stdio: 'inherit',
        });
        return new Promise((resolve, reject) => {
            sandboxProcess?.on('error', reject);
            sandboxProcess?.on('close', (code) => {
                process.stdin.resume();
                resolve(code ?? 1);
            });
        });
    }
    writeStderrLine(`hopping into sandbox (command: ${config.command}) ...`);
    // determine full path for hopcode-code to distinguish linked vs installed setting
    const gcPath = fs.realpathSync(process.argv[1]);
    const projectSandboxDockerfile = path.join(SETTINGS_DIRECTORY_NAME, 'sandbox.Dockerfile');
    const isCustomProjectSandbox = fs.existsSync(projectSandboxDockerfile);
    const image = config.image;
    const workdir = path.resolve(process.cwd());
    const containerWorkdir = getContainerPath(workdir);
    // if BUILD_SANDBOX is set, then call scripts/build_sandbox.js under hopcode-code repo
    //
    // note this can only be done with binary linked from hopcode-code repo
    if (process.env['BUILD_SANDBOX']) {
        if (!gcPath.includes('hopcode/packages/')) {
            throw new FatalSandboxError('Cannot build sandbox using installed HopCode binary; ' +
                'run `npm link ./packages/cli` under HopCode-cli repo to switch to linked binary.');
        }
        else {
            writeStderrLine('building sandbox ...');
            const gcRoot = gcPath.split('/packages/')[0];
            // if project folder has sandbox.Dockerfile under project settings folder, use that
            let buildArgs = '';
            const projectSandboxDockerfile = path.join(SETTINGS_DIRECTORY_NAME, 'sandbox.Dockerfile');
            if (isCustomProjectSandbox) {
                writeStderrLine(`using ${projectSandboxDockerfile} for sandbox`);
                buildArgs += `-f ${path.resolve(projectSandboxDockerfile)} -i ${image}`;
            }
            execSync(`cd ${gcRoot} && node scripts/build_sandbox.js -s ${buildArgs}`, {
                stdio: 'inherit',
                env: {
                    ...process.env,
                    HOPCODE_SANDBOX: config.command, // in case sandbox is enabled via flags (see config.ts under cli package)
                },
            });
        }
    }
    // stop if image is missing
    if (!(await ensureSandboxImageIsPresent(config.command, image))) {
        const remedy = image === LOCAL_DEV_SANDBOX_IMAGE_NAME
            ? 'Try running `npm run build:all` or `npm run build:sandbox` under the hopcode repo to build it locally, or check the image name and your network connection.'
            : 'Please check the image name, your network connection, or notify the HopCode team if the issue persists.';
        throw new FatalSandboxError(`Sandbox image '${image}' is missing or could not be pulled. ${remedy}`);
    }
    // use interactive mode and auto-remove container on exit
    // run init binary inside container to forward signals & reap zombies
    const args = ['run', '-i', '--rm', '--init', '--workdir', containerWorkdir];
    // add custom flags from SANDBOX_FLAGS
    if (process.env['SANDBOX_FLAGS']) {
        const flags = parse(process.env['SANDBOX_FLAGS'], process.env).filter((f) => typeof f === 'string');
        args.push(...flags);
    }
    // add TTY only if stdin is TTY as well, i.e. for piped input don't init TTY in container
    if (process.stdin.isTTY) {
        args.push('-t');
    }
    // allow access to host.docker.internal
    args.push('--add-host', 'host.docker.internal:host-gateway');
    // mount current directory as working directory in sandbox (set via --workdir)
    args.push('--volume', `${workdir}:${containerWorkdir}`);
    // mount user settings directory inside container, after creating if missing
    // note user/home changes inside sandbox and we mount at BOTH paths for consistency
    const userSettingsDirOnHost = USER_SETTINGS_DIR;
    const userSettingsDirInSandbox = getContainerPath(`/home/node/${SETTINGS_DIRECTORY_NAME}`);
    if (!fs.existsSync(userSettingsDirOnHost)) {
        fs.mkdirSync(userSettingsDirOnHost);
    }
    args.push('--volume', `${userSettingsDirOnHost}:${userSettingsDirInSandbox}`);
    if (userSettingsDirInSandbox !== userSettingsDirOnHost) {
        args.push('--volume', `${userSettingsDirOnHost}:${getContainerPath(userSettingsDirOnHost)}`);
    }
    // mount os.tmpdir() as os.tmpdir() inside container
    args.push('--volume', `${os.tmpdir()}:${getContainerPath(os.tmpdir())}`);
    // mount gcloud config directory if it exists
    const gcloudConfigDir = path.join(os.homedir(), '.config', 'gcloud');
    if (fs.existsSync(gcloudConfigDir)) {
        args.push('--volume', `${gcloudConfigDir}:${getContainerPath(gcloudConfigDir)}:ro`);
    }
    // mount ADC file if GOOGLE_APPLICATION_CREDENTIALS is set
    if (process.env['GOOGLE_APPLICATION_CREDENTIALS']) {
        const adcFile = process.env['GOOGLE_APPLICATION_CREDENTIALS'];
        if (fs.existsSync(adcFile)) {
            args.push('--volume', `${adcFile}:${getContainerPath(adcFile)}:ro`);
            args.push('--env', `GOOGLE_APPLICATION_CREDENTIALS=${getContainerPath(adcFile)}`);
        }
    }
    // mount paths listed in SANDBOX_MOUNTS
    if (process.env['SANDBOX_MOUNTS']) {
        for (let mount of process.env['SANDBOX_MOUNTS'].split(',')) {
            if (mount.trim()) {
                // parse mount as from:to:opts
                let [from, to, opts] = mount.trim().split(':');
                to = to || from; // default to mount at same path inside container
                opts = opts || 'ro'; // default to read-only
                mount = `${from}:${to}:${opts}`;
                // check that from path is absolute
                if (!path.isAbsolute(from)) {
                    throw new FatalSandboxError(`Path '${from}' listed in SANDBOX_MOUNTS must be absolute`);
                }
                // check that from path exists on host
                if (!fs.existsSync(from)) {
                    throw new FatalSandboxError(`Missing mount path '${from}' listed in SANDBOX_MOUNTS`);
                }
                writeStderrLine(`SANDBOX_MOUNTS: ${from} -> ${to} (${opts})`);
                args.push('--volume', mount);
            }
        }
    }
    // expose env-specified ports on the sandbox
    ports().forEach((p) => args.push('--publish', `${p}:${p}`));
    // if DEBUG is set, expose debugging port
    if (process.env['DEBUG']) {
        const debugPort = process.env['DEBUG_PORT'] || '9229';
        args.push(`--publish`, `${debugPort}:${debugPort}`);
    }
    // copy proxy environment variables, replacing localhost with SANDBOX_PROXY_NAME
    // copy as both upper-case and lower-case as is required by some utilities
    // HOPCODE_SANDBOX_PROXY_COMMAND implies HTTPS_PROXY unless HTTP_PROXY is set
    const proxyCommand = process.env['HOPCODE_SANDBOX_PROXY_COMMAND'];
    if (proxyCommand) {
        let proxy = process.env['HTTPS_PROXY'] ||
            process.env['https_proxy'] ||
            process.env['HTTP_PROXY'] ||
            process.env['http_proxy'] ||
            'http://localhost:8877';
        proxy = proxy.replace('localhost', SANDBOX_PROXY_NAME);
        if (proxy) {
            args.push('--env', `HTTPS_PROXY=${proxy}`);
            args.push('--env', `https_proxy=${proxy}`); // lower-case can be required, e.g. for curl
            args.push('--env', `HTTP_PROXY=${proxy}`);
            args.push('--env', `http_proxy=${proxy}`);
        }
        const noProxy = process.env['NO_PROXY'] || process.env['no_proxy'];
        if (noProxy) {
            args.push('--env', `NO_PROXY=${noProxy}`);
            args.push('--env', `no_proxy=${noProxy}`);
        }
        // if using proxy, switch to internal networking through proxy
        if (proxy) {
            execSync(`${config.command} network inspect ${SANDBOX_NETWORK_NAME} || ${config.command} network create --internal ${SANDBOX_NETWORK_NAME}`);
            args.push('--network', SANDBOX_NETWORK_NAME);
            // if proxy command is set, create a separate network w/ host access (i.e. non-internal)
            // we will run proxy in its own container connected to both host network and internal network
            // this allows proxy to work even on rootless podman on macos with host<->vm<->container isolation
            if (proxyCommand) {
                execSync(`${config.command} network inspect ${SANDBOX_PROXY_NAME} || ${config.command} network create ${SANDBOX_PROXY_NAME}`);
            }
        }
    }
    // name container after image, plus random suffix to avoid conflicts
    const imageName = parseImageName(image);
    const isIntegrationTest = process.env['HOPCODE_INTEGRATION_TEST'] === 'true';
    let containerName;
    if (isIntegrationTest) {
        containerName = `hopcode-integration-test-${randomBytes(4).toString('hex')}`;
        writeStderrLine(`ContainerName: ${containerName}`);
    }
    else {
        let index = 0;
        const containerNameCheck = execSync(`${config.command} ps -a --format "{{.Names}}"`)
            .toString()
            .trim();
        while (containerNameCheck.includes(`${imageName}-${index}`)) {
            index++;
        }
        containerName = `${imageName}-${index}`;
        writeStderrLine(`ContainerName (regular): ${containerName}`);
    }
    args.push('--name', containerName, '--hostname', containerName);
    // copy HOPCODE_TEST_VAR for integration tests
    if (process.env['HOPCODE_TEST_VAR']) {
        args.push('--env', `HOPCODE_TEST_VAR=${process.env['HOPCODE_TEST_VAR']}`);
    }
    // copy GEMINI_API_KEY(s)
    if (process.env['GEMINI_API_KEY']) {
        args.push('--env', `GEMINI_API_KEY=${process.env['GEMINI_API_KEY']}`);
    }
    if (process.env['GOOGLE_API_KEY']) {
        args.push('--env', `GOOGLE_API_KEY=${process.env['GOOGLE_API_KEY']}`);
    }
    // copy OPENAI_API_KEY and related env vars for HopCode
    if (process.env['OPENAI_API_KEY']) {
        args.push('--env', `OPENAI_API_KEY=${process.env['OPENAI_API_KEY']}`);
    }
    if (process.env['OPENAI_BASE_URL']) {
        args.push('--env', `OPENAI_BASE_URL=${process.env['OPENAI_BASE_URL']}`);
    }
    if (process.env['OPENAI_MODEL']) {
        args.push('--env', `OPENAI_MODEL=${process.env['OPENAI_MODEL']}`);
    }
    // copy GOOGLE_GENAI_USE_VERTEXAI
    if (process.env['GOOGLE_GENAI_USE_VERTEXAI']) {
        args.push('--env', `GOOGLE_GENAI_USE_VERTEXAI=${process.env['GOOGLE_GENAI_USE_VERTEXAI']}`);
    }
    // copy GOOGLE_GENAI_USE_GCA
    if (process.env['GOOGLE_GENAI_USE_GCA']) {
        args.push('--env', `GOOGLE_GENAI_USE_GCA=${process.env['GOOGLE_GENAI_USE_GCA']}`);
    }
    // copy GOOGLE_CLOUD_PROJECT
    if (process.env['GOOGLE_CLOUD_PROJECT']) {
        args.push('--env', `GOOGLE_CLOUD_PROJECT=${process.env['GOOGLE_CLOUD_PROJECT']}`);
    }
    // copy GOOGLE_CLOUD_LOCATION
    if (process.env['GOOGLE_CLOUD_LOCATION']) {
        args.push('--env', `GOOGLE_CLOUD_LOCATION=${process.env['GOOGLE_CLOUD_LOCATION']}`);
    }
    // copy GEMINI_MODEL
    if (process.env['GEMINI_MODEL']) {
        args.push('--env', `GEMINI_MODEL=${process.env['GEMINI_MODEL']}`);
    }
    // copy TERM and COLORTERM to try to maintain terminal setup
    if (process.env['TERM']) {
        args.push('--env', `TERM=${process.env['TERM']}`);
    }
    if (process.env['COLORTERM']) {
        args.push('--env', `COLORTERM=${process.env['COLORTERM']}`);
    }
    // Pass through IDE mode environment variables
    for (const envVar of [
        'HOPCODE_IDE_SERVER_PORT',
        'HOPCODE_IDE_WORKSPACE_PATH',
        'TERM_PROGRAM',
    ]) {
        if (process.env[envVar]) {
            args.push('--env', `${envVar}=${process.env[envVar]}`);
        }
    }
    // copy VIRTUAL_ENV if under working directory
    // also mount-replace VIRTUAL_ENV directory with <project_settings>/sandbox.venv
    // sandbox can then set up this new VIRTUAL_ENV directory using sandbox.bashrc (see below)
    // directory will be empty if not set up, which is still preferable to having host binaries
    if (process.env['VIRTUAL_ENV']?.toLowerCase().startsWith(workdir.toLowerCase())) {
        const sandboxVenvPath = path.resolve(SETTINGS_DIRECTORY_NAME, 'sandbox.venv');
        if (!fs.existsSync(sandboxVenvPath)) {
            fs.mkdirSync(sandboxVenvPath, { recursive: true });
        }
        args.push('--volume', `${sandboxVenvPath}:${getContainerPath(process.env['VIRTUAL_ENV'])}`);
        args.push('--env', `VIRTUAL_ENV=${getContainerPath(process.env['VIRTUAL_ENV'])}`);
    }
    // copy additional environment variables from SANDBOX_ENV
    if (process.env['SANDBOX_ENV']) {
        for (let env of process.env['SANDBOX_ENV'].split(',')) {
            if ((env = env.trim())) {
                if (env.includes('=')) {
                    writeStderrLine(`SANDBOX_ENV: ${env}`);
                    args.push('--env', env);
                }
                else {
                    throw new FatalSandboxError('SANDBOX_ENV must be a comma-separated list of key=value pairs');
                }
            }
        }
    }
    // copy NODE_OPTIONS
    const existingNodeOptions = process.env['NODE_OPTIONS'] || '';
    const allNodeOptions = [
        ...(existingNodeOptions ? [existingNodeOptions] : []),
        ...nodeArgs,
    ].join(' ');
    if (allNodeOptions.length > 0) {
        args.push('--env', `NODE_OPTIONS="${allNodeOptions}"`);
    }
    // set SANDBOX as container name
    args.push('--env', `SANDBOX=${containerName}`);
    // for podman only, use empty --authfile to skip unnecessary auth refresh overhead
    if (config.command === 'podman') {
        const emptyAuthFilePath = path.join(os.tmpdir(), 'empty_auth.json');
        fs.writeFileSync(emptyAuthFilePath, '{}', 'utf-8');
        args.push('--authfile', emptyAuthFilePath);
    }
    // Determine if the current user's UID/GID should be passed to the sandbox.
    // See shouldUseCurrentUserInSandbox for more details.
    let userFlag = '';
    const finalEntrypoint = entrypoint(workdir, cliArgs);
    // Check if we should use current user's UID/GID in sandbox
    // In integration test mode, we still respect SANDBOX_SET_UID_GID to allow
    // tests that need to access host's ~/.hopcode (e.g., --resume functionality)
    const useCurrentUser = await shouldUseCurrentUserInSandbox();
    if (useCurrentUser) {
        // SANDBOX_SET_UID_GID is enabled: create user with host's UID/GID
        // This includes integration test mode with SANDBOX_SET_UID_GID=true,
        // allowing tests that need to access host's ~/.hopcode (e.g., --resume) to work.
        // For the user-creation logic to work, the container must start as root.
        // The entrypoint script then handles dropping privileges to the correct user.
        args.push('--user', 'root');
        const uid = execSync('id -u').toString().trim();
        const gid = execSync('id -g').toString().trim();
        // Instead of passing --user to the main sandbox container, we let it
        // start as root, then create a user with the host's UID/GID, and
        // finally switch to that user to run the hopcode process. This is
        // necessary on Linux to ensure the user exists within the
        // container's /etc/passwd file, which is required by os.userInfo().
        const username = 'qwen';
        const homeDir = getContainerPath(os.homedir());
        const setupUserCommands = [
            // Use -f with groupadd to avoid errors if the group already exists.
            `groupadd -f -g ${gid} ${username}`,
            // Create user only if it doesn't exist. Use -o for non-unique UID.
            `id -u ${username} &>/dev/null || useradd -o -u ${uid} -g ${gid} -d ${homeDir} -s /bin/bash ${username}`,
        ].join(' && ');
        const originalCommand = finalEntrypoint[2];
        const escapedOriginalCommand = originalCommand.replace(/'/g, "'\\''");
        // Use `su -p` to preserve the environment.
        const suCommand = `su -p ${username} -c '${escapedOriginalCommand}'`;
        // The entrypoint is always `['bash', '-c', '<command>']`, so we modify the command part.
        finalEntrypoint[2] = `${setupUserCommands} && ${suCommand}`;
        // We still need userFlag for the simpler proxy container, which does not have this issue.
        userFlag = `--user ${uid}:${gid}`;
        // When forcing a UID in the sandbox, $HOME can be reset to '/', so we copy $HOME as well.
        args.push('--env', `HOME=${os.homedir()}`);
    }
    else if (isIntegrationTest) {
        // Integration test mode with UID/GID matching disabled: use root
        args.push('--user', 'root');
        userFlag = '--user root';
    }
    // else: non-IT mode with UID/GID matching disabled - use image default user (node)
    // push container image name
    args.push(image);
    // push container entrypoint (including args)
    args.push(...finalEntrypoint);
    // start and set up proxy if HOPCODE_SANDBOX_PROXY_COMMAND is set
    let proxyProcess = undefined;
    let sandboxProcess = undefined;
    if (proxyCommand) {
        // run proxyCommand in its own container
        const proxyContainerCommand = `${config.command} run --rm --init ${userFlag} --name ${SANDBOX_PROXY_NAME} --network ${SANDBOX_PROXY_NAME} -p 8877:8877 -v ${process.cwd()}:${workdir} --workdir ${workdir} ${image} ${proxyCommand}`;
        const isWindows = os.platform() === 'win32';
        const proxyShell = isWindows ? 'cmd.exe' : 'bash';
        const proxyShellArgs = isWindows
            ? ['/c', proxyContainerCommand]
            : ['-c', proxyContainerCommand];
        // Note: CodeQL flags this as js/shell-command-injection-from-environment.
        // This is intentional - CLI tool executes user-provided proxy commands in container.
        proxyProcess = spawn(proxyShell, proxyShellArgs, {
            stdio: ['ignore', 'pipe', 'pipe'],
            detached: true,
        });
        // install handlers to stop proxy on exit/signal
        const stopProxy = () => {
            writeStderrLine('stopping proxy container ...');
            execSync(`${config.command} rm -f ${SANDBOX_PROXY_NAME}`);
        };
        process.on('exit', stopProxy);
        process.on('SIGINT', stopProxy);
        process.on('SIGTERM', stopProxy);
        // commented out as it disrupts ink rendering
        // proxyProcess.stdout?.on('data', (data) => {
        //   console.info(data.toString());
        // });
        proxyProcess.stderr?.on('data', (data) => {
            writeStderrLine(data.toString().trim());
        });
        proxyProcess.on('close', (code, signal) => {
            if (sandboxProcess?.pid) {
                process.kill(-sandboxProcess.pid, 'SIGTERM');
            }
            throw new FatalSandboxError(`Proxy container command '${proxyContainerCommand}' exited with code ${code}, signal ${signal}`);
        });
        writeStderrLine('waiting for proxy to start ...');
        await execAsync(`until timeout 0.25 curl -s http://localhost:8877; do sleep 0.25; done`);
        // connect proxy container to sandbox network
        // (workaround for older versions of docker that don't support multiple --network args)
        await execAsync(`${config.command} network connect ${SANDBOX_NETWORK_NAME} ${SANDBOX_PROXY_NAME}`);
    }
    // spawn child and let it inherit stdio
    process.stdin.pause();
    sandboxProcess = spawn(config.command, args, {
        stdio: 'inherit',
    });
    return new Promise((resolve, reject) => {
        sandboxProcess.on('error', (err) => {
            writeStderrLine(`Sandbox process error: ${err}`);
            reject(err);
        });
        sandboxProcess?.on('close', (code, signal) => {
            process.stdin.resume();
            if (code !== 0 && code !== null) {
                writeStderrLine(`Sandbox process exited with code: ${code}, signal: ${signal}`);
            }
            resolve(code ?? 1);
        });
    });
}
// Helper functions to ensure sandbox image is present
async function imageExists(sandbox, image) {
    return new Promise((resolve) => {
        const args = ['images', '-q', image];
        const checkProcess = spawn(sandbox, args);
        let stdoutData = '';
        if (checkProcess.stdout) {
            checkProcess.stdout.on('data', (data) => {
                stdoutData += data.toString();
            });
        }
        checkProcess.on('error', (err) => {
            writeStderrLine(`Failed to start '${sandbox}' command for image check: ${err.message}`);
            resolve(false);
        });
        checkProcess.on('close', (code) => {
            // Non-zero code might indicate docker daemon not running, etc.
            // The primary success indicator is non-empty stdoutData.
            if (code !== 0) {
                // console.warn(`'${sandbox} images -q ${image}' exited with code ${code}.`);
            }
            resolve(stdoutData.trim() !== '');
        });
    });
}
async function pullImage(sandbox, image) {
    writeStderrLine(`Attempting to pull image ${image} using ${sandbox}...`);
    return new Promise((resolve) => {
        const args = ['pull', image];
        const pullProcess = spawn(sandbox, args, { stdio: 'pipe' });
        let stderrData = '';
        const onStdoutData = (data) => {
            writeStderrLine(data.toString().trim()); // Show pull progress
        };
        const onStderrData = (data) => {
            stderrData += data.toString();
            writeStderrLine(data.toString().trim()); // Show pull errors/info from the command itself
        };
        const onError = (err) => {
            writeStderrLine(`Failed to start '${sandbox} pull ${image}' command: ${err.message}`);
            cleanup();
            resolve(false);
        };
        const onClose = (code) => {
            if (code === 0) {
                writeStderrLine(`Successfully pulled image ${image}.`);
                cleanup();
                resolve(true);
            }
            else {
                writeStderrLine(`Failed to pull image ${image}. '${sandbox} pull ${image}' exited with code ${code}.`);
                if (stderrData.trim()) {
                    // Details already printed by the stderr listener above
                }
                cleanup();
                resolve(false);
            }
        };
        const cleanup = () => {
            if (pullProcess.stdout) {
                pullProcess.stdout.removeListener('data', onStdoutData);
            }
            if (pullProcess.stderr) {
                pullProcess.stderr.removeListener('data', onStderrData);
            }
            pullProcess.removeListener('error', onError);
            pullProcess.removeListener('close', onClose);
            if (pullProcess.connected) {
                pullProcess.disconnect();
            }
        };
        if (pullProcess.stdout) {
            pullProcess.stdout.on('data', onStdoutData);
        }
        if (pullProcess.stderr) {
            pullProcess.stderr.on('data', onStderrData);
        }
        pullProcess.on('error', onError);
        pullProcess.on('close', onClose);
    });
}
async function ensureSandboxImageIsPresent(sandbox, image) {
    writeStderrLine(`Checking for sandbox image: ${image}`);
    if (await imageExists(sandbox, image)) {
        writeStderrLine(`Sandbox image ${image} found locally.`);
        return true;
    }
    writeStderrLine(`Sandbox image ${image} not found locally.`);
    if (image === LOCAL_DEV_SANDBOX_IMAGE_NAME) {
        // user needs to build the image themselves
        return false;
    }
    if (await pullImage(sandbox, image)) {
        // After attempting to pull, check again to be certain
        if (await imageExists(sandbox, image)) {
            writeStderrLine(`Sandbox image ${image} is now available after pulling.`);
            return true;
        }
        else {
            writeStderrLine(`Sandbox image ${image} still not found after a pull attempt. This might indicate an issue with the image name or registry, or the pull command reported success but failed to make the image available.`);
            return false;
        }
    }
    writeStderrLine(`Failed to obtain sandbox image ${image} after check and pull attempt.`);
    return false; // Pull command failed or image still not present
}
//# sourceMappingURL=sandbox.js.map