<?php
namespace EasyChat\Library\Messenger;









class SchedulerCliSupport {
    private const MIN_PHP_VERSION_ID = 70400;

    public static function minPhpVersionId(): int {
        return self::MIN_PHP_VERSION_ID;
    }

    


    public static function commandProfile(string $script_path, array $required_extensions = []): array {
        $runtime = self::detectPhpCli($script_path, $required_extensions);
        $simple_command = '';
        $protected_command = '';
        $flock = self::detectFlockBinary();
        $chain = [
            'success' => false,
            'error' => 'php_cli_unavailable',
            'message' => 'Сначала подтвердите совместимый PHP CLI.',
        ];

        if (!empty($runtime['success'])) {
            $simple_command = self::buildPhpCommand((string)$runtime['binary'], $script_path)
                . ' >> /dev/null 2>&1';
            if (!empty($flock['success'])) {
                $chain = self::probeProtectedChain((string)$flock['binary'], (string)$runtime['binary']);
                if (!empty($chain['success'])) {
                    $protected_command = self::buildFlockCommand(
                        (string)$flock['binary'],
                        (string)$runtime['binary'],
                        $script_path,
                        !empty($chain['conflict_exit_normalized'])
                    );
                }
            } else {
                $chain = $flock;
            }
        }

        return [
            'success' => !empty($runtime['success']),
            'runtime' => $runtime,
            'flock' => $flock,
            'protected_chain' => $chain,
            'protected_command' => $protected_command,
            'simple_command' => $simple_command,
            'script_path' => $script_path,
        ];
    }

    public static function cronCommand(string $script_path): string {
        return (string)(self::commandProfile($script_path)['protected_command'] ?? '');
    }

    public static function cronCommandSimple(string $script_path): string {
        return (string)(self::commandProfile($script_path)['simple_command'] ?? '');
    }

    public static function flockCommand(string $script_path): string {
        return self::cronCommand($script_path);
    }

    public static function phpCommand(string $script_path): string {
        $runtime = self::detectPhpCli($script_path);
        if (empty($runtime['success']) || empty($runtime['binary'])) {
            return '';
        }

        return self::buildPhpCommand((string)$runtime['binary'], $script_path);
    }

    public static function flockBinaryForCommand(): string {
        $detected = self::detectFlockBinary();
        return !empty($detected['success']) ? (string)($detected['binary'] ?? '') : '';
    }

    public static function phpBinaryForCommand(string $script_path = ''): string {
        $detected = self::detectPhpCli($script_path);
        return !empty($detected['success']) ? (string)($detected['binary'] ?? '') : '';
    }

    


