<?php

declare(strict_types=1);

namespace App\Services\SupportCenter;

use App\Enums\SupportCenter\HostSystemUpdateEnvironmentDiagnosticLevel;
use App\Models\HostSystemUpdatePackage;

/**
 * 系统更新中心环境诊断：评估当前 PHP/Web 环境是否适合浏览器内一键更新（含面板加固、共享主机等受限场景）。
 */
final class HostSystemUpdateEnvironmentDiagnosticService
{
    /**
     * @var list<string>
     */
    private const REQUIRED_PROCESS_FUNCTIONS = [
        'proc_open',
        'proc_close',
        'proc_get_status',
        'proc_terminate',
    ];

    public function __construct(
        private readonly HostSystemUpdateVendorStrategyResolver $vendorStrategyResolver,
    ) {}

    public function diagnose(): HostSystemUpdateEnvironmentDiagnosticReport
    {
        /** @var list<HostSystemUpdateEnvironmentDiagnosticItem> $items */
        $items = [];

        $items = array_merge($items, $this->processFunctionItems());
        $items = array_merge($items, $this->extensionItems());
        $items = array_merge($items, $this->filesystemItems());
        $items[] = $this->openBasedirItem();
        $items[] = $this->runtimeUserItem();
        $items = array_merge($items, $this->phpIniLimitItems());
        $items[] = $this->phpBinaryItem();

        $blockingCount = 0;
        $warningCount = 0;

        foreach ($items as $item) {
            if ($item->isBlocking()) {
                $blockingCount++;
            } elseif ($item->isWarning()) {
                $warningCount++;
            }
        }

        $canRunBrowserUpdate = $blockingCount === 0;

        return new HostSystemUpdateEnvironmentDiagnosticReport(
            items: $items,
            canRunBrowserUpdate: $canRunBrowserUpdate,
            blockingCount: $blockingCount,
            warningCount: $warningCount,
            summaryMessage: $this->summaryMessage($canRunBrowserUpdate, $blockingCount, $warningCount),
        );
    }

    /**
     * @return list<HostSystemUpdateEnvironmentDiagnosticItem>
     */
    private function processFunctionItems(): array
    {
        $disabledFunctions = $this->disabledFunctionsList();
        $items = [];

        foreach (self::REQUIRED_PROCESS_FUNCTIONS as $function) {
            $available = $this->isFunctionAvailable($function, $disabledFunctions);
            $items[] = new HostSystemUpdateEnvironmentDiagnosticItem(
                key: 'process_function_'.$function,
                label: (string) __('dberp.system_update_center.environment_diagnostics.items.process_function', [
                    'function' => $function,
                ]),
                status: $available ? 'pass' : 'fail',
                level: $available
                    ? HostSystemUpdateEnvironmentDiagnosticLevel::Info
                    : HostSystemUpdateEnvironmentDiagnosticLevel::Blocking,
                message: $available
                    ? (string) __('dberp.system_update_center.environment_diagnostics.messages.process_function_ok', [
                        'function' => $function,
                    ])
                    : (string) __('dberp.system_update_center.environment_diagnostics.messages.process_function_fail', [
                        'function' => $function,
                    ]),
                suggestion: $available
                    ? ''
                    : (string) __('dberp.system_update_center.environment_diagnostics.suggestions.process_function_fail'),
            );
        }

        return $items;
    }

