<?php

/**
 * Безопасный исполнитель команд
 *
 * Предоставляет безопасные обертки для exec(), system(), passthru()
 * с валидацией команд и защитой от инъекций
 *
 * ВНИМАНИЕ: Этот класс специально создан для безопасного выполнения системных команд.
 * Использование exec(), system(), passthru() внутри этого класса является частью его функциональности
 * и защищено валидацией и проверками безопасности.
 *
 * @package Flowaxy\Infrastructure\Security
 * @version 1.0.0 Alpha prerelease
 */

declare(strict_types=1);

namespace Flowaxy\Infrastructure\Security;

use Flowaxy\Support\Facades\Log;

final class SafeCommandExecutor
{
    /**
     * Опасные паттерны в командах
     *
     * @var array<string>
     */
    private const DANGEROUS_PATTERNS = [
        ';',           // Разделитель команд
        '&&',          // Логическое И
        '||',          // Логическое ИЛИ
        '|',           // Пайп
        '`',           // Обратные кавычки
        '$',           // Переменные оболочки
        '$(',          // Подстановка команд
        '${',          // Подстановка переменных
        '<',           // Перенаправление ввода
        '>',           // Перенаправление вывода
        '>>',          // Добавление в файл
        '2>&1',        // Перенаправление ошибок
        '&',           // Фоновый процесс
        '\n',          // Новая строка
        '\r',          // Возврат каретки
    ];

    /**
     * Максимальное время выполнения команды (секунды)
     */
    private const MAX_EXECUTION_TIME = 300; // 5 минут

    /**
     * Максимальная длина команды
     */
    private const MAX_COMMAND_LENGTH = 4096;

    /**
     * Белый список разрешенных команд (опционально, из конфига)
     *
     * @var array<string>|null
     */
    private static ?array $whitelist = null;

    /**
     * Безопасное выполнение команды через exec()
     *
     * @param string $command Команда для выполнения
     * @param array<string>|null $output Массив для вывода
     * @param int|null $returnCode Код возврата
     * @return bool true если команда выполнена успешно
     * @throws \RuntimeException Если команда небезопасна
     */
    public static function exec(string $command, ?array &$output = null, ?int &$returnCode = null): bool
    {
        self::validateCommand($command);

        // Логируем вызов
        self::logCommand('exec', $command);

        try {
            // @phpstan-ignore-next-line - это безопасная обертка с валидацией команд
            $result = exec($command, $output ?? [], $returnCode ?? 0);
            return $result !== false;
        } catch (\Throwable $e) {
            Log::Error('SafeCommandExecutor::exec failed', [
                'command' => $command,
                'error' => $e->getMessage(),
            ]);
            throw new \RuntimeException('Command execution failed: ' . $e->getMessage(), 0, $e);
        }
    }

    /**
     * Безопасное выполнение команды через system()
     *
     * @param string $command Команда для выполнения
     * @param int|null $returnCode Код возврата
     * @return string|false Вывод команды или false при ошибке
     * @throws \RuntimeException Если команда небезопасна
     */
    public static function system(string $command, ?int &$returnCode = null): string|false
    {
        self::validateCommand($command);

        // Логируем вызов
        self::logCommand('system', $command);

        try {
            ob_start();
            // @phpstan-ignore-next-line - это безопасная обертка с валидацией команд
            $result = system($command, $returnCode ?? 0);
            $output = ob_get_clean();

            if ($result === false) {
                return false;
            }

            return $output !== false ? $output : $result;
        } catch (\Throwable $e) {
            ob_end_clean();
            Log::Error('SafeCommandExecutor::system failed', [
                'command' => $command,
                'error' => $e->getMessage(),
            ]);
            throw new \RuntimeException('Command execution failed: ' . $e->getMessage(), 0, $e);
        }
    }

    /**
     * Безопасное выполнение команды через passthru()
     *
     * @param string $command Команда для выполнения
     * @param int|null $returnCode Код возврата
     * @return void
     * @throws \RuntimeException Если команда небезопасна
     */
    public static function passthru(string $command, ?int &$returnCode = null): void
    {
        self::validateCommand($command);

        // Логируем вызов
        self::logCommand('passthru', $command);

        try {
            // @phpstan-ignore-next-line - это безопасная обертка с валидацией команд
            passthru($command, $returnCode ?? 0);
        } catch (\Throwable $e) {
            Log::Error('SafeCommandExecutor::passthru failed', [
                'command' => $command,
                'error' => $e->getMessage(),
            ]);
            throw new \RuntimeException('Command execution failed: ' . $e->getMessage(), 0, $e);
        }
    }

