"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.remoteConnectionManager = exports.RemoteConnectionManager = exports.RemoteConnection = void 0;
const ssh2_1 = require("ssh2");
const events_1 = require("events");
/**
 * Class to manage SSH connections to remote servers
 */
class RemoteConnection extends events_1.EventEmitter {
    // Getters for secure access to authentication details
    get hasPassword() {
        return !!this.config.password;
    }
    get hasPrivateKey() {
        return !!this.config.privateKey;
    }
    get hasPassphrase() {
        return !!this.config.passphrase;
    }
    // Getter for secure config (without sensitive data)
    get secureConfig() {
        return {
            name: this.name,
            host: this.host,
            port: this.port,
            username: this.username
        };
    }
    // Method to get the full config for use by connection manager
    getFullConfig() {
        return this.config;
    }
    constructor(config) {
        super();
        this._isConnected = false;
        this.isPM2Installed = false;
        /** Index into PM2_BUILDERS of the invocation that actually works on this host. -1 = not yet resolved. */
        this.resolvedBuilderIndex = -1;
        this.client = new ssh2_1.Client();
        this.config = config;
        // Initialize public properties
        this.name = config.name || config.host;
        this.host = config.host;
        this.port = config.port || 22;
        this.username = config.username;
    }
    /**
     * Check if the connection is active
     */
    isConnected() {
        return this._isConnected;
    }
    /**
     * Connect to the remote server
     */ async connect() {
        if (this._isConnected) {
            return Promise.resolve();
        }
        return new Promise((resolve, reject) => {
            this.client.on('ready', () => {
                this._isConnected = true;
                console.log(`Successfully connected to ${this.config.host}`);
                this.emit('connected');
                resolve();
            });
            this.client.on('error', (err) => {
                this._isConnected = false;
                console.error(`SSH connection error for ${this.config.host}:`, err.message);
                reject(err);
            });
            this.client.on('end', () => {
                this._isConnected = false;
                console.log(`Disconnected from ${this.config.host}`);
                this.emit('disconnected');
            });
            // Attempt connection with the provided configuration
            const connectionConfig = {
                host: this.config.host,
                port: this.config.port || 22,
                username: this.config.username,
                // Add reasonable timeouts optimized for VPN/remote scenarios
                readyTimeout: 20000, // 20 seconds to establish connection (was 10s)
                keepaliveInterval: 10000 // Send keepalive every 10 seconds (was 30s) for aggressive firewalls
            };
            // Debug output
            console.log(`Connecting to ${this.config.host}:${this.config.port} as ${this.config.username}`);
            console.log(`Authentication methods available: ${this.config.password ? 'Password' : 'No password'}, ${this.config.privateKey ? 'Private key' : 'No private key'}`);
            // Add authentication method
            if (this.config.privateKey) {
                connectionConfig.privateKey = this.config.privateKey;
                if (this.config.passphrase) {
                    connectionConfig.passphrase = this.config.passphrase;
                }
            }
            else if (this.config.password) {
                connectionConfig.password = this.config.password;
            }
            else {
                console.error('No authentication method provided for', this.config.host);
                return reject(new Error('No authentication method provided'));
            }
            this.client.connect(connectionConfig);
        });
    }
    /**
     * Disconnect from the remote server
     */
    disconnect() {
        return new Promise((resolve) => {
            if (!this._isConnected) {
                resolve();
                return;
            }
            this.client.once('end', () => {
                this._isConnected = false;
                resolve();
            });
            this.client.end();
        });
    } /**
     * Execute a command on the remote server
     * @param command The command to execute
     * @param forceSudo Whether to force using sudo for this specific command
     */
    async executeCommand(command, forceSudo) {
        // Connect if not already connected
        if (!this._isConnected) {
            await this.connect();
        }
        // Apply sudo if configured or explicitly requested for this command
        const useElevatedPrivileges = forceSudo || this.config.useSudo;
        // Use sudo -S so sudo reads the password from stdin instead of piping it in the shell command
        const finalCommand = (useElevatedPrivileges && this.config.password)
            ? `sudo -S ${command}`
            : command;
        console.log(`Executing command: ${useElevatedPrivileges ? '[sudo] ' : ''}${command}`);
        return new Promise((resolve, reject) => {
            this.client.exec(finalCommand, (err, channel) => {
                if (err) {
                    console.error('Error executing command:', err);
                    return reject(err);
                }
                // Write password to sudo's stdin, then close stdin
                if (useElevatedPrivileges && this.config.password) {
                    channel.stdin.write(this.config.password + '\n');
                    channel.stdin.end();
                }
                let stdout = '';
                let stderr = '';
                let exitCode = null;
                channel.on('data', (data) => {
                    stdout += data.toString();
                });
                channel.stderr.on('data', (data) => {
                    stderr += data.toString();
                });
                channel.on('exit', (code) => {
                    exitCode = code;
                });
                channel.on('close', () => {
                    resolve({
                        stdout,
                        stderr,
                        code: exitCode
                    });
                });
                channel.on('error', (err) => {
                    console.error('Channel error:', err);
                    reject(err);
                });
            });
        });
    }
    static validateRemotePath(remotePath) {
        if (!remotePath)
            return false;
        if (remotePath.includes('..'))
            return false;
        if (RemoteConnection.SHELL_UNSAFE.test(remotePath))
            return false;
        if (!/\.(log|gz)$/i.test(remotePath))
            return false;
        return true;
    }
    async streamFileToResponse(remotePath, res, fileName) {
        // Validate path before any shell interpolation (fallback methods 2-4 use exec)
        if (!RemoteConnection.validateRemotePath(remotePath)) {
            if (!res.headersSent)
                res.status(400).json({ success: false, error: 'Invalid log file path' });
            return;
        }
        const setHeaders = () => {
            if (!res.headersSent) {
                res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
                res.setHeader('Content-Type', 'text/plain; charset=utf-8');
            }
        };
        const escapeRemotePathForShell = (value) => {
            if (/[\0\n\r]/.test(value)) {
                throw new Error('Invalid remote path');
            }
            return `'${value.replace(/'/g, `'\"'\"'`)}'`;
        };
        let escapedRemotePath;
        try {
            escapedRemotePath = escapeRemotePathForShell(remotePath);
        }
        catch {
            if (!res.headersSent) {
                res.status(400).json({ success: false, error: 'Invalid remote path' });
            }
            return;
        }
        // @group StreamFileToResponse : Helper — pipe an exec channel stdout to response
        const pipeExec = (cmd, sudoPassword) => new Promise((resolve) => {
            this.client.exec(cmd, (err, channel) => {
                if (err) {
                    resolve('error');
                    return;
                }
                if (sudoPassword) {
                    channel.stdin.write(sudoPassword + '\n');
                }
                channel.stdin.end();
                let hasData = false;
                channel.on('data', (chunk) => {
                    if (!hasData) {
                        setHeaders();
                        hasData = true;
                    }
                    res.write(chunk);
                });
                channel.on('close', (code) => {
                    if (hasData) {
                        res.end();
                        resolve('ok');
                    }
                    else
                        resolve(code === 0 ? 'empty' : 'error');
                });
                channel.on('error', () => resolve('error'));
                channel.stderr.resume(); // Discard stderr so it doesn't block the channel
            });
        });
        // ── 1. SFTP (preferred — real streaming, no exec overhead) ──────────
        const sftpOk = await new Promise((resolve) => {
            this.client.sftp((err, sftp) => {
                if (err) {
                    resolve(false);
                    return;
                }
                const stream = sftp.createReadStream(remotePath);
                let hasData = false;
                stream.on('data', (chunk) => {
                    if (!hasData) {
                        setHeaders();
                        hasData = true;
                    }
                    res.write(chunk);
                });
                stream.on('end', () => { if (hasData) {
                    res.end();
                    resolve(true);
                }
                else
                    resolve(false); });
                stream.on('error', () => resolve(false));
            });
        });
        if (sftpOk)
            return;
        // ── 2. Plain cat ─────────────────────────────────────────────────────
        if (await pipeExec(`cat -- ${escapedRemotePath}`) === 'ok')
            return;
        // ── 3. sudo -S cat (password auth) ───────────────────────────────────
        if (this.config.password) {
            if (await pipeExec(`sudo -S cat -- ${escapedRemotePath}`, this.config.password) === 'ok')
                return;
        }
        // ── 4. sudo cat without -S (NOPASSWD / key-based auth) ───────────────
        if (await pipeExec(`sudo cat -- ${escapedRemotePath}`) === 'ok')
            return;
        // All attempts failed
        if (!res.headersSent) {
            res.status(403).json({ success: false, error: 'Cannot read log file — check file permissions or sudo configuration' });
        }
    }
    /**
     * Stream a remote .gz file to an HTTP response, decompressing it on the fly.
     * Uses SFTP + local zlib.createGunzip() pipeline — no server-side buffering.
     * Falls back to exec `zcat` if SFTP is unavailable.
     */
    async streamGzFileToResponse(remotePath, res, fileName) {
        if (!RemoteConnection.validateRemotePath(remotePath)) {
            if (!res.headersSent)
                res.status(400).json({ success: false, error: 'Invalid log file path' });
            return;
        }
        const zlib = require('zlib');
        const setHeaders = () => {
            if (!res.headersSent) {
                res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
                res.setHeader('Content-Type', 'text/plain; charset=utf-8');
            }
        };
        // ── SFTP + local gunzip (preferred — no exec, streaming, no buffering) ──
        const sftpOk = await new Promise((resolve) => {
            this.client.sftp((err, sftp) => {
                if (err) {
                    resolve(false);
                    return;
                }
                const inputStream = sftp.createReadStream(remotePath);
                const gunzip = zlib.createGunzip();
                setHeaders();
                inputStream.pipe(gunzip).pipe(res);
                gunzip.on('finish', () => resolve(true));
                gunzip.on('error', () => { inputStream.destroy(); resolve(false); });
                inputStream.on('error', () => resolve(false));
            });
        });
        if (sftpOk)
            return;
        // ── Fallback: exec zcat and buffer (path already validated) ──────────
        if (!res.headersSent) {
            const result = await this.executeCommand(`zcat -- "${remotePath}" 2>/dev/null`);
            const decompressed = result.stdout ||
                (await this.executeCommand(`sudo zcat -- "${remotePath}" 2>/dev/null`)).stdout;
            if (decompressed) {
                setHeaders();
                res.send(decompressed);
            }
            else if (!res.headersSent) {
                res.status(500).json({ success: false, error: 'Could not decompress file' });
            }
        }
    }
    /**
     * Check if PM2 is installed on the remote server
     * Uses multiple detection methods for better reliability
     */
    async checkPM2Installation() {
        try {
            // Method 1: Try running pm2 --version directly (most reliable)
            try {
                const versionResult = await this.executeCommand('pm2 --version');
                if (versionResult.code === 0 && versionResult.stdout.trim()) {
                    console.log(`PM2 detected via version check: ${versionResult.stdout.trim()}`);
                    this.isPM2Installed = true;
                    return true;
                }
            }
            catch (error) {
                console.log('PM2 version check failed, trying alternative methods');
            }
            // Method 2: Check common installation paths
            const pathChecks = [
                'which pm2',
                'command -v pm2',
                'ls -la /usr/local/bin/pm2',
                'ls -la ~/.npm-global/bin/pm2',
                'ls -la ~/node_modules/.bin/pm2',
                'find /usr -name "pm2" -type f -executable 2>/dev/null | head -1'
            ];
            for (const pathCheck of pathChecks) {
                try {
                    const result = await this.executeCommand(pathCheck);
                    if (result.code === 0 && result.stdout.trim() && !result.stdout.includes('not found')) {
                        console.log(`PM2 found via: ${pathCheck} -> ${result.stdout.trim()}`);
                        this.isPM2Installed = true;
                        return true;
                    }
                }
                catch (error) {
                    // Continue to next method
                    continue;
                }
            }
            // Method 3: Try with full environment loading
            try {
                const envResult = await this.executeCommand('bash -l -c "pm2 --version"');
                if (envResult.code === 0 && envResult.stdout.trim()) {
                    console.log(`PM2 detected via bash login shell: ${envResult.stdout.trim()}`);
                    this.isPM2Installed = true;
                    return true;
                }
            }
            catch (error) {
                console.log('PM2 not found via bash login shell');
            }
            // Method 4: Try npm ls -g pm2
            try {
                const npmResult = await this.executeCommand('npm ls -g pm2 --depth=0');
                if (npmResult.code === 0 && !npmResult.stdout.includes('(empty)')) {
                    console.log('PM2 detected via npm global list');
                    this.isPM2Installed = true;
                    return true;
                }
            }
            catch (error) {
                console.log('PM2 not found via npm global list');
            }
            console.log('PM2 not detected via any method');
            this.isPM2Installed = false;
            return false;
        }
        catch (error) {
            console.error('Error during PM2 installation check:', error);
            this.isPM2Installed = false;
            return false;
        }
    }
    /**
     * Execute a PM2 command with proper PATH handling.
     * The first successful invocation is cached so later calls (and streaming
     * commands) reuse the same shell/binary path without re-probing.
     */
    async executePM2Command(pm2Args) {
        // Fast path: reuse the builder we already know works on this host
        if (this.resolvedBuilderIndex >= 0) {
            return this.executeCommand(RemoteConnection.PM2_BUILDERS[this.resolvedBuilderIndex](pm2Args));
        }
        let lastError = null;
        for (let i = 0; i < RemoteConnection.PM2_BUILDERS.length; i++) {
            const command = RemoteConnection.PM2_BUILDERS[i](pm2Args);
            try {
                const result = await this.executeCommand(command);
                if (result.code === 0) {
                    this.resolvedBuilderIndex = i; // Cache for all future calls on this connection
                    console.log(`[executePM2Command] Resolved pm2 via builder[${i}]: ${command.substring(0, 60)}`);
                    return result;
                }
                lastError = new Error(`Command failed (code ${result.code}): ${result.stderr}`);
            }
            catch (error) {
                lastError = error;
            }
        }
        throw lastError || new Error(`Failed to execute PM2 command: ${pm2Args}`);
    }
    /**
     * Build a pm2 command string using the already-resolved invocation pattern.
     * Falls back to bash -li -c if the resolver has not yet run.
     * Use this when you need the raw command string for a streaming shell exec.
     */
    buildPM2StreamCommand(pm2Args) {
        if (this.resolvedBuilderIndex >= 0) {
            return RemoteConnection.PM2_BUILDERS[this.resolvedBuilderIndex](pm2Args);
        }
        // Default: login+interactive shell covers .profile AND .bashrc (catches nvm)
        return `bash -li -c "pm2 ${pm2Args}"`;
    }
    /**
     * Get PM2 processes from the remote server
     */
    async getPM2Processes() {
        if (!this._isConnected) {
            await this.connect();
        }
        let result;
        try {
            // executePM2Command tries all known paths/shells before giving up
            result = await this.executePM2Command('jlist');
            this.isPM2Installed = true;
        }
        catch (err) {
            // All path attempts failed — PM2 is genuinely not found
            console.error('Error getting PM2 processes:', err);
            throw new Error('PM2 is not installed or not accessible on the remote server. Please install PM2 globally using: npm install -g pm2');
        }
        // Extract the JSON array from the output (pm2 jlist may include extra lines)
        let cleanedOutput = result.stdout.trim();
        const startIndex = cleanedOutput.indexOf('[');
        const endIndex = cleanedOutput.lastIndexOf(']') + 1;
        if (startIndex !== -1 && endIndex > 0) {
            cleanedOutput = cleanedOutput.substring(startIndex, endIndex);
        }
        else {
            console.log('Invalid PM2 jlist output, returning empty list. Raw:', result.stdout);
            return [];
        }
        try {
            const processList = JSON.parse(cleanedOutput);
            // Return the full PM2 process shape (pm2_env + monit) so all frontend
            // components work without modification when viewing a remote server.
            return processList.map((proc) => {
                var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v;
                return ({
                    pid: proc.pid || 0,
                    pm_id: proc.pm_id || 0,
                    name: proc.name || '',
                    monit: {
                        cpu: proc.monit ? (proc.monit.cpu || 0) : 0,
                        memory: proc.monit ? (proc.monit.memory || 0) : 0,
                    },
                    pm2_env: {
                        // identity
                        pm_id: proc.pm_id || 0,
                        name: proc.name || '',
                        namespace: ((_a = proc.pm2_env) === null || _a === void 0 ? void 0 : _a.namespace) || 'default',
                        version: ((_b = proc.pm2_env) === null || _b === void 0 ? void 0 : _b.version) || '',
                        versioning: ((_c = proc.pm2_env) === null || _c === void 0 ? void 0 : _c.versioning) || null,
                        // timing
                        pm_uptime: ((_d = proc.pm2_env) === null || _d === void 0 ? void 0 : _d.pm_uptime) || 0,
                        created_at: ((_e = proc.pm2_env) === null || _e === void 0 ? void 0 : _e.created_at) || 0,
                        // paths
                        pm_cwd: ((_f = proc.pm2_env) === null || _f === void 0 ? void 0 : _f.pm_cwd) || '',
                        pm_exec_path: ((_g = proc.pm2_env) === null || _g === void 0 ? void 0 : _g.pm_exec_path) || '',
                        pm_out_log_path: ((_h = proc.pm2_env) === null || _h === void 0 ? void 0 : _h.pm_out_log_path) || '',
                        pm_err_log_path: ((_j = proc.pm2_env) === null || _j === void 0 ? void 0 : _j.pm_err_log_path) || '',
                        // runtime
                        exec_interpreter: ((_k = proc.pm2_env) === null || _k === void 0 ? void 0 : _k.exec_interpreter) || 'node',
                        exec_mode: ((_l = proc.pm2_env) === null || _l === void 0 ? void 0 : _l.exec_mode) || 'fork',
                        instances: ((_m = proc.pm2_env) === null || _m === void 0 ? void 0 : _m.instances) || 1,
                        node_args: ((_o = proc.pm2_env) === null || _o === void 0 ? void 0 : _o.node_args) || [],
                        status: ((_p = proc.pm2_env) === null || _p === void 0 ? void 0 : _p.status) || 'unknown',
                        restart_time: ((_q = proc.pm2_env) === null || _q === void 0 ? void 0 : _q.restart_time) || 0,
                        unstable_restarts: ((_r = proc.pm2_env) === null || _r === void 0 ? void 0 : _r.unstable_restarts) || 0,
                        autorestart: (_t = (_s = proc.pm2_env) === null || _s === void 0 ? void 0 : _s.autorestart) !== null && _t !== void 0 ? _t : true,
                        watch: ((_u = proc.pm2_env) === null || _u === void 0 ? void 0 : _u.watch) || false,
                        env: ((_v = proc.pm2_env) === null || _v === void 0 ? void 0 : _v.env) || {},
                    },
                });
            });
        }
        catch (error) {
            console.error('Error parsing PM2 process list:', error);
            console.log('Raw output:', result.stdout);
            return [];
        }
    }
    /**
     * Format memory size to human readable format
     */
    formatMemory(bytes) {
        if (bytes === 0)
            return '0 B';
        const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
        const i = Math.floor(Math.log(bytes) / Math.log(1024));
        return parseFloat((bytes / Math.pow(1024, i)).toFixed(2)) + ' ' + sizes[i];
    }
    /**
     * Format uptime to human readable format
     */ formatUptime(timestamp) {
        if (!timestamp || isNaN(timestamp))
            return 'N/A';
        try {
            const uptime = Date.now() - timestamp;
            if (uptime < 0)
                return 'N/A'; // Invalid timestamp
            const seconds = Math.floor(uptime / 1000);
            if (seconds < 60) {
                return `${seconds}s`;
            }
            else if (seconds < 3600) {
                return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
            }
            else if (seconds < 86400) {
                const hours = Math.floor(seconds / 3600);
                const mins = Math.floor((seconds % 3600) / 60);
                return `${hours}h ${mins}m`;
            }
            else {
                const days = Math.floor(seconds / 86400);
                const hours = Math.floor((seconds % 86400) / 3600);
                return `${days}d ${hours}h`;
            }
        }
        catch (error) {
            console.error('Error formatting uptime:', error);
            return 'N/A';
        }
    }
    /**
     * Start a PM2 process
     */
    async startPM2Process(processName) {
        return await this.executePM2Command(`start ${processName}`);
    }
    /**
     * Stop a PM2 process
     */
    async stopPM2Process(processName) {
        return await this.executePM2Command(`stop ${processName}`);
    }
    /**
     * Restart a PM2 process
     */
    async restartPM2Process(processName) {
        return await this.executePM2Command(`restart ${processName}`);
    }
    /**
     * Delete a PM2 process
     */
    async deletePM2Process(processName) {
        return await this.executePM2Command(`delete ${processName}`);
    }
    /**
     * Install PM2 on the remote server
     */
    async installPM2() {
        try {
            console.log('Attempting to install PM2 on remote server...');
            // Try different installation methods
            const installCommands = [
                'npm install -g pm2', // Standard global install
                'sudo npm install -g pm2', // With sudo if needed
                'npm install -g pm2 --unsafe-perm=true', // With unsafe permissions
            ];
            let lastResult = null;
            for (const command of installCommands) {
                try {
                    const result = await this.executeCommand(command, command.includes('sudo'));
                    if (result.code === 0) {
                        console.log(`PM2 installed successfully with: ${command}`);
                        // Verify installation
                        const isPM2Installed = await this.checkPM2Installation();
                        if (isPM2Installed) {
                            this.isPM2Installed = true;
                            return result;
                        }
                    }
                    lastResult = result;
                }
                catch (error) {
                    console.log(`Failed to install PM2 with command: ${command}`);
                    lastResult = {
                        stdout: '',
                        stderr: error instanceof Error ? error.message : 'Unknown error',
                        code: 1
                    };
                }
            }
            return lastResult || {
                stdout: '',
                stderr: 'Failed to install PM2 with any method',
                code: 1
            };
        }
        catch (error) {
            console.error('Error installing PM2:', error);
            throw error;
        }
    }
    /**
     * Get system information from the remote server
     */
    async getSystemInfo() {
        try {
            // Connect if not already connected
            if (!this._isConnected) {
                await this.connect();
            }
            // Run a series of commands to get system information
            const [hostname, platform, arch, nodeVersion, memInfo, cpuInfo] = await Promise.all([
                this.executeCommand('hostname'),
                this.executeCommand('uname -s'),
                this.executeCommand('uname -m'),
                this.executeCommand('node -v'),
                this.executeCommand('free -b'),
                this.executeCommand('cat /proc/cpuinfo | grep processor | wc -l')
            ]);
            // Parse memory information
            const memLines = memInfo.stdout.split('\n');
            let totalMemory = 0;
            let freeMemory = 0;
            if (memLines.length > 1) {
                const memValues = memLines[1].split(/\s+/);
                if (memValues.length > 6) {
                    totalMemory = parseInt(memValues[1], 10);
                    freeMemory = parseInt(memValues[3], 10);
                }
            }
            // Get load average
            const loadAvgResult = await this.executeCommand('cat /proc/loadavg');
            const loadAvg = loadAvgResult.stdout.trim().split(' ').slice(0, 3).map(parseFloat);
            return {
                hostname: hostname.stdout.trim(),
                platform: platform.stdout.trim(),
                arch: arch.stdout.trim(),
                nodeVersion: nodeVersion.stdout.trim(),
                totalMemory: this.formatMemory(totalMemory),
                freeMemory: this.formatMemory(freeMemory),
                cpuCount: parseInt(cpuInfo.stdout.trim(), 10),
                loadAverage: loadAvg
            };
        }
        catch (error) {
            console.error('Error getting system info:', error);
            throw error;
        }
    }
    /**
     * Get logs for a PM2 process
     */
    async getPM2Logs(processName, lines = 100) {
        return await this.executeCommand(`pm2 logs ${processName} --lines ${lines} --nostream`);
    } /**
     * Create a streaming log connection for a command
     */
    async createLogStream(command, useSudo = false) {
        if (!this._isConnected) {
            throw new Error('Connection not established');
        }
        // Use sudo -S so sudo reads the password from stdin rather than the shell command line
        const useSudoAuth = useSudo && this.config.useSudo && !!this.config.password;
        const finalCommand = useSudoAuth ? `sudo -S ${command}` : command;
        let isInitialized = false;
        console.log(`[createLogStream] Executing command: ${useSudoAuth ? '[sudo] ' : ''}${command}`);
        return new Promise((resolve, reject) => {
            this.client.exec(finalCommand, (err, stream) => {
                var _a;
                if (err) {
                    console.error(`[createLogStream] Exec error:`, err);
                    reject(err);
                    return;
                }
                // Write password to sudo's stdin (do not close stdin — stream must stay open)
                if (useSudoAuth && this.config.password) {
                    stream.stdin.write(this.config.password + '\n');
                }
                const logEmitter = new events_1.EventEmitter();
                stream.on('data', (data) => {
                    const dataStr = data.toString();
                    console.log(`[createLogStream] Raw data:`, dataStr);
                    // Skip the sudo password prompt and initial setup messages
                    if (!isInitialized) {
                        if (dataStr.includes('[sudo] password') ||
                            dataStr.includes('Password:') ||
                            dataStr.trim() === '') {
                            return; // Skip initialization messages
                        }
                        isInitialized = true;
                    }
                    logEmitter.emit('data', dataStr);
                });
                (_a = stream.stderr) === null || _a === void 0 ? void 0 : _a.on('data', (data) => {
                    const dataStr = data.toString();
                    console.log(`[createLogStream] Stderr data:`, dataStr);
                    // Don't emit stderr data for sudo prompts or permission messages
                    if (!dataStr.includes('[sudo] password') &&
                        !dataStr.includes('Password:') &&
                        !dataStr.includes('Sorry, try again')) {
                        logEmitter.emit('data', dataStr);
                    }
                });
                stream.on('close', (code) => {
                    console.log(`[createLogStream] Stream closed with code:`, code);
                    logEmitter.emit('close', code);
                });
                stream.on('error', (error) => {
                    console.error(`[createLogStream] Stream error:`, error);
                    logEmitter.emit('error', error);
                });
                // Add a method to kill the stream
                logEmitter.kill = () => {
                    console.log(`[createLogStream] Killing stream`);
                    stream.close();
                };
                resolve(logEmitter);
            });
        });
    }
}
exports.RemoteConnection = RemoteConnection;
/**
 * Stream a remote file directly into an HTTP response without buffering.
 *
 * Strategy (tried in order):
 *   1. SFTP createReadStream  — best; handles large files, uses file-transfer protocol
 *   2. exec `cat <file>`      — current SSH user, direct pipe to response
 *   3. exec `sudo -S cat`     — password-based sudo (when password auth is configured)
 *   4. exec `sudo cat`        — NOPASSWD sudo (key-based auth where sudo needs no password)
 */