    /**
     * @return list<HostSystemUpdateEnvironmentDiagnosticItem>
     */
    private function extensionItems(): array
    {
        $zipAvailable = class_exists(ZipArchive::class) || extension_loaded('zip');
        $items = [
            new HostSystemUpdateEnvironmentDiagnosticItem(
                key: 'zip_archive',
                label: (string) __('dberp.system_update_center.environment_diagnostics.items.zip_archive'),
                status: $zipAvailable ? 'pass' : 'fail',
                level: $zipAvailable
                    ? HostSystemUpdateEnvironmentDiagnosticLevel::Info
                    : HostSystemUpdateEnvironmentDiagnosticLevel::Blocking,
                message: $zipAvailable
                    ? (string) __('dberp.system_update_center.environment_diagnostics.messages.zip_archive_ok')
                    : (string) __('dberp.system_update_center.environment_diagnostics.messages.zip_archive_fail'),
                suggestion: $zipAvailable
                    ? ''
                    : (string) __('dberp.system_update_center.environment_diagnostics.suggestions.zip_archive_fail'),
            ),
        ];

        if ($this->tarGzExtractionMayBeUsed()) {
            $pharAvailable = class_exists(\Phar::class) && extension_loaded('phar');
            $items[] = new HostSystemUpdateEnvironmentDiagnosticItem(
                key: 'phar_tar_gz',
                label: (string) __('dberp.system_update_center.environment_diagnostics.items.phar_tar_gz'),
                status: $pharAvailable ? 'pass' : 'warn',
                level: $pharAvailable
                    ? HostSystemUpdateEnvironmentDiagnosticLevel::Info
                    : HostSystemUpdateEnvironmentDiagnosticLevel::Warning,
                message: $pharAvailable
                    ? (string) __('dberp.system_update_center.environment_diagnostics.messages.phar_ok')
                    : (string) __('dberp.system_update_center.environment_diagnostics.messages.phar_warn'),
                suggestion: $pharAvailable
                    ? ''
                    : (string) __('dberp.system_update_center.environment_diagnostics.suggestions.phar_warn'),
            );
        }

        return $items;
    }

    /**
     * @return list<HostSystemUpdateEnvironmentDiagnosticItem>
     */
    private function filesystemItems(): array
    {
        $items = [];

        $workspacePaths = [
            'base_path' => base_path(),
            'update_workspace' => HostSystemUpdateWorkspaceStorage::baseDirectory(),
            'update_packages' => HostSystemUpdateWorkspaceStorage::packagesDirectory(),
            'update_staging' => HostSystemUpdateWorkspaceStorage::baseDirectory().'/staging',
            'update_backups' => HostSystemUpdateWorkspaceStorage::baseDirectory().'/backups',
            'bootstrap_cache' => base_path('bootstrap/cache'),
        ];

        foreach ($workspacePaths as $key => $absolutePath) {
            $writable = $this->isDirectoryWritable($absolutePath);
            $blocking = $this->isBlockingFilesystemKey($key);

            $items[] = new HostSystemUpdateEnvironmentDiagnosticItem(
                key: 'writable_'.$key,
                label: (string) __('dberp.system_update_center.environment_diagnostics.items.writable_'.$key),
                status: $writable ? 'pass' : ($blocking ? 'fail' : 'warn'),
                level: $writable
                    ? HostSystemUpdateEnvironmentDiagnosticLevel::Info
                    : ($blocking
                        ? HostSystemUpdateEnvironmentDiagnosticLevel::Blocking
                        : HostSystemUpdateEnvironmentDiagnosticLevel::Warning),
                message: $writable
                    ? (string) __('dberp.system_update_center.environment_diagnostics.messages.directory_writable', [
                        'path' => $this->displayPath($absolutePath),
                    ])
                    : (string) __('dberp.system_update_center.environment_diagnostics.messages.directory_not_writable', [
                        'path' => $this->displayPath($absolutePath),
                    ]),
                suggestion: $writable
                    ? ''
                    : (string) __('dberp.system_update_center.environment_diagnostics.suggestions.directory_not_writable'),
            );
        }

        $items[] = $this->envFileItem();

        if ($this->requiresProjectFileReplacement()) {
            $items[] = $this->projectFilesWritableItem();
        }

        $vendorItem = $this->vendorWritableItem();

        if ($vendorItem instanceof HostSystemUpdateEnvironmentDiagnosticItem) {
            $items[] = $vendorItem;
        }

        return $items;
    }