    /**
     * Валидация команды на безопасность
     *
     * @param string $command Команда для проверки
     * @return void
     * @throws \RuntimeException Если команда небезопасна
     */
    private static function validateCommand(string $command): void
    {
        // Проверка длины
        if (strlen($command) > self::MAX_COMMAND_LENGTH) {
            throw new \RuntimeException('Command exceeds maximum length');
        }

        // Проверка на пустую команду
        if (trim($command) === '') {
            throw new \RuntimeException('Empty command is not allowed');
        }

        // Проверка на опасные паттерны
        foreach (self::DANGEROUS_PATTERNS as $pattern) {
            if (str_contains($command, $pattern)) {
                throw new \RuntimeException("Command contains dangerous pattern: {$pattern}");
            }
        }

        // Проверка белого списка (если настроен)
        $whitelist = self::getWhitelist();
        if ($whitelist !== null && !empty($whitelist)) {
            $commandBase = self::extractCommandBase($command);
            if (!in_array($commandBase, $whitelist, true)) {
                throw new \RuntimeException("Command '{$commandBase}' is not in whitelist");
            }
        }

        // Дополнительная проверка на инъекции
        if (self::containsInjection($command)) {
            throw new \RuntimeException('Command contains potential injection pattern');
        }
    }

    /**
     * Извлечь базовую команду (без аргументов)
     */
    private static function extractCommandBase(string $command): string
    {
        $command = trim($command);
        $parts = explode(' ', $command, 2);
        return $parts[0];
    }

    /**
     * Проверка на наличие инъекций
     */
    private static function containsInjection(string $command): bool
    {
        // Проверка на подозрительные комбинации
        $suspiciousPatterns = [
            '/\$\([^)]*\)/',           // $(command)
            '/`[^`]*`/',               // `command`
            '/\$\{[^}]*\}/',           // ${variable}
            '/\|\s*\w+\s*\|/',         // | command |
            '/;\s*\w+/',               // ; command
            '/&&\s*\w+/',              // && command
            '/\|\|\s*\w+/',            // || command
        ];

        foreach ($suspiciousPatterns as $pattern) {
            if (preg_match($pattern, $command) === 1) {
                return true;
            }
        }

        return false;
    }

    /**
     * Получить белый список команд из конфигурации
     *
     * @return array<string>|null
     */
    private static function getWhitelist(): ?array
    {
        if (self::$whitelist !== null) {
            return self::$whitelist;
        }

        // Загружаем из конфигурации
        if (class_exists(\Flowaxy\Core\System\PathResolver::class)) {
            $configFile = \Flowaxy\Core\System\PathResolver::storageConfig() . DS . 'security.ini';
            if (file_exists($configFile)) {
                $iniConfig = \Flowaxy\Support\Helpers\IniHelper::readFile($configFile, true);
                if (is_array($iniConfig) && isset($iniConfig['command_whitelist'])) {
                    $whitelist = $iniConfig['command_whitelist'];
                    // Преобразуем INI массив в обычный массив (убираем пустые значения)
                    $whitelistArray = [];
                    foreach ($whitelist as $key => $value) {
                        if (!empty($key) && $value !== '') {
                            $whitelistArray[] = $key;
                        }
                    }
                    self::$whitelist = $whitelistArray;
                    return self::$whitelist;
                }
            }
        }

        // Fallback: старая загрузка из PHP (для обратной совместимости)
        $configFile = dirname(__DIR__, 3) . DS . 'config' . DS . 'security.php';
        if (file_exists($configFile)) {
            $config = require $configFile;
            if (is_array($config) && isset($config['command_whitelist']) && is_array($config['command_whitelist'])) {
                self::$whitelist = $config['command_whitelist'];
                return self::$whitelist;
            }
        }

        // По умолчанию белый список не используется
        self::$whitelist = [];
        return null;
    }

    /**
     * Логирование выполнения команды
     */
    private static function logCommand(string $method, string $command): void
    {
        Log::Debug('SafeCommandExecutor::' . $method, [
            'command' => $command,
            'timestamp' => time(),
        ]);
    }

    /**
     * Установить белый список команд (для тестирования)
     *
     * @param array<string>|null $whitelist
     * @return void
     */
    public static function setWhitelist(?array $whitelist): void
    {
        self::$whitelist = $whitelist;
    }
}