// @group Security : Shell metacharacter allowlist for remote file paths
RemoteConnection.SHELL_UNSAFE = /['"`$|&;<>(){}\\\n\r\0]/;
/**
 * Ordered list of command builders tried when resolving the pm2 binary.
 * The index of the first successful builder is cached so that subsequent
 * calls (including streaming commands) reuse the same invocation.
 */
RemoteConnection.PM2_BUILDERS = [
    args => `pm2 ${args}`,
    args => `bash -l -c "pm2 ${args}"`,
    args => `bash -i -c "pm2 ${args}"`,
    args => `bash -li -c "pm2 ${args}"`,
    args => `/usr/local/bin/pm2 ${args}`,
    args => `/usr/bin/pm2 ${args}`,
    args => `~/.npm-global/bin/pm2 ${args}`,
    args => `~/.npm/bin/pm2 ${args}`,
    args => `~/node_modules/.bin/pm2 ${args}`,
    args => `. ~/.nvm/nvm.sh && pm2 ${args}`,
    args => `. ~/.nvm/nvm.sh && nvm use --lts && pm2 ${args}`,
    args => `. /usr/share/nvm/init-nvm.sh && pm2 ${args}`,
    args => `npx pm2 ${args}`,
];
/**
 * Connection manager to handle multiple remote connections
 */
class RemoteConnectionManager {
    constructor() {
        this.connections = new Map();
        const path = require('path');
        this.configFilePath = path.join(__dirname, '../config/remote-connections.json');
        this.loadConnectionsFromDisk();
    }
    /**
     * Create a new remote connection
     * @param config Connection configuration
     * @returns The connection ID
     */
    createConnection(config) {
        const connectionId = `${config.host}-${config.port}-${config.username}`;
        if (this.connections.has(connectionId)) {
            return connectionId;
        }
        const connection = new RemoteConnection(config);
        this.connections.set(connectionId, connection);
        // Save the updated connections to disk
        this.saveConnectionsToDisk();
        return connectionId;
    }
    /**
     * Update an existing remote connection
     * @param connectionId The connection ID
     * @param config New connection configuration
     * @returns True if the connection was updated, false if it didn't exist
     */
    async updateConnection(connectionId, config) {
        const existingConnection = this.connections.get(connectionId);
        if (!existingConnection) {
            return false;
        }
        // Close the existing connection if it's active
        if (existingConnection.isConnected()) {
            await existingConnection.disconnect();
        }
        // Get the old config to preserve password if not provided in update
        const oldConfig = existingConnection.getFullConfig();
        // If password is not provided in the update, keep the old one
        const updatedConfig = {
            ...config,
            password: config.password || oldConfig.password,
            privateKey: config.privateKey || oldConfig.privateKey,
            passphrase: config.passphrase || oldConfig.passphrase
        };
        // Create a new connection with updated config
        const newConnection = new RemoteConnection(updatedConfig);
        // Replace the old connection with the new one
        this.connections.set(connectionId, newConnection);
        // Save the updated connections to disk
        this.saveConnectionsToDisk();
        return true;
    }
    /**
     * Get a connection by ID
     * @param connectionId The connection ID
     */
    getConnection(connectionId) {
        return this.connections.get(connectionId);
    }
    /**
     * Close a connection by ID (disconnect but keep the configuration)
     * @param connectionId The connection ID
     */
    async closeConnection(connectionId) {
        const connection = this.connections.get(connectionId);
        if (connection) {
            await connection.disconnect();
            return true;
        }
        return false;
    }
    /**
     * Close all connections
     */ async closeAllConnections() {
        const closePromises = Array.from(this.connections.values()).map((conn) => conn.disconnect());
        await Promise.all(closePromises);
        this.connections.clear();
    }
    /**
     * Get all connections
     * @returns Map of all connections
     */
    getAllConnections() {
        return this.connections;
    }
    /**
   * Delete a connection by ID
   * @param connectionId The connection ID
   * @returns True if the connection was deleted, false if it didn't exist
   */
    deleteConnection(connectionId) {
        const deleted = this.connections.delete(connectionId);
        if (deleted) {
            // Save the updated connections to disk
            this.saveConnectionsToDisk();
        }
        return deleted;
    }
    /**
     * Load connection configurations from disk
     */
    loadConnectionsFromDisk() {
        try {
            const fs = require('fs');
            const path = require('path');
            const { decrypt } = require('./encryption');
            // Ensure the config directory exists
            const configDir = path.dirname(this.configFilePath);
            if (!fs.existsSync(configDir)) {
                fs.mkdirSync(configDir, { recursive: true });
            }
            // Create the file with default content if it doesn't exist
            if (!fs.existsSync(this.configFilePath)) {
                fs.writeFileSync(this.configFilePath, JSON.stringify({ connections: [] }, null, 2));
            }
            // Read the configuration file
            const data = fs.readFileSync(this.configFilePath, 'utf8');
            const config = JSON.parse(data);
            if (config && Array.isArray(config.connections)) {
                // Create connections from saved configs
                config.connections.forEach((conn) => {
                    const connectionConfig = {
                        name: conn.name,
                        host: conn.host,
                        port: conn.port,
                        username: conn.username,
                        // Decrypt sensitive data if it exists
                        password: conn.password ? decrypt(conn.password) : undefined,
                        privateKey: conn.privateKey ? decrypt(conn.privateKey) : undefined,
                        passphrase: conn.passphrase ? decrypt(conn.passphrase) : undefined,
                        useSudo: conn.useSudo
                    };
                    // Re-create the connection with the original ID
                    const connection = new RemoteConnection(connectionConfig);
                    this.connections.set(conn.id, connection);
                });
                console.log(`Loaded ${config.connections.length} remote connections from disk`);
            }
        }
        catch (error) {
            console.error('Error loading remote connections from disk:', error);
        }
    }
    /**
   * Save connection configurations to disk
   */
    saveConnectionsToDisk() {
        try {
            const fs = require('fs');
            const path = require('path');
            const { encrypt } = require('./encryption');
            // Ensure the config directory exists
            const configDir = path.dirname(this.configFilePath);
            if (!fs.existsSync(configDir)) {
                fs.mkdirSync(configDir, { recursive: true });
            }
            // Convert connections map to serializable array with encrypted sensitive data
            const savedConnections = Array.from(this.connections.entries()).map(([id, conn]) => {
                // Get the original configuration to access sensitive data
                const config = conn.getFullConfig();
                return {
                    id,
                    name: conn.name,
                    host: conn.host,
                    port: conn.port,
                    username: conn.username,
                    password: config.password ? encrypt(config.password) : undefined,
                    privateKey: config.privateKey ? encrypt(config.privateKey) : undefined,
                    passphrase: config.passphrase ? encrypt(config.passphrase) : undefined,
                    useSudo: config.useSudo
                };
            });
            // Write to file
            fs.writeFileSync(this.configFilePath, JSON.stringify({
                connections: savedConnections
            }, null, 2));
            console.log(`Saved ${savedConnections.length} remote connections to disk`);
        }
        catch (error) {
            console.error('Error saving remote connections to disk:', error);
        }
    }
}
exports.RemoteConnectionManager = RemoteConnectionManager;
// Create a singleton instance of the connection manager
exports.remoteConnectionManager = new RemoteConnectionManager();