    private function envFileItem(): HostSystemUpdateEnvironmentDiagnosticItem
    {
        $envPath = base_path('.env');
        $writable = is_file($envPath) && is_writable($envPath);

        return new HostSystemUpdateEnvironmentDiagnosticItem(
            key: 'writable_env',
            label: (string) __('dberp.system_update_center.environment_diagnostics.items.writable_env'),
            status: $writable ? 'pass' : 'fail',
            level: $writable
                ? HostSystemUpdateEnvironmentDiagnosticLevel::Info
                : HostSystemUpdateEnvironmentDiagnosticLevel::Blocking,
            message: $writable
                ? (string) __('dberp.system_update_center.environment_diagnostics.messages.env_writable')
                : (string) __('dberp.system_update_center.environment_diagnostics.messages.env_not_writable'),
            suggestion: $writable
                ? ''
                : (string) __('dberp.system_update_center.environment_diagnostics.suggestions.env_not_writable'),
        );
    }

    private function projectFilesWritableItem(): HostSystemUpdateEnvironmentDiagnosticItem
    {
        $appPath = app_path();
        $writable = $this->isDirectoryWritable($appPath) && is_writable($appPath);

        return new HostSystemUpdateEnvironmentDiagnosticItem(
            key: 'writable_project_files',
            label: (string) __('dberp.system_update_center.environment_diagnostics.items.writable_project_files'),
            status: $writable ? 'pass' : 'fail',
            level: $writable
                ? HostSystemUpdateEnvironmentDiagnosticLevel::Info
                : HostSystemUpdateEnvironmentDiagnosticLevel::Blocking,
            message: $writable
                ? (string) __('dberp.system_update_center.environment_diagnostics.messages.project_files_writable')
                : (string) __('dberp.system_update_center.environment_diagnostics.messages.project_files_not_writable'),
            suggestion: $writable
                ? ''
                : (string) __('dberp.system_update_center.environment_diagnostics.suggestions.project_files_not_writable'),
        );
    }

    private function vendorWritableItem(): ?HostSystemUpdateEnvironmentDiagnosticItem
    {
        $strategy = $this->resolveExpectedVendorStrategy();

        if ($strategy === HostSystemUpdateVendorStrategyResolver::STRATEGY_NONE) {
            return null;
        }

        $vendorDirectory = base_path('vendor');
        $writable = $this->isDirectoryWritable($vendorDirectory);
        $blocking = $strategy === HostSystemUpdateVendorStrategyResolver::STRATEGY_INCLUDED_VENDOR;

        return new HostSystemUpdateEnvironmentDiagnosticItem(
            key: 'writable_vendor',
            label: (string) __('dberp.system_update_center.environment_diagnostics.items.writable_vendor'),
            status: $writable ? 'pass' : ($blocking ? 'fail' : 'warn'),
            level: $writable
                ? HostSystemUpdateEnvironmentDiagnosticLevel::Info
                : ($blocking
                    ? HostSystemUpdateEnvironmentDiagnosticLevel::Blocking
                    : HostSystemUpdateEnvironmentDiagnosticLevel::Warning),
            message: $writable
                ? (string) __('dberp.system_update_center.environment_diagnostics.messages.vendor_writable')
                : (string) __('dberp.system_update_center.environment_diagnostics.messages.vendor_not_writable'),
            suggestion: $writable
                ? ''
                : (string) __('dberp.system_update_center.environment_diagnostics.suggestions.vendor_not_writable'),
        );
    }

    private function openBasedirItem(): HostSystemUpdateEnvironmentDiagnosticItem
    {
        $restriction = trim((string) ini_get('open_basedir'));

        if ($restriction === '') {
            return new HostSystemUpdateEnvironmentDiagnosticItem(
                key: 'open_basedir',
                label: (string) __('dberp.system_update_center.environment_diagnostics.items.open_basedir'),
                status: 'pass',
                level: HostSystemUpdateEnvironmentDiagnosticLevel::Info,
                message: (string) __('dberp.system_update_center.environment_diagnostics.messages.open_basedir_not_set'),
                suggestion: '',
            );
        }

        $pathsToVerify = [
            base_path(),
            storage_path(),
            HostSystemUpdateWorkspaceStorage::baseDirectory(),
        ];

        $blocked = [];

        foreach ($pathsToVerify as $path) {
            if (! $this->pathAllowedByOpenBasedir($path, $restriction)) {
                $blocked[] = $this->displayPath($path);
            }
        }

        $ok = $blocked === [];

        return new HostSystemUpdateEnvironmentDiagnosticItem(
            key: 'open_basedir',
            label: (string) __('dberp.system_update_center.environment_diagnostics.items.open_basedir'),
            status: $ok ? 'pass' : 'fail',
            level: $ok
                ? HostSystemUpdateEnvironmentDiagnosticLevel::Info
                : HostSystemUpdateEnvironmentDiagnosticLevel::Blocking,
            message: $ok
                ? (string) __('dberp.system_update_center.environment_diagnostics.messages.open_basedir_ok')
                : (string) __('dberp.system_update_center.environment_diagnostics.messages.open_basedir_blocked', [
                    'paths' => implode('、', $blocked),
                ]),
            suggestion: $ok
                ? ''
                : (string) __('dberp.system_update_center.environment_diagnostics.suggestions.open_basedir_blocked'),
        );
    }

