// *****************************************************************************
// Copyright (C) 2026 EclipseSource GmbH.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

import * as path from 'path';
import { injectable } from '@theia/core/shared/inversify';
import { spawn, ChildProcess, execSync } from 'child_process';
import { ShellExecutionServer, ShellExecutionRequest, ShellExecutionResult } from '../common/shell-execution-server';

const DEFAULT_TIMEOUT = 120000; // 2 minutes
const MAX_TIMEOUT = 600000;     // 10 minutes
const MAX_OUTPUT_SIZE = 1024 * 1024; // 1MB

@injectable()
export class ShellExecutionServerImpl implements ShellExecutionServer {

    protected readonly runningProcesses = new Map<string, ChildProcess>();
    protected readonly canceledExecutions = new Set<string>();

    async execute(request: ShellExecutionRequest): Promise<ShellExecutionResult> {
        const { command, cwd, timeout, executionId } = request;
        if (cwd && !path.isAbsolute(cwd)) {
            throw new Error(
                `Shell execution backend received a non-absolute cwd: '${cwd}'. ` +
                'The frontend must resolve cwd to an absolute path before sending it to the backend.'
            );
        }
        const effectiveTimeout = Math.min(timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT);
        const startTime = Date.now();

        return new Promise<ShellExecutionResult>(resolve => {
            let stdout = '';
            let stderr = '';
            let killed = false;

            const childProcess = spawn(command, [], {
                cwd,
                shell: true,
                detached: process.platform !== 'win32',
                windowsHide: true,
                env: process.env,
            });

            if (executionId) {
                this.runningProcesses.set(executionId, childProcess);
            }

            childProcess.stdout?.on('data', (data: Buffer) => {
                if (stdout.length < MAX_OUTPUT_SIZE) {
                    stdout += data.toString();
                }
            });

            childProcess.stderr?.on('data', (data: Buffer) => {
                if (stderr.length < MAX_OUTPUT_SIZE) {
                    stderr += data.toString();
                }
            });

            const timeoutId = setTimeout(() => {
                killed = true;
                this.killProcessTree(childProcess);
            }, effectiveTimeout);

            childProcess.on('close', (code, signal) => {
                clearTimeout(timeoutId);

                if (executionId) {
                    this.runningProcesses.delete(executionId);
                }

                const duration = Date.now() - startTime;
                const wasCanceledByUser = executionId ? this.canceledExecutions.has(executionId) : false;
                if (executionId) {
                    this.canceledExecutions.delete(executionId);
                }

                if (signal || killed) {
                    if (wasCanceledByUser) {
                        resolve({
                            success: false,
                            exitCode: undefined,
                            stdout,
                            stderr,
                            error: 'Command canceled by user',
                            duration,
                            canceled: true,
                            resolvedCwd: cwd,
                        });
                    } else {
                        resolve({
                            success: false,
                            exitCode: undefined,
                            stdout,
                            stderr,
                            error: `Command timed out after ${effectiveTimeout}ms`,
                            duration,
                            resolvedCwd: cwd,
                        });
                    }
                } else if (code === 0) {
                    resolve({
                        success: true,
                        exitCode: 0,
                        stdout,
                        stderr,
                        duration,
                        resolvedCwd: cwd,
                    });
                } else {
                    resolve({
                        success: false,
                        exitCode: code ?? undefined,
                        stdout,
                        stderr,
                        duration,
                        resolvedCwd: cwd,
                    });
                }
            });

            childProcess.on('error', (error: Error) => {
                clearTimeout(timeoutId);

                if (executionId) {
                    this.runningProcesses.delete(executionId);
                    this.canceledExecutions.delete(executionId);
                }

                resolve({
                    success: false,
                    exitCode: undefined,
                    stdout,
                    stderr,
                    error: error.message,
                    duration: Date.now() - startTime,
                    resolvedCwd: cwd,
                });
            });
        });
    }

    async cancel(executionId: string): Promise<boolean> {
        const childProcess = this.runningProcesses.get(executionId);
        if (childProcess) {
            this.canceledExecutions.add(executionId);
            this.killProcessTree(childProcess);
            this.runningProcesses.delete(executionId);
            return true;
        }
        return false;
    }

    protected killProcessTree(childProcess: ChildProcess): void {
        if (!childProcess.pid) {
            return;
        }

        try {
            if (process.platform === 'win32') {
                execSync(`taskkill /pid ${childProcess.pid} /T /F`, { stdio: 'ignore' });
            } else {
                process.kill(-childProcess.pid, 'SIGTERM');
            }
        } catch {
            try {
                childProcess.kill('SIGKILL');
            } catch {
                // Process may already be dead
            }
        }
    }
}