    public static function detectPhpCli(string $script_path = '', array $required_extensions = []): array {
        static $cache = [];

        $script_path = trim($script_path);
        $required_extensions = self::normalizeRequiredExtensions($required_extensions);
        $cache_key = hash(
            'sha256',
            self::configuredDatabaseDriver() . "\0" . $script_path . "\0" . implode(',', $required_extensions)
        );
        if (isset($cache[$cache_key]) && is_array($cache[$cache_key])) {
            return $cache[$cache_key];
        }

        if (!function_exists('exec')) {
            return $cache[$cache_key] = [
                'success' => false,
                'error' => 'exec_disabled',
                'message' => 'Хостинг запретил автоматическую проверку PHP CLI. EasyChat не будет предлагать неподтверждённую команду.',
            ];
        }

        if ($script_path !== '' && (!is_file($script_path) || !is_readable($script_path))) {
            return $cache[$cache_key] = [
                'success' => false,
                'error' => 'cron_entrypoint_unavailable',
                'message' => 'Файл планировщика отсутствует или недоступен для чтения. Проверьте полноту обновления EasyChat.',
            ];
        }

        $database_driver_missing = false;
        $runtime_capability_missing = false;
        $missing_required_extension = '';
        $entrypoint_invalid = false;
        $required_database_extension = self::requiredDatabaseExtension();

        foreach (self::candidateBinaries() as $candidate) {
            $probe = self::probePhpCli($candidate, '', $script_path, $required_extensions);
            if (!empty($probe['success'])) {
                return $cache[$cache_key] = $probe;
            }
            if (($probe['error'] ?? '') === 'database_driver_missing') {
                $database_driver_missing = true;
            } elseif (($probe['error'] ?? '') === 'runtime_capability_missing') {
                $runtime_capability_missing = true;
            } elseif (($probe['error'] ?? '') === 'required_extension_missing') {
                $runtime_capability_missing = true;
                $missing_required_extension = (string)($probe['required_extension'] ?? '');
            } elseif (($probe['error'] ?? '') === 'entrypoint_lint_failed') {
                $entrypoint_invalid = true;
            }
        }

        if ($entrypoint_invalid) {
            return $cache[$cache_key] = [
                'success' => false,
                'error' => 'cron_entrypoint_invalid',
                'message' => 'PHP CLI найден, но целевой файл EasyChat не прошёл проверку синтаксиса. Обновление runtime могло завершиться не полностью.',
            ];
        }

        if ($database_driver_missing) {
            $extension_label = $required_database_extension !== ''
                ? $required_database_extension
                : 'драйвер MySQL';
            return $cache[$cache_key] = [
                'success' => false,
                'error' => 'php_cli_database_driver_missing',
                'required_extension' => $required_database_extension,
                'message' => 'PHP CLI 7.4+ найден, но в нём нет расширения '
                    . $extension_label
                    . '. mysqlnd без mysqli не даёт EasyChat доступ к базе. '
                    . 'Выберите другой PHP CLI или включите расширение для CLI в панели хостинга.',
            ];
        }

        if ($runtime_capability_missing) {
            $detail = $missing_required_extension !== ''
                ? ' В CLI отсутствует расширение ' . $missing_required_extension . '.'
                : '';
            return $cache[$cache_key] = [
                'success' => false,
                'error' => 'php_cli_runtime_capability_missing',
                'required_extension' => $missing_required_extension,
                'message' => 'PHP CLI 7.4+ найден, но в нём нет обязательных возможностей сети, хранения или шифрования.'
                    . $detail . ' Выберите полноценную CLI-сборку PHP.',
            ];
        }

        return $cache[$cache_key] = [
            'success' => false,
            'error' => 'php_cli_74_missing',
            'message' => 'Не найден подтверждённый PHP CLI 7.4 или выше. EasyChat не подставляет короткое имя PHP наугад.',
        ];
    }

    


    public static function detectFlockBinary(): array {
        static $cached_result = null;
        if (is_array($cached_result)) {
            return $cached_result;
        }

        if (!function_exists('exec')) {
            return $cached_result = [
                'success' => false,
                'error' => 'exec_disabled',
                'message' => 'Хостинг запретил проверку системного flock.',
            ];
        }

        foreach (self::candidateFlockBinaries() as $candidate) {
            $executable = self::resolveExecutableProfile($candidate);
            if (empty($executable['binary'])) {
                continue;
            }
            $binary = (string)$executable['binary'];

            $output = [];
            $code = 0;
            self::executeQuietly(self::shellWord($binary) . ' --version 2>/dev/null', $output, $code);
            if ($code === 0 && self::executableProfileIsCurrent($executable)) {
                return $cached_result = [
                    'success' => true,
                    'binary' => $binary,
                ];
            }
        }

        return $cached_result = [
            'success' => false,
            'error' => 'flock_missing',
            'message' => 'Системный flock не найден или не запускается. Доступен только короткий вариант команды без внешней блокировки.',
        ];
    }

