/**
 * HVAC Testing Framework - Secure Command Execution
 * 
 * Provides secure command execution with input validation, parameterization,
 * and command allowlisting to prevent injection vulnerabilities.
 * 
 * Security Features:
 * - Command allowlisting with parameter validation
 * - Input sanitization and validation
 * - Subprocess execution with proper isolation
 * - Audit logging of all command executions
 * - Timeout and resource limits
 * 
 * @author Claude Code - Emergency Security Response
 * @version 1.0.0
 * @security CRITICAL - Prevents command injection vulnerabilities
 */

const { spawn, execFile } = require('child_process');
const path = require('path');
const { getCredentialManager } = require('./SecureCredentialManager');

class SecureCommandExecutor {
    constructor() {
        this.credentialManager = getCredentialManager();
        this.allowedCommands = this.initializeCommandAllowlist();
        this.executionLog = [];
    }

    /**
     * Initialize allowlist of safe commands with their parameter patterns
     * @returns {Map} Command allowlist configuration
     */
    initializeCommandAllowlist() {
        return new Map([
            // WordPress CLI commands
            ['wp', {
                binary: 'wp',
                allowedSubcommands: [
                    'rewrite flush',
                    'eval',
                    'user list',
                    'user get',
                    'option get',
                    'option update',
                    'db query',
                    'plugin list',
                    'theme list'
                ],
                parameterValidation: {
                    'eval': /^['"][^'";&|`$()]*['"]$/,
                    'user get': /^[a-zA-Z0-9@._-]+$/,
                    'option get': /^[a-zA-Z0-9_-]+$/,
                    'option update': /^[a-zA-Z0-9_-]+\s+['"][^'";&|`$()]*['"]$/,
                    'db query': /^['"]SELECT\s+[^'";&|`$()]*['"]$/
                },
                timeout: 30000,
                maxOutputSize: 1024 * 1024 // 1MB
            }],
            
            // Node.js and npm commands
            ['node', {
                binary: 'node',
                allowedSubcommands: [
                    '--version',
                    '-e'
                ],
                parameterValidation: {
                    '-e': /^['"]console\.log\([^'";&|`$()]*\)['"]$/
                },
                timeout: 10000,
                maxOutputSize: 64 * 1024 // 64KB
            }],
            
            // Playwright commands
            ['npx', {
                binary: 'npx',
                allowedSubcommands: [
                    'playwright install',
                    'playwright test'
                ],
                parameterValidation: {
                    'playwright test': /^[a-zA-Z0-9/_.-]+\.js$/
                },
                timeout: 120000,
                maxOutputSize: 10 * 1024 * 1024 // 10MB
            }],

            // System information commands (read-only)
            ['ls', {
                binary: 'ls',
                allowedSubcommands: ['-la', '-l', ''],
                parameterValidation: {
                    '': /^[a-zA-Z0-9/_.-]+$/,
                    '-l': /^[a-zA-Z0-9/_.-]+$/,
                    '-la': /^[a-zA-Z0-9/_.-]+$/
                },
                timeout: 5000,
                maxOutputSize: 64 * 1024
            }],

            ['cat', {
                binary: 'cat',
                allowedSubcommands: [''],
                parameterValidation: {
                    '': /^[a-zA-Z0-9/_.-]+\.(log|txt|json)$/
                },
                timeout: 5000,
                maxOutputSize: 1024 * 1024
            }]
        ]);
    }

    /**
     * Execute a command securely with validation and isolation
     * @param {string} command - Command to execute
     * @param {Array} args - Command arguments
     * @param {Object} options - Execution options
     * @returns {Promise<Object>} Execution result
     */
    async executeCommand(command, args = [], options = {}) {
        // Validate command is in allowlist
        const commandConfig = this.allowedCommands.get(command);
        if (!commandConfig) {
            throw new Error(`Command not allowed: ${command}`);
        }

        // Validate subcommand and parameters
        await this.validateCommandParameters(command, args, commandConfig);

        // Prepare execution environment
        const executionOptions = this.prepareExecutionOptions(commandConfig, options);

        // Log command execution attempt
        const executionId = this.logCommandExecution(command, args, options);

        try {
            // Execute command with isolation
            const result = await this.executeWithIsolation(
                commandConfig.binary,
                args,
                executionOptions
            );

            // Log successful execution
            this.logCommandResult(executionId, 'SUCCESS', result);

            return result;
        } catch (error) {
            // Log failed execution
            this.logCommandResult(executionId, 'FAILED', { error: error.message });
            throw error;
        }
    }