    private function runtimeUserItem(): HostSystemUpdateEnvironmentDiagnosticItem
    {
        $userLabel = $this->resolveRuntimeUserLabel();

        return new HostSystemUpdateEnvironmentDiagnosticItem(
            key: 'php_runtime_user',
            label: (string) __('dberp.system_update_center.environment_diagnostics.items.php_runtime_user'),
            status: 'pass',
            level: HostSystemUpdateEnvironmentDiagnosticLevel::Info,
            message: (string) __('dberp.system_update_center.environment_diagnostics.messages.php_runtime_user', [
                'user' => $userLabel,
            ]),
            suggestion: (string) __('dberp.system_update_center.environment_diagnostics.suggestions.php_runtime_user'),
        );
    }

    /**
     * @return list<HostSystemUpdateEnvironmentDiagnosticItem>
     */
    private function phpIniLimitItems(): array
    {
        return [
            $this->iniLimitItem(
                key: 'max_execution_time',
                labelKey: 'max_execution_time',
                rawValue: ini_get('max_execution_time'),
                evaluate: $this->evaluateMaxExecutionTime((string) ini_get('max_execution_time')),
            ),
            $this->iniLimitItem(
                key: 'memory_limit',
                labelKey: 'memory_limit',
                rawValue: ini_get('memory_limit'),
                evaluate: $this->evaluateMemoryLimit((string) ini_get('memory_limit')),
            ),
        ];
    }

    /**
     * @return array{status: string, level: HostSystemUpdateEnvironmentDiagnosticLevel, message: string, suggestion: string}
     */
    private function evaluateMaxExecutionTime(string $raw): array
    {
        $seconds = (int) $raw;

        if ($seconds === 0) {
            return [
                'status' => 'pass',
                'level' => HostSystemUpdateEnvironmentDiagnosticLevel::Info,
                'message' => (string) __('dberp.system_update_center.environment_diagnostics.messages.max_execution_time_unlimited'),
                'suggestion' => '',
            ];
        }

        if ($seconds < 120) {
            return [
                'status' => 'warn',
                'level' => HostSystemUpdateEnvironmentDiagnosticLevel::Warning,
                'message' => (string) __('dberp.system_update_center.environment_diagnostics.messages.max_execution_time_low', [
                    'seconds' => (string) $seconds,
                ]),
                'suggestion' => (string) __('dberp.system_update_center.environment_diagnostics.suggestions.max_execution_time_low'),
            ];
        }

        return [
            'status' => 'pass',
            'level' => HostSystemUpdateEnvironmentDiagnosticLevel::Info,
            'message' => (string) __('dberp.system_update_center.environment_diagnostics.messages.max_execution_time_ok', [
                'seconds' => (string) $seconds,
            ]),
            'suggestion' => '',
        ];
    }

