/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { getPty } from '../utils/getPty.js';
import { spawn as cpSpawn } from 'child_process';
import { TextDecoder } from 'util';
import os from 'os';
import { getCachedEncodingForBuffer } from '../utils/systemEncoding.js';
import { isBinary } from '../utils/textUtils.js';
import pkg from '@xterm/headless';
import stripAnsi from 'strip-ansi';
const { Terminal } = pkg;
const SIGKILL_TIMEOUT_MS = 200;
// @ts-expect-error getFullText is not a public API.
const getFullText = (terminal) => {
    const buffer = terminal.buffer.active;
    const lines = [];
    for (let i = 0; i < buffer.length; i++) {
        const line = buffer.getLine(i);
        lines.push(line ? line.translateToString(true) : '');
    }
    return lines.join('\n').trim();
};
/**
 * A centralized service for executing shell commands with robust process
 * management, cross-platform compatibility, and streaming output capabilities.
 *
 */
export class ShellExecutionService {
    /**
     * Executes a shell command using `node-pty`, capturing all output and lifecycle events.
     *
     * @param commandToExecute The exact command string to run.
     * @param cwd The working directory to execute the command in.
     * @param onOutputEvent A callback for streaming structured events about the execution, including data chunks and status updates.
     * @param abortSignal An AbortSignal to terminate the process and its children.
     * @returns An object containing the process ID (pid) and a promise that
     *          resolves with the complete execution result.
     */
    static async execute(commandToExecute, cwd, onOutputEvent, abortSignal, shouldUseNodePty, terminalColumns, terminalRows) {
        if (shouldUseNodePty) {
            const ptyInfo = await getPty();
            if (ptyInfo) {
                try {
                    return this.executeWithPty(commandToExecute, cwd, onOutputEvent, abortSignal, terminalColumns, terminalRows, ptyInfo);
                }
                catch (_e) {
                    // Fallback to child_process
                }
            }
        }
        return this.childProcessFallback(commandToExecute, cwd, onOutputEvent, abortSignal);
    }
    static childProcessFallback(commandToExecute, cwd, onOutputEvent, abortSignal) {
        try {
            const isWindows = os.platform() === 'win32';
            const child = cpSpawn(commandToExecute, [], {
                cwd,
                stdio: ['ignore', 'pipe', 'pipe'],
                shell: isWindows ? true : 'bash',
                detached: !isWindows,
                env: {
                    ...process.env,
                    QWEN_CODE: '1',
                    TERM: 'xterm-256color',
                    PAGER: 'cat',
                },
            });
            const result = new Promise((resolve) => {
                let stdoutDecoder = null;
                let stderrDecoder = null;
                let stdout = '';
                let stderr = '';
                const outputChunks = [];
                let error = null;
                let exited = false;
                let isStreamingRawContent = true;
                const MAX_SNIFF_SIZE = 4096;
                let sniffedBytes = 0;
                const handleOutput = (data, stream) => {
                    if (!stdoutDecoder || !stderrDecoder) {
                        const encoding = getCachedEncodingForBuffer(data);
                        try {
                            stdoutDecoder = new TextDecoder(encoding);
                            stderrDecoder = new TextDecoder(encoding);
                        }
                        catch {
                            stdoutDecoder = new TextDecoder('utf-8');
                            stderrDecoder = new TextDecoder('utf-8');
                        }
                    }
                    outputChunks.push(data);
                    if (isStreamingRawContent && sniffedBytes < MAX_SNIFF_SIZE) {
                        const sniffBuffer = Buffer.concat(outputChunks.slice(0, 20));
                        sniffedBytes = sniffBuffer.length;
                        if (isBinary(sniffBuffer)) {
                            isStreamingRawContent = false;
                            onOutputEvent({ type: 'binary_detected' });
                        }
                    }
                    const decoder = stream === 'stdout' ? stdoutDecoder : stderrDecoder;
                    const decodedChunk = decoder.decode(data, { stream: true });
                    const strippedChunk = stripAnsi(decodedChunk);
                    if (stream === 'stdout') {
                        stdout += strippedChunk;
                    }
                    else {
                        stderr += strippedChunk;
                    }
                    if (isStreamingRawContent) {
                        onOutputEvent({ type: 'data', chunk: strippedChunk });
                    }
                    else {
                        const totalBytes = outputChunks.reduce((sum, chunk) => sum + chunk.length, 0);
                        onOutputEvent({
                            type: 'binary_progress',
                            bytesReceived: totalBytes,
                        });
                    }
                };
                const handleExit = (code, signal) => {
                    const { finalBuffer } = cleanup();
                    // Ensure we don't add an extra newline if stdout already ends with one.
                    const separator = stdout.endsWith('\n') ? '' : '\n';
                    const combinedOutput = stdout + (stderr ? (stdout ? separator : '') + stderr : '');
                    resolve({
                        rawOutput: finalBuffer,
                        output: combinedOutput.trim(),
                        exitCode: code,
                        signal: signal ? os.constants.signals[signal] : null,
                        error,
                        aborted: abortSignal.aborted,
                        pid: child.pid,
                        executionMethod: 'child_process',
                    });
                };
                child.stdout.on('data', (data) => handleOutput(data, 'stdout'));
                child.stderr.on('data', (data) => handleOutput(data, 'stderr'));
                child.on('error', (err) => {
                    error = err;
                    handleExit(1, null);
                });
                const abortHandler = async () => {
                    if (child.pid && !exited) {
                        if (isWindows) {
                            cpSpawn('taskkill', ['/pid', child.pid.toString(), '/f', '/t']);
                        }
                        else {
                            try {
                                process.kill(-child.pid, 'SIGTERM');
                                await new Promise((res) => setTimeout(res, SIGKILL_TIMEOUT_MS));
                                if (!exited) {
                                    process.kill(-child.pid, 'SIGKILL');
                                }
                            }
                            catch (_e) {
                                if (!exited)
                                    child.kill('SIGKILL');
                            }
                        }
                    }
                };
                abortSignal.addEventListener('abort', abortHandler, { once: true });
                child.on('exit', (code, signal) => {
                    handleExit(code, signal);
                });
                function cleanup() {
                    exited = true;
                    abortSignal.removeEventListener('abort', abortHandler);
                    if (stdoutDecoder) {
                        const remaining = stdoutDecoder.decode();
                        if (remaining) {
                            stdout += stripAnsi(remaining);
                        }
                    }
                    if (stderrDecoder) {
                        const remaining = stderrDecoder.decode();
                        if (remaining) {
                            stderr += stripAnsi(remaining);
                        }
                    }
                    const finalBuffer = Buffer.concat(outputChunks);
                    return { stdout, stderr, finalBuffer };
                }
            });
            return { pid: child.pid, result };
        }
        catch (e) {
            const error = e;
            return {
                pid: undefined,
                result: Promise.resolve({
                    error,
                    rawOutput: Buffer.from(''),
                    output: '',
                    exitCode: 1,
                    signal: null,
                    aborted: false,
                    pid: undefined,
                    executionMethod: 'none',
                }),
            };
        }
    }
    static executeWithPty(commandToExecute, cwd, onOutputEvent, abortSignal, terminalColumns, terminalRows, ptyInfo) {
        try {
            const cols = terminalColumns ?? 80;
            const rows = terminalRows ?? 30;
            const isWindows = os.platform() === 'win32';
            const shell = isWindows ? 'cmd.exe' : 'bash';
            const args = isWindows
                ? ['/c', commandToExecute]
                : ['-c', commandToExecute];
            const ptyProcess = ptyInfo?.module.spawn(shell, args, {
                cwd,
                name: 'xterm-color',
                cols,
                rows,
                env: {
                    ...process.env,
                    QWEN_CODE: '1',
                    TERM: 'xterm-256color',
                    PAGER: 'cat',
                },
                handleFlowControl: true,
            });
            const result = new Promise((resolve) => {
                const headlessTerminal = new Terminal({
                    allowProposedApi: true,
                    cols,
                    rows,
                });
                let processingChain = Promise.resolve();
                let decoder = null;
                let output = '';
                const outputChunks = [];
                const error = null;
                let exited = false;
                let isStreamingRawContent = true;
                const MAX_SNIFF_SIZE = 4096;
                let sniffedBytes = 0;
                const handleOutput = (data) => {
                    processingChain = processingChain.then(() => new Promise((resolve) => {
                        if (!decoder) {
                            const encoding = getCachedEncodingForBuffer(data);
                            try {
                                decoder = new TextDecoder(encoding);
                            }
                            catch {
                                decoder = new TextDecoder('utf-8');
                            }
                        }
                        outputChunks.push(data);
                        if (isStreamingRawContent && sniffedBytes < MAX_SNIFF_SIZE) {
                            const sniffBuffer = Buffer.concat(outputChunks.slice(0, 20));
                            sniffedBytes = sniffBuffer.length;
                            if (isBinary(sniffBuffer)) {
                                isStreamingRawContent = false;
                                onOutputEvent({ type: 'binary_detected' });
                            }
                        }
                        if (isStreamingRawContent) {
                            const decodedChunk = decoder.decode(data, { stream: true });
                            headlessTerminal.write(decodedChunk, () => {
                                const newStrippedOutput = getFullText(headlessTerminal);
                                output = newStrippedOutput;
                                onOutputEvent({ type: 'data', chunk: newStrippedOutput });
                                resolve();
                            });
                        }
                        else {
                            const totalBytes = outputChunks.reduce((sum, chunk) => sum + chunk.length, 0);
                            onOutputEvent({
                                type: 'binary_progress',
                                bytesReceived: totalBytes,
                            });
                            resolve();
                        }
                    }));
                };
                ptyProcess.onData((data) => {
                    const bufferData = Buffer.from(data, 'utf-8');
                    handleOutput(bufferData);
                });
                ptyProcess.onExit(({ exitCode, signal }) => {
                    exited = true;
                    abortSignal.removeEventListener('abort', abortHandler);
                    processingChain.then(() => {
                        const finalBuffer = Buffer.concat(outputChunks);
                        resolve({
                            rawOutput: finalBuffer,
                            output,
                            exitCode,
                            signal: signal ?? null,
                            error,
                            aborted: abortSignal.aborted,
                            pid: ptyProcess.pid,
                            executionMethod: ptyInfo?.name ?? 'node-pty',
                        });
                    });
                });
                const abortHandler = async () => {
                    if (ptyProcess.pid && !exited) {
                        ptyProcess.kill('SIGHUP');
                    }
                };
                abortSignal.addEventListener('abort', abortHandler, { once: true });
            });
            return { pid: ptyProcess.pid, result };
        }
        catch (e) {
            const error = e;
            return {
                pid: undefined,
                result: Promise.resolve({
                    error,
                    rawOutput: Buffer.from(''),
                    output: '',
                    exitCode: 1,
                    signal: null,
                    aborted: false,
                    pid: undefined,
                    executionMethod: 'none',
                }),
            };
        }
    }
}
//# sourceMappingURL=shellExecutionService.js.map