    /**
     * Validate command parameters against allowlist patterns
     * @param {string} command - Base command
     * @param {Array} args - Command arguments
     * @param {Object} commandConfig - Command configuration
     */
    async validateCommandParameters(command, args, commandConfig) {
        if (args.length === 0) return;

        const subcommand = args[0];
        const fullSubcommand = args.join(' ');

        // Check if subcommand is allowed
        const isAllowed = commandConfig.allowedSubcommands.some(allowed => {
            if (allowed === '') return true;
            if (allowed === subcommand) return true;
            if (fullSubcommand.startsWith(allowed)) return true;
            return false;
        });

        if (!isAllowed) {
            throw new Error(`Subcommand not allowed: ${command} ${fullSubcommand}`);
        }

        // Validate parameters against patterns
        if (commandConfig.parameterValidation) {
            for (const [pattern, regex] of Object.entries(commandConfig.parameterValidation)) {
                if (fullSubcommand.startsWith(pattern)) {
                    const parameterPart = fullSubcommand.substring(pattern.length).trim();
                    if (parameterPart && !regex.test(parameterPart)) {
                        throw new Error(`Invalid parameters for ${command} ${pattern}: ${parameterPart}`);
                    }
                }
            }
        }

        // Additional security checks
        await this.performSecurityChecks(args);
    }