    /**
     * @return array{status: string, level: HostSystemUpdateEnvironmentDiagnosticLevel, message: string, suggestion: string}
     */
    private function evaluateMemoryLimit(string $raw): array
    {
        $bytes = $this->parseIniSize($raw);

        if ($bytes > 0 && $bytes < 256 * 1024 * 1024) {
            return [
                'status' => 'warn',
                'level' => HostSystemUpdateEnvironmentDiagnosticLevel::Warning,
                'message' => (string) __('dberp.system_update_center.environment_diagnostics.messages.memory_limit_low', [
                    'limit' => $raw !== '' ? $raw : '?',
                ]),
                'suggestion' => (string) __('dberp.system_update_center.environment_diagnostics.suggestions.memory_limit_low'),
            ];
        }

        return [
            'status' => 'pass',
            'level' => HostSystemUpdateEnvironmentDiagnosticLevel::Info,
            'message' => (string) __('dberp.system_update_center.environment_diagnostics.messages.memory_limit_ok', [
                'limit' => $raw !== '' ? $raw : (string) __('dberp.system_update_center.environment_diagnostics.messages.unlimited'),
            ]),
            'suggestion' => '',
        ];
    }

    /**
     * @param  array{status: string, level: HostSystemUpdateEnvironmentDiagnosticLevel, message: string, suggestion: string}  $evaluate
     */
    private function iniLimitItem(
        string $key,
        string $labelKey,
        string|false $rawValue,
        array $evaluate,
    ): HostSystemUpdateEnvironmentDiagnosticItem {
        return new HostSystemUpdateEnvironmentDiagnosticItem(
            key: $key,
            label: (string) __('dberp.system_update_center.environment_diagnostics.items.'.$labelKey),
            status: $evaluate['status'],
            level: $evaluate['level'],
            message: $evaluate['message'],
            suggestion: $evaluate['suggestion'],
        );
    }

    private function phpBinaryItem(): HostSystemUpdateEnvironmentDiagnosticItem
    {
        $binary = defined('PHP_BINARY') ? (string) PHP_BINARY : '';
        $binary = trim($binary);

        if ($binary === '') {
            return new HostSystemUpdateEnvironmentDiagnosticItem(
                key: 'php_binary',
                label: (string) __('dberp.system_update_center.environment_diagnostics.items.php_binary'),
                status: 'warn',
                level: HostSystemUpdateEnvironmentDiagnosticLevel::Warning,
                message: (string) __('dberp.system_update_center.environment_diagnostics.messages.php_binary_unknown'),
                suggestion: (string) __('dberp.system_update_center.environment_diagnostics.suggestions.php_binary_unknown'),
            );
        }

        $exists = is_file($binary);
        $executable = $exists && is_executable($binary);

        if ($executable) {
            return new HostSystemUpdateEnvironmentDiagnosticItem(
                key: 'php_binary',
                label: (string) __('dberp.system_update_center.environment_diagnostics.items.php_binary'),
                status: 'pass',
                level: HostSystemUpdateEnvironmentDiagnosticLevel::Info,
                message: (string) __('dberp.system_update_center.environment_diagnostics.messages.php_binary_ok', [
                    'path' => $this->displayPath($binary),
                ]),
                suggestion: '',
            );
        }

        return new HostSystemUpdateEnvironmentDiagnosticItem(
            key: 'php_binary',
            label: (string) __('dberp.system_update_center.environment_diagnostics.items.php_binary'),
            status: 'warn',
            level: HostSystemUpdateEnvironmentDiagnosticLevel::Warning,
            message: $exists
                ? (string) __('dberp.system_update_center.environment_diagnostics.messages.php_binary_not_executable', [
                    'path' => $this->displayPath($binary),
                ])
                : (string) __('dberp.system_update_center.environment_diagnostics.messages.php_binary_missing', [
                    'path' => $this->displayPath($binary),
                ]),
            suggestion: (string) __('dberp.system_update_center.environment_diagnostics.suggestions.php_binary_unusable'),
        );
    }

    private function summaryMessage(bool $canRun, int $blockingCount, int $warningCount): string
    {
        if (! $canRun) {
            return (string) __('dberp.system_update_center.environment_diagnostics.summary.blocking', [
                'count' => (string) $blockingCount,
            ]);
        }

        if ($warningCount > 0) {
            return (string) __('dberp.system_update_center.environment_diagnostics.summary.warning', [
                'count' => (string) $warningCount,
            ]);
        }

        return (string) __('dberp.system_update_center.environment_diagnostics.summary.ready');
    }

    private function isBlockingFilesystemKey(string $key): bool
    {
        return in_array($key, [
            'update_workspace',
            'update_packages',
            'bootstrap_cache',
        ], true);
    }

    private function requiresProjectFileReplacement(): bool
    {
        return true;
    }

    private function resolveExpectedVendorStrategy(): string
    {
        $package = HostSystemUpdatePackage::query()
            ->where('manifest_status', HostSystemUpdatePackage::MANIFEST_STATUS_VALID)
            ->whereNotNull('manifest')
            ->latest('id')
            ->first();

        if ($package instanceof HostSystemUpdatePackage && is_array($package->manifest)) {
            return $this->vendorStrategyResolver->resolveStrategy($package->manifest);
        }

        return HostSystemUpdateVendorStrategyResolver::STRATEGY_INCLUDED_VENDOR;
    }

    private function tarGzExtractionMayBeUsed(): bool
    {
        return HostSystemUpdatePackageArchiveFormat::isSupported(HostSystemUpdatePackageArchiveFormat::TAR_GZ);
    }

    private function isDirectoryWritable(string $directory): bool
    {
        if (is_dir($directory)) {
            return is_writable($directory);
        }

        $parent = dirname($directory);

        if (! is_dir($parent) && ! @mkdir($parent, 0755, true) && ! is_dir($parent)) {
            return false;
        }

        if (! is_dir($directory) && ! @mkdir($directory, 0755, true) && ! is_dir($directory)) {
            return false;
        }

        return is_dir($directory) && is_writable($directory);
    }

    private function pathAllowedByOpenBasedir(string $absolutePath, string $restriction): bool
    {
        $real = realpath($absolutePath);

        if ($real === false) {
            $real = $absolutePath;
        }

        $real = str_replace('\\', '/', $real);
        $roots = preg_split('/[:;]/', str_replace('\\', '/', $restriction)) ?: [];

        foreach ($roots as $root) {
            $root = rtrim(trim($root), '/');

            if ($root === '') {
                continue;
            }

            $rootReal = realpath($root);
            $normalizedRoot = str_replace('\\', '/', $rootReal !== false ? $rootReal : $root);

            if ($real === $normalizedRoot || str_starts_with($real.'/', $normalizedRoot.'/')) {
                return true;
            }
        }

        return false;
    }

    private function resolveRuntimeUserLabel(): string
    {
        if (function_exists('posix_geteuid') && function_exists('posix_getpwuid')) {
            $account = posix_getpwuid(posix_geteuid());

            if (is_array($account) && is_string($account['name'] ?? null) && $account['name'] !== '') {
                return $account['name'];
            }
        }

        $current = get_current_user();

        return $current !== '' ? $current : (string) __('dberp.system_update_center.environment_diagnostics.messages.runtime_user_unknown');
    }

    private function disabledFunctionsList(): string
    {
        return strtolower(str_replace(' ', '', (string) ini_get('disable_functions')));
    }

    private function isFunctionAvailable(string $function, string $disabledFunctionsList): bool
    {
        if (! function_exists($function)) {
            return false;
        }

        if ($disabledFunctionsList === '') {
            return true;
        }

        $entries = array_filter(explode(',', $disabledFunctionsList));

        return ! in_array(strtolower($function), $entries, true);
    }

    private function parseIniSize(string $value): int
    {
        $value = trim($value);

        if ($value === '' || $value === '-1') {
            return -1;
        }

        $unit = strtolower(substr($value, -1));
        $number = (float) $value;

        return (int) match ($unit) {
            'g' => $number * 1024 * 1024 * 1024,
            'm' => $number * 1024 * 1024,
            'k' => $number * 1024,
            default => (float) $value,
        };
    }

    private function displayPath(string $absolutePath): string
    {
        $normalized = str_replace('\\', '/', $absolutePath);
        $base = str_replace('\\', '/', rtrim(base_path(), '/\\')).'/';

        if (str_starts_with($normalized, $base)) {
            return substr($normalized, strlen($base));
        }

        return $normalized;
    }
}