    private static function candidateBinaries(): array {
        $candidates = [];
        if (defined('EASYCHAT_PHP_CLI') && trim((string)EASYCHAT_PHP_CLI) !== '') {
            $candidates[] = trim((string)EASYCHAT_PHP_CLI);
        }

        $versions = [];
        if (PHP_VERSION_ID >= self::MIN_PHP_VERSION_ID) {
            $versions[] = PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION;
        }
        foreach (['8.4', '8.3', '8.2', '8.1', '8.0', '7.4'] as $version) {
            if (!in_array($version, $versions, true)) {
                $versions[] = $version;
            }
        }

        foreach ($versions as $version) {
            $compact = str_replace('.', '', $version);
             
             
             
            $candidates[] = '/usr/local/bin/php' . $version;
            $candidates[] = '/usr/local/bin/php' . $compact;
            $candidates[] = '/usr/local/php/cgi/' . $version . '/bin/php';
            $candidates[] = '/usr/local/php-cgi/' . $version . '/bin/php';
            $candidates[] = '/usr/local/php-cgi/' . $version . '/bin/php-script';
            $candidates[] = '/usr/bin/php' . $version;
            $candidates[] = '/usr/bin/php' . $compact;
            $candidates[] = '/opt/php' . $compact . '/bin/php';
            $candidates[] = '/opt/php/' . $version . '/bin/php';
            $candidates[] = 'php' . $version;
            $candidates[] = 'php' . $compact;
        }

        if (defined('PHP_BINDIR') && PHP_BINDIR !== '') {
            $candidates[] = rtrim((string)PHP_BINDIR, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'php';
        }
        if (defined('PHP_BINARY') && PHP_BINARY !== '') {
            $candidates[] = (string)PHP_BINARY;
        }

         
         
        $candidates[] = '/usr/local/bin/php';
        $candidates[] = '/usr/bin/php';
        $candidates[] = 'php';

        return self::uniqueStrings($candidates);
    }

    private static function candidateFlockBinaries(): array {
        return self::uniqueStrings([
            '/usr/bin/flock',
            '/bin/flock',
            '/usr/local/bin/flock',
            'flock',
        ]);
    }

    


    private static function probePhpCli(
        string $binary,
        string $database_driver = '',
        string $script_path = '',
        array $required_extensions = []
    ): array {
        $executable = self::resolveExecutableProfile($binary);
        if (empty($executable['binary']) || empty($executable['resolved_binary'])) {
            return ['success' => false, 'error' => 'binary_not_executable', 'binary' => $binary];
        }
        $invocation = (string)$executable['binary'];
        $resolved = (string)$executable['resolved_binary'];

        $name = strtolower(basename($resolved));
        if (strpos($name, 'cgi') !== false || strpos($name, 'fpm') !== false) {
            return [
                'success' => false,
                'error' => 'not_cli_binary',
                'binary' => $invocation,
                'resolved_binary' => $resolved,
            ];
        }

        $database_driver = $database_driver !== ''
            ? strtolower(trim($database_driver))
            : self::configuredDatabaseDriver();
        $required_extensions = self::normalizeRequiredExtensions($required_extensions);
        $probe_code = '$driver=' . var_export($database_driver, true) . ';'
            . '$requiredExtensions=' . var_export($required_extensions, true) . ';'
            . '$databaseReady=($driver==="mysqli" ? class_exists("mysqli")'
            . ':($driver==="mpdo" ? class_exists("PDO") && in_array("mysql", PDO::getAvailableDrivers(), true)'
            . ':($driver==="mysql" ? function_exists("mysql_connect") : true)));'
            . '$extraReady=true;$missing="";foreach($requiredExtensions as $extension){'
            . '$loaded=$extension==="pdo_mysql"'
            . '?class_exists("PDO")&&in_array("mysql",PDO::getAvailableDrivers(),true)'
            . ':extension_loaded($extension);'
            . 'if(!$loaded){$extraReady=false;$missing=$extension;break;}}'
            . '$networkReady=function_exists("stream_socket_client");'
            . '$cryptoReady=extension_loaded("openssl");'
            . '$jsonReady=function_exists("json_encode") && function_exists("json_decode");'
            . 'echo PHP_SAPI."|".PHP_VERSION_ID."|".PHP_VERSION."|".($databaseReady?"1":"0")'
            . '."|".($networkReady?"1":"0")."|".($cryptoReady?"1":"0")."|".($jsonReady?"1":"0")'
            . '."|".($extraReady?"1":"0")."|".$missing;';
        $output = [];
        $code = 0;
        self::executeQuietly(
            self::shellWord($invocation) . ' -r ' . escapeshellarg($probe_code) . ' 2>/dev/null',
            $output,
            $code
        );
        if ($code !== 0 || empty($output[0])) {
            return [
                'success' => false,
                'error' => 'probe_failed',
                'binary' => $invocation,
                'resolved_binary' => $resolved,
            ];
        }

        $parts = explode('|', (string)$output[0]);
        $sapi = (string)($parts[0] ?? '');
        $version_id = (int)($parts[1] ?? 0);
        $version = (string)($parts[2] ?? '');
        $database_ready = (string)($parts[3] ?? '') === '1';
        $network_ready = (string)($parts[4] ?? '') === '1';
        $crypto_ready = (string)($parts[5] ?? '') === '1';
        $json_ready = (string)($parts[6] ?? '') === '1';
        $extra_ready = (string)($parts[7] ?? '') === '1';
        $missing_extension = preg_replace('/[^a-z0-9_]/', '', strtolower((string)($parts[8] ?? '')));

        if ($sapi !== 'cli') {
            return [
                'success' => false,
                'error' => 'sapi_not_cli',
                'binary' => $invocation,
                'resolved_binary' => $resolved,
                'sapi' => $sapi,
            ];
        }
        if ($version_id < self::MIN_PHP_VERSION_ID) {
            return [
                'success' => false,
                'error' => 'php_version_too_old',
                'binary' => $invocation,
                'resolved_binary' => $resolved,
                'version' => $version,
                'version_id' => $version_id,
            ];
        }
        if (!$database_ready) {
            return [
                'success' => false,
                'error' => 'database_driver_missing',
                'binary' => $invocation,
                'resolved_binary' => $resolved,
                'version' => $version,
                'version_id' => $version_id,
                'required_extension' => self::requiredDatabaseExtension($database_driver),
            ];
        }
        if (!$network_ready || !$crypto_ready || !$json_ready) {
            return [
                'success' => false,
                'error' => 'runtime_capability_missing',
                'binary' => $invocation,
                'resolved_binary' => $resolved,
                'version' => $version,
                'version_id' => $version_id,
                'network_ready' => $network_ready,
                'crypto_ready' => $crypto_ready,
                'json_ready' => $json_ready,
            ];
        }
        if (!$extra_ready) {
            return [
                'success' => false,
                'error' => 'required_extension_missing',
                'binary' => $invocation,
                'resolved_binary' => $resolved,
                'version' => $version,
                'version_id' => $version_id,
                'required_extension' => $missing_extension,
            ];
        }

        if ($script_path !== '') {
            $lint_output = [];
            $lint_code = 0;
            self::executeQuietly(
                self::shellWord($invocation) . ' -l ' . escapeshellarg($script_path) . ' 2>/dev/null',
                $lint_output,
                $lint_code
            );
            if ($lint_code !== 0) {
                return [
                    'success' => false,
                    'error' => 'entrypoint_lint_failed',
                    'binary' => $invocation,
                    'resolved_binary' => $resolved,
                    'version' => $version,
                    'version_id' => $version_id,
                ];
            }
        }

         
         
         
        if (!self::executableProfileIsCurrent($executable)) {
            return [
                'success' => false,
                'error' => 'binary_changed_during_probe',
                'binary' => $invocation,
                'resolved_binary' => $resolved,
            ];
        }

        return [
            'success' => true,
            'binary' => $invocation,
            'resolved_binary' => $resolved,
            'version' => $version,
            'version_id' => $version_id,
            'database_driver' => $database_driver,
            'database_extension' => self::requiredDatabaseExtension($database_driver),
            'network_ready' => true,
            'crypto_ready' => true,
            'json_ready' => true,
            'required_extensions' => $required_extensions,
            'entrypoint_verified' => $script_path !== '',
        ];
    }

    private static function probeProtectedChain(string $flock_binary, string $php_binary): array {
        $flock_executable = self::resolveExecutableProfile($flock_binary);
        $php_executable = self::resolveExecutableProfile($php_binary);
        if (empty($flock_executable['binary']) || empty($php_executable['binary'])) {
            return [
                'success' => false,
                'error' => 'flock_php_chain_changed',
                'message' => 'Путь PHP или flock изменился до совместной проверки. Обновите страницу и повторите диагностику.',
            ];
        }
        $flock_binary = (string)$flock_executable['binary'];
        $php_binary = (string)$php_executable['binary'];

        static $cache = [];
        $cache_key = hash(
            'sha256',
            $flock_binary
                . "\0"
                . json_encode($flock_executable['identity'] ?? [])
                . "\0"
                . $php_binary
                . "\0"
                . json_encode($php_executable['identity'] ?? [])
        );
        if (isset($cache[$cache_key])) {
            return $cache[$cache_key];
        }

        $temp_dir = sys_get_temp_dir();
        $lock_file = tempnam($temp_dir, 'easychat-cron-probe-');
        $exit_lock_file = tempnam($temp_dir, 'easychat-cron-exit-');
        if ($lock_file === false || $exit_lock_file === false) {
            if (is_string($lock_file)) {
                self::removeProbeFile($lock_file);
            }
            if (is_string($exit_lock_file)) {
                self::removeProbeFile($exit_lock_file);
            }
            return $cache[$cache_key] = [
                'success' => false,
                'error' => 'flock_probe_storage_unavailable',
                'message' => 'Не удалось безопасно проверить совместный запуск flock и PHP.',
            ];
        }

        $permissions_ready = (bool)self::quietRuntimeCall(static function () use ($lock_file, $exit_lock_file) {
            return chmod($lock_file, 0600) && chmod($exit_lock_file, 0600);
        }, false);
        if (!$permissions_ready) {
            self::removeProbeFile($lock_file);
            self::removeProbeFile($exit_lock_file);
            return $cache[$cache_key] = [
                'success' => false,
                'error' => 'flock_probe_storage_unavailable',
                'message' => 'Не удалось защитить служебный lock-файл для проверки flock.',
            ];
        }

        $marker = 'EASYCHAT_CRON_CHAIN_OK';
        $output = [];
        $code = 0;
        $command = self::shellWord($flock_binary)
            . ' -n ' . escapeshellarg($lock_file)
            . ' ' . self::shellWord($php_binary)
            . ' -r ' . escapeshellarg('echo ' . var_export($marker, true) . ';')
            . ' 2>/dev/null';
        self::executeQuietly($command, $output, $code);

        if ($code !== 0 || trim(implode("\n", $output)) !== $marker) {
            self::removeProbeFile($lock_file);
            self::removeProbeFile($exit_lock_file);
            return $cache[$cache_key] = [
                'success' => false,
                'error' => 'flock_php_chain_failed',
                'message' => 'PHP и flock найдены отдельно, но их совместный запуск не подтвердился. Используйте короткий вариант команды.',
            ];
        }

        $conflict_exit_normalized = self::probeFlockConflictExit(
            $flock_binary,
            $php_binary,
            $lock_file,
            $exit_lock_file
        );
        $lock_cleanup_ready = self::removeProbeFile($lock_file);
        $exit_lock_cleanup_ready = self::removeProbeFile($exit_lock_file);
        $cleanup_ready = $lock_cleanup_ready && $exit_lock_cleanup_ready;
        if (!$cleanup_ready) {
            return $cache[$cache_key] = [
                'success' => false,
                'error' => 'flock_probe_cleanup_failed',
                'message' => 'Не удалось удалить служебный lock-файл после проверки flock.',
            ];
        }

        if (!self::executableProfileIsCurrent($flock_executable)
            || !self::executableProfileIsCurrent($php_executable)) {
            return $cache[$cache_key] = [
                'success' => false,
                'error' => 'flock_php_chain_changed',
                'message' => 'Путь PHP или flock изменился во время проверки. EasyChat не покажет потенциально подменённую команду.',
            ];
        }

        return $cache[$cache_key] = [
            'success' => true,
            'conflict_exit_normalized' => $conflict_exit_normalized,
            'message' => $conflict_exit_normalized
                ? 'PHP CLI и flock проверены: пересекающийся запуск безопасно пропускается без ложной ошибки.'
                : 'PHP CLI и flock проверены совместным запуском, но этот flock не поддерживает тихий код пересечения.',
        ];
    }

    



    private static function probeFlockConflictExit(
        string $flock_binary,
        string $php_binary,
        string $lock_file,
        string $exit_lock_file
    ): bool {
        $handle = self::quietRuntimeCall(static function () use ($lock_file) {
            return fopen($lock_file, 'c+b');
        }, false);
        if (!is_resource($handle)) {
            return false;
        }

        $locked = (bool)self::quietRuntimeCall(static function () use ($handle) {
            return flock($handle, LOCK_EX | LOCK_NB);
        }, false);
        if (!$locked) {
            self::quietRuntimeCall(static function () use ($handle) {
                fclose($handle);
                return true;
            }, false);
            return false;
        }

        $conflict_output = [];
        $conflict_code = -1;
        self::executeQuietly(
            self::shellWord($flock_binary)
                . ' -n -E 0 ' . escapeshellarg($lock_file)
                . ' ' . self::shellWord($php_binary)
                . ' -r ' . escapeshellarg('echo "UNEXPECTED";')
                . ' 2>/dev/null',
            $conflict_output,
            $conflict_code
        );

        self::quietRuntimeCall(static function () use ($handle) {
            flock($handle, LOCK_UN);
            fclose($handle);
            return true;
        }, false);

        $php_error_output = [];
        $php_error_code = -1;
        self::executeQuietly(
            self::shellWord($flock_binary)
                . ' -n -E 0 ' . escapeshellarg($exit_lock_file)
                . ' ' . self::shellWord($php_binary)
                . ' -r ' . escapeshellarg('exit(42);')
                . ' 2>/dev/null',
            $php_error_output,
            $php_error_code
        );

        return $conflict_code === 0
            && trim(implode("\n", $conflict_output)) === ''
            && $php_error_code === 42;
    }

    





    private static function resolveExecutableProfile(string $candidate): array {
        $candidate = trim($candidate);
        if ($candidate === '' || preg_match('/[\r\n\0]/', $candidate)) {
            return [];
        }

        $path = '';
        if (strpos($candidate, DIRECTORY_SEPARATOR) !== false) {
            if ($candidate[0] === DIRECTORY_SEPARATOR && is_file($candidate) && is_executable($candidate)) {
                $path = $candidate;
            }
        } elseif (preg_match('/^[A-Za-z0-9_.+-]+$/', $candidate)) {
            $output = [];
            $code = 0;
            self::executeQuietly('command -v ' . escapeshellarg($candidate) . ' 2>/dev/null', $output, $code);
            $command_path = $code === 0 && !empty($output[0]) ? trim((string)$output[0]) : '';
            if ($command_path !== ''
                && $command_path[0] === DIRECTORY_SEPARATOR
                && is_file($command_path)
                && is_executable($command_path)) {
                $path = $command_path;
            }
        }

        $resolved = $path !== '' ? realpath($path) : false;
        if ($resolved === false
            || $resolved === ''
            || $resolved[0] !== DIRECTORY_SEPARATOR
            || preg_match('/[\r\n\0]/', $path . $resolved)) {
            return [];
        }

        $identity = self::executableIdentity($path, $resolved);
        if ($identity === []) {
            return [];
        }

        return [
            'binary' => $path,
            'resolved_binary' => $resolved,
            'identity' => $identity,
        ];
    }

    private static function executableProfileIsCurrent(array $profile): bool {
        $path = (string)($profile['binary'] ?? '');
        $resolved = (string)($profile['resolved_binary'] ?? '');
        $identity = (array)($profile['identity'] ?? []);
        if ($path === '' || $resolved === '' || $identity === []) {
            return false;
        }

        return self::executableIdentity($path, $resolved) === $identity;
    }

    


    private static function executableIdentity(string $path, string $resolved): array {
        $path_stat = self::quietRuntimeCall(static function () use ($path) {
            return lstat($path);
        }, false);
        $target_stat = self::quietRuntimeCall(static function () use ($resolved) {
            return stat($resolved);
        }, false);
        if (!is_array($path_stat) || !is_array($target_stat)) {
            return [];
        }

        $current_resolved = realpath($path);
        if ($current_resolved === false || $current_resolved !== $resolved) {
            return [];
        }

        return [
            'path_dev' => (int)($path_stat['dev'] ?? -1),
            'path_ino' => (int)($path_stat['ino'] ?? -1),
            'path_mode' => (int)($path_stat['mode'] ?? 0),
            'path_size' => (int)($path_stat['size'] ?? -1),
            'path_mtime' => (int)($path_stat['mtime'] ?? -1),
            'path_ctime' => (int)($path_stat['ctime'] ?? -1),
            'target_dev' => (int)($target_stat['dev'] ?? -1),
            'target_ino' => (int)($target_stat['ino'] ?? -1),
            'target_mode' => (int)($target_stat['mode'] ?? 0),
            'target_size' => (int)($target_stat['size'] ?? -1),
            'target_mtime' => (int)($target_stat['mtime'] ?? -1),
            'target_ctime' => (int)($target_stat['ctime'] ?? -1),
            'resolved' => $resolved,
        ];
    }

    private static function buildPhpCommand(string $php_binary, string $script_path): string {
        return self::shellWord($php_binary) . ' ' . self::shellWord($script_path);
    }

    



    private static function executeQuietly(string $command, array &$output, int &$code): bool {
        $output = [];
        $code = -1;

        return (bool)self::quietRuntimeCall(static function () use ($command, &$output, &$code) {
            exec($command, $output, $code);
            return true;
        }, false);
    }

    





    private static function quietRuntimeCall(callable $callback, $fallback = false) {
        set_error_handler(static function (): bool {
            return true;
        });
        try {
            return $callback();
        } catch (\Throwable $e) {
            return $fallback;
        } finally {
            restore_error_handler();
        }
    }

    private static function removeProbeFile(string $path): bool {
        if (!is_file($path) && !is_link($path)) {
            return true;
        }

        return (bool)self::quietRuntimeCall(static function () use ($path) {
            return unlink($path);
        }, false);
    }

    private static function buildFlockCommand(
        string $flock_binary,
        string $php_binary,
        string $script_path,
        bool $normalize_conflict_exit
    ): string {
        $paths = self::cronRuntimePaths($script_path);
        return self::shellWord($flock_binary)
            . ($normalize_conflict_exit ? ' -n -E 0 ' : ' -n ')
            . self::shellWord($paths['lock'])
            . ' '
            . self::buildPhpCommand($php_binary, $script_path)
            . ' >> '
            . self::shellWord($paths['log'])
            . ' 2>&1';
    }

    private static function configuredDatabaseDriver(): string {
        $driver = defined('DB_DRIVER') ? strtolower(trim((string)DB_DRIVER)) : 'mysqli';
        return $driver !== '' ? $driver : 'mysqli';
    }

    private static function requiredDatabaseExtension(string $driver = ''): string {
        $driver = $driver !== '' ? strtolower(trim($driver)) : self::configuredDatabaseDriver();
        if ($driver === 'mysqli') {
            return 'mysqli';
        }
        if ($driver === 'mpdo') {
            return 'pdo_mysql';
        }
        if ($driver === 'mysql') {
            return 'mysql';
        }
        return '';
    }

    private static function cronRuntimePaths(string $script_path): array {
        $script_path = trim($script_path);
        $script_dir = dirname($script_path);
        $root = basename($script_dir) === 'cli' ? dirname($script_dir) : $script_dir;
        $log_dir = rtrim($root, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'log';
        $name = strtolower((string)pathinfo($script_path, PATHINFO_FILENAME));
        $name = preg_replace('/[^a-z0-9]+/', '-', $name);
        $name = trim((string)$name, '-');
        if ($name === '' || $name === 'cron') {
            $name = 'main-cron';
        } elseif ($name === 'max-polling') {
            $name = 'max-polling';
        }

        return [
            'lock' => $log_dir . DIRECTORY_SEPARATOR . 'easychat-' . $name . '.lock',
            'log' => $log_dir . DIRECTORY_SEPARATOR . 'easychat-' . $name . '.log',
        ];
    }

    private static function uniqueStrings(array $values): array {
        $unique = [];
        foreach ($values as $value) {
            $value = trim((string)$value);
            if ($value !== '' && !isset($unique[$value])) {
                $unique[$value] = true;
            }
        }
        return array_keys($unique);
    }

    private static function shellWord(string $value): string {
        $value = trim($value);
        if ($value !== '' && preg_match('/^[A-Za-z0-9_@%+=:,.\/-]+$/', $value)) {
            return $value;
        }
        return escapeshellarg($value);
    }

    



    private static function normalizeRequiredExtensions(array $extensions): array {
        $normalized = [];
        foreach ($extensions as $extension) {
            $extension = strtolower(trim((string)$extension));
            if (preg_match('/^[a-z][a-z0-9_]{0,31}$/D', $extension) === 1) {
                $normalized[$extension] = true;
            }
        }
        $result = array_keys($normalized);
        sort($result);
        return $result;
    }
}