    /**
     * Perform additional security checks on command arguments
     * @param {Array} args - Command arguments
     */
    async performSecurityChecks(args) {
        const dangerousPatterns = [
            /[;&|`$(){}]/,          // Shell metacharacters
            /\.\./,                 // Directory traversal
            /\/etc\/passwd/,        // System file access
            /rm\s+-rf/,            // Destructive commands
            /sudo/,                // Privilege escalation
            /chmod/,               // Permission changes
            /curl.*http/,          // External network access
            /wget.*http/           // External downloads
        ];

        const fullCommand = args.join(' ');
        for (const pattern of dangerousPatterns) {
            if (pattern.test(fullCommand)) {
                throw new Error(`Security violation: dangerous pattern detected in command`);
            }
        }
    }

    /**
     * Prepare secure execution options
     * @param {Object} commandConfig - Command configuration
     * @param {Object} userOptions - User-provided options
     * @returns {Object} Execution options
     */
    prepareExecutionOptions(commandConfig, userOptions) {
        return {
            timeout: commandConfig.timeout,
            maxBuffer: commandConfig.maxOutputSize,
            env: this.createSecureEnvironment(),
            shell: false, // Disable shell to prevent injection
            stdio: ['pipe', 'pipe', 'pipe'],
            ...userOptions
        };
    }

    /**
     * Create secure execution environment
     * @returns {Object} Environment variables
     */
    createSecureEnvironment() {
        // Start with minimal environment
        const secureEnv = {
            PATH: '/usr/local/bin:/usr/bin:/bin',
            NODE_ENV: process.env.NODE_ENV || 'development',
            HOME: process.env.HOME
        };

        // Add WordPress CLI path if needed
        if (process.env.WP_CLI_PATH) {
            secureEnv.WP_CLI_PATH = process.env.WP_CLI_PATH;
        }

        return secureEnv;
    }

    /**
     * Execute command with proper isolation
     * @param {string} binary - Command binary
     * @param {Array} args - Arguments
     * @param {Object} options - Execution options
     * @returns {Promise<Object>} Execution result
     */
    async executeWithIsolation(binary, args, options) {
        return new Promise((resolve, reject) => {
            const startTime = Date.now();
            let stdout = '';
            let stderr = '';

            const child = spawn(binary, args, options);

            // Set up timeout
            const timeoutId = setTimeout(() => {
                child.kill('SIGTERM');
                reject(new Error(`Command timeout after ${options.timeout}ms`));
            }, options.timeout);

            // Collect output with size limits
            child.stdout.on('data', (data) => {
                stdout += data.toString();
                if (stdout.length > options.maxBuffer) {
                    child.kill('SIGTERM');
                    reject(new Error('Output size limit exceeded'));
                }
            });

            child.stderr.on('data', (data) => {
                stderr += data.toString();
                if (stderr.length > options.maxBuffer) {
                    child.kill('SIGTERM');
                    reject(new Error('Error output size limit exceeded'));
                }
            });

            child.on('close', (code) => {
                clearTimeout(timeoutId);
                const duration = Date.now() - startTime;

                if (code === 0) {
                    resolve({
                        stdout: stdout.trim(),
                        stderr: stderr.trim(),
                        exitCode: code,
                        duration
                    });
                } else {
                    reject(new Error(`Command failed with exit code ${code}: ${stderr}`));
                }
            });

            child.on('error', (error) => {
                clearTimeout(timeoutId);
                reject(error);
            });
        });
    }

    /**
     * Log command execution for audit trail
     * @param {string} command - Command name
     * @param {Array} args - Arguments
     * @param {Object} options - Options
     * @returns {string} Execution ID
     */
    logCommandExecution(command, args, options) {
        const executionId = require('crypto').randomUUID();
        const logEntry = {
            executionId,
            timestamp: new Date().toISOString(),
            command,
            args: args.map(arg => this.sanitizeForLogging(arg)),
            options: this.sanitizeOptionsForLogging(options),
            status: 'STARTED'
        };

        this.executionLog.push(logEntry);
        this.credentialManager.auditLogger('COMMAND_EXECUTION_STARTED', logEntry);

        return executionId;
    }

    /**
     * Log command execution result
     * @param {string} executionId - Execution ID
     * @param {string} status - Execution status
     * @param {Object} result - Execution result
     */
    logCommandResult(executionId, status, result) {
        const logEntry = {
            executionId,
            timestamp: new Date().toISOString(),
            status,
            duration: result.duration,
            exitCode: result.exitCode,
            outputSize: result.stdout ? result.stdout.length : 0,
            errorSize: result.stderr ? result.stderr.length : 0
        };

        this.executionLog.push(logEntry);
        this.credentialManager.auditLogger('COMMAND_EXECUTION_COMPLETED', logEntry);
    }

    /**
     * Sanitize arguments for logging (remove sensitive data)
     * @param {string} arg - Argument to sanitize
     * @returns {string} Sanitized argument
     */
    sanitizeForLogging(arg) {
        // Remove potential passwords and sensitive data
        return arg.replace(/password[=:]\s*[^\s]+/gi, 'password=***')
                  .replace(/token[=:]\s*[^\s]+/gi, 'token=***')
                  .replace(/key[=:]\s*[^\s]+/gi, 'key=***');
    }

    /**
     * Sanitize execution options for logging
     * @param {Object} options - Options to sanitize
     * @returns {Object} Sanitized options
     */
    sanitizeOptionsForLogging(options) {
        const sanitized = { ...options };
        if (sanitized.env) {
            sanitized.env = Object.keys(sanitized.env);
        }
        return sanitized;
    }

    /**
     * WordPress-specific secure command execution
     * @param {string} wpCommand - WP-CLI command
     * @param {Array} args - Additional arguments
     * @returns {Promise<Object>} Command result
     */
    async executeWordPressCommand(wpCommand, args = []) {
        const wpArgs = wpCommand.split(' ').concat(args);
        return await this.executeCommand('wp', wpArgs);
    }

    /**
     * Get execution audit log
     * @returns {Array} Complete execution log
     */
    getExecutionLog() {
        return [...this.executionLog];
    }

    /**
     * Clear execution log (for cleanup)
     */
    clearExecutionLog() {
        this.executionLog = [];
    }
}

// Singleton instance
let commandExecutorInstance = null;

/**
 * Get singleton instance of SecureCommandExecutor
 * @returns {SecureCommandExecutor}
 */
function getCommandExecutor() {
    if (!commandExecutorInstance) {
        commandExecutorInstance = new SecureCommandExecutor();
    }
    return commandExecutorInstance;
}

module.exports = {
    SecureCommandExecutor,
    getCommandExecutor
};