<?php

declare(strict_types=1);

namespace App\Services\Install;

final class EnvironmentCheckService
{
    /**
     * 系统在线更新通过 Symfony Process 执行 Artisan（维护模式、迁移等），须未被 disable_functions 禁用。
     *
     * @var list<string>
     */
    private const REQUIRED_PROCESS_FUNCTIONS = [
        'proc_open',
        'proc_close',
        'proc_get_status',
        'proc_terminate',
    ];

    public function __construct(
        private readonly InstallPublicStorageLinkGuard $publicStorageLinkGuard,
    ) {}

    /**
     * @return array{
     *     can_continue: bool,
     *     has_recommended_warnings: bool,
     *     groups: list<array{title: string, items: list<array{label: string, ok: bool, blocking: bool, summary: string, hint: string}>}>
     * }
     */
    public function report(): array
    {
        InstallRuntimeDirectorySupport::ensureWritableDirectories(base_path());

        $groups = [];
        $blockingFailed = false;

        $publicStorageItem = $this->evaluatePublicStoragePath();
        if (! $publicStorageItem['ok']) {
            $blockingFailed = true;
        }
        $groups[] = [
            'title' => $this->trans('groups.public_storage'),
            'items' => [$publicStorageItem],
        ];

        $phpOk = version_compare(PHP_VERSION, '8.3.0', '>=');
        if (! $phpOk) {
            $blockingFailed = true;
        }

        $symlinkOk = function_exists('symlink');
        if (! $symlinkOk) {
            $blockingFailed = true;
        }

        $disabledFunctions = trim((string) ini_get('disable_functions'));

        $phpRuntimeItems = [
            $this->checkItem(
                label: $this->trans('labels.php_version'),
                ok: $phpOk,
                blocking: true,
                summary: $phpOk
                    ? $this->trans('summaries.php_ok', ['version' => PHP_VERSION])
                    : $this->trans('summaries.php_fail', ['version' => PHP_VERSION]),
                hint: $phpOk ? '' : $this->trans('hints.php_version_fail'),
            ),
            $this->checkItem(
                label: $this->trans('labels.symlink'),
                ok: $symlinkOk,
                blocking: true,
                summary: $symlinkOk
                    ? $this->trans('summaries.symlink_ok')
                    : $this->trans('summaries.symlink_fail', [
                        'disabled' => $disabledFunctions !== ''
                            ? $disabledFunctions
                            : $this->trans('summaries.disabled_empty'),
                    ]),
                hint: $symlinkOk ? '' : $this->trans('hints.symlink_fail'),
            ),
        ];

        foreach (self::REQUIRED_PROCESS_FUNCTIONS as $function) {
            $processFunctionItem = $this->processFunctionItem($function, $disabledFunctions);
            if (! $processFunctionItem['ok']) {
                $blockingFailed = true;
            }
            $phpRuntimeItems[] = $processFunctionItem;
        }

        $groups[] = [
            'title' => $this->trans('groups.php_runtime'),
            'items' => $phpRuntimeItems,
        ];

        $requiredExtIds = [
            'openssl', 'pdo', 'mbstring', 'tokenizer', 'xml', 'ctype', 'json', 'fileinfo', 'curl', 'zip',
        ];
        $requiredItems = [];
        foreach ($requiredExtIds as $ext) {
            $ok = extension_loaded($ext);
            if (! $ok) {
                $blockingFailed = true;
            }
            $requiredItems[] = $this->extensionItem($ext, $ok, blocking: true);
        }
        $groups[] = [
            'title' => $this->trans('groups.extensions_required'),
            'items' => $requiredItems,
        ];

        $recommendedExtIds = ['opcache', 'redis', 'bcmath', 'intl', 'gd', 'exif'];
        $recommendedItems = [];
        foreach ($recommendedExtIds as $ext) {
            $ok = $this->isRecommendedExtensionEnabled($ext);
            $recommendedItems[] = $this->extensionItem($ext, $ok, blocking: false);
        }
        $groups[] = [
            'title' => $this->trans('groups.extensions_recommended'),
            'items' => $recommendedItems,
        ];

        $directoryPaths = [
            storage_path(),
            storage_path('app'),
            storage_path('framework'),
            storage_path('logs'),
            base_path('bootstrap/cache'),
        ];
        $dirItems = [];
        foreach ($directoryPaths as $absolutePath) {
            $writable = is_dir($absolutePath) && is_writable($absolutePath);
            if (! $writable) {
                $blockingFailed = true;
            }
            $dirItems[] = $this->writableDirectoryItem($absolutePath, $writable);
        }
        $groups[] = [
            'title' => $this->trans('groups.writable_paths'),
            'items' => $dirItems,
        ];

        $envResult = $this->evaluateEnvFile();
        if (! $envResult['ok']) {
            $blockingFailed = true;
        }
        $groups[] = [
            'title' => $this->trans('groups.env_file'),
            'items' => [$envResult],
        ];

        $hasRecommendedWarnings = false;
        foreach ($recommendedItems as $recommendedItem) {
            if (! $recommendedItem['ok']) {
                $hasRecommendedWarnings = true;
                break;
            }
        }

        return [
            'can_continue' => ! $blockingFailed,
            'has_recommended_warnings' => $hasRecommendedWarnings,
            'groups' => $groups,
        ];
    }

    /**
     * @return array{label: string, ok: bool, blocking: bool, summary: string, hint: string}
     */
    private function writableDirectoryItem(string $absolutePath, bool $writable): array
    {
        $displayPath = $this->displayPath($absolutePath);

        if ($writable) {
            $summary = $this->trans('summaries.dir_ok');
        } elseif (is_dir($absolutePath)) {
            $summary = $this->trans('summaries.dir_not_writable');
        } else {
            $summary = $this->trans('summaries.dir_missing');
        }

        return $this->checkItem(
            label: $this->trans('labels.writable_directory', ['path' => $displayPath]),
            ok: $writable,
            blocking: true,
            summary: $summary.' · '.$this->trans('summaries.path_suffix', ['path' => $displayPath]),
            hint: $writable ? '' : $this->trans('hints.directory_not_writable'),
        );
    }

    /**
     * @return array{label: string, ok: bool, blocking: bool, summary: string, hint: string}
     */
    private function processFunctionItem(string $function, string $disabledFunctionsList): array
    {
        $ok = function_exists($function);

        return $this->checkItem(
            label: $this->trans('labels.process_function', ['function' => $function]),
            ok: $ok,
            blocking: true,
            summary: $ok
                ? $this->trans('summaries.process_function_ok', ['function' => $function])
                : $this->trans('summaries.process_function_fail', [
                    'function' => $function,
                    'disabled' => $disabledFunctionsList !== ''
                        ? $disabledFunctionsList
                        : $this->trans('summaries.disabled_empty'),
                ]),
            hint: $ok ? '' : $this->trans('hints.process_function_fail', ['function' => $function]),
        );
    }

    /**
     * @return array{label: string, ok: bool, blocking: bool, summary: string, hint: string}
     */
    private function checkItem(
        string $label,
        bool $ok,
        bool $blocking,
        string $summary,
        string $hint = '',
    ): array {
        return [
            'label' => $label,
            'ok' => $ok,
            'blocking' => $blocking,
            'summary' => $summary,
            'hint' => $hint,
        ];
    }

    /**
     * @return array{label: string, ok: bool, blocking: bool, summary: string, hint: string}
     */
    private function extensionItem(string $ext, bool $ok, bool $blocking): array
    {
        if ($ext === 'opcache') {
            return $this->checkItem(
                label: $this->trans('labels.extension_opcache'),
                ok: $ok,
                blocking: $blocking,
                summary: $ok
                    ? $this->trans('summaries.opcache_ok')
                    : $this->trans('summaries.opcache_fail', [
                        'loaded' => $this->isOpcacheExtensionLoaded() ? 'true' : 'false',
                        'enable' => (string) ini_get('opcache.enable'),
                    ]),
                hint: $ok ? '' : $this->trans('hints.opcache_recommended_fail'),
            );
        }

        return $this->checkItem(
            label: $this->trans('labels.extension', ['ext' => $ext]),
            ok: $ok,
            blocking: $blocking,
            summary: $ok
                ? $this->trans('summaries.extension_ok', ['ext' => $ext])
                : $this->trans('summaries.extension_fail', ['ext' => $ext]),
            hint: $ok ? '' : $this->extensionEnableHint($ext, $blocking),
        );
    }

    private function extensionEnableHint(string $ext, bool $blocking): string
    {
        if (! $blocking) {
            return $this->trans('hints.extension_recommended_fail', ['ext' => $ext]);
        }

        return $this->trans('hints.extension_required_fail', ['ext' => $ext]);
    }

    /**
     * @return array{label: string, ok: bool, blocking: bool, summary: string, hint: string}
     */
    private function evaluatePublicStoragePath(): array
    {
        $link = $this->publicStorageLinkGuard->publicStoragePath();
        $target = $this->publicStorageLinkGuard->publicStorageTargetPath();

        try {
            if ($this->publicStorageLinkGuard->isValidLink($link, $target)) {
                return $this->checkItem(
                    label: $this->trans('labels.public_storage'),
                    ok: true,
                    blocking: true,
                    summary: $this->trans('summaries.public_storage_symlink_ok', [
                        'link' => $this->displayPath($link),
                        'target' => $this->displayPath($target),
                    ]),
                );
            }

            $removed = $this->publicStorageLinkGuard->removeInvalidPublicStoragePath();

            return $this->checkItem(
                label: $this->trans('labels.public_storage'),
                ok: true,
                blocking: true,
                summary: $removed
                    ? $this->trans('summaries.public_storage_invalid_removed')
                    : $this->trans('summaries.public_storage_ready'),
                hint: $removed
                    ? $this->trans('hints.public_storage_invalid_removed')
                    : '',
            );
        } catch (\Throwable $e) {
            return $this->checkItem(
                label: $this->trans('labels.public_storage'),
                ok: false,
                blocking: true,
                summary: $this->trans('summaries.public_storage_cleanup_fail', [
                    'message' => $e->getMessage(),
                ]),
                hint: $this->trans('hints.public_storage_cleanup_fail'),
            );
        }
    }

    /**
     * @return array{label: string, ok: bool, blocking: bool, summary: string, hint: string}
     */
    private function evaluateEnvFile(): array
    {
        $envPath = base_path('.env');
        $examplePath = base_path('.env.example');
        $rootPath = base_path();

        if (is_file($envPath)) {
            $writable = is_writable($envPath);

            return $this->checkItem(
                label: $this->trans('labels.env_file'),
                ok: $writable,
                blocking: true,
                summary: ($writable
                    ? $this->trans('summaries.env_exists_writable')
                    : $this->trans('summaries.env_exists_not_writable'))
                    .' · '.$this->trans('summaries.path_suffix', ['path' => $this->displayPath($envPath)]),
                hint: $writable ? '' : $this->trans('hints.env_not_writable'),
            );
        }

        if (! is_file($examplePath)) {
            return $this->checkItem(
                label: $this->trans('labels.env_file'),
                ok: false,
                blocking: true,
                summary: $this->trans('summaries.env_missing_template')
                    .' · '.$this->trans('summaries.path_suffix', ['path' => $this->displayPath($examplePath)]),
                hint: $this->trans('hints.env_missing_example'),
            );
        }

        $rootWritable = is_writable($rootPath);

        return $this->checkItem(
            label: $this->trans('labels.env_file'),
            ok: $rootWritable,
            blocking: true,
            summary: ($rootWritable
                ? $this->trans('summaries.env_will_copy_writable')
                : $this->trans('summaries.env_will_copy_not_writable'))
                .' · '.$this->trans('summaries.path_suffix', ['path' => $this->displayPath($rootPath)]),
            hint: $rootWritable
                ? $this->trans('hints.env_will_copy')
                : $this->trans('hints.env_root_not_writable'),
        );
    }

    private function isRecommendedExtensionEnabled(string $ext): bool
    {
        return match ($ext) {
            'opcache' => $this->isOpcacheEnabled(),
            default => extension_loaded($ext),
        };
    }

    private function isOpcacheExtensionLoaded(): bool
    {
        return extension_loaded('Zend OPcache');
    }

    private function isOpcacheEnabled(): bool
    {
        if (! $this->isOpcacheExtensionLoaded()) {
            return false;
        }

        return filter_var(ini_get('opcache.enable'), FILTER_VALIDATE_BOOLEAN);
    }

    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;
    }

    /**
     * @param  array<string, string>  $replace
     */
    private function trans(string $key, array $replace = []): string
    {
        return (string) __('dberp.installer.env_check.'.$key, $replace);
    }
}
