<?php

declare(strict_types=1);

/**
 * 本文件属于 DBErp 进销存系统。
 *
 * 版权所有 © 2026 北京珑大钜商科技有限公司。保留所有权利。
 *
 * 未经官方授权，不得删除、篡改或隐藏本文件中的版权声明、产品标识与来源信息。
 * 商业授权、官方更新与技术支持服务，请以珑大官方支持中心记录为准。
 *
 * @link https://support.loongdom.cn
 * @see  https://www.dberp.com.cn
 */

namespace App\Services\SupportCenter;

use App\Exceptions\SupportCenter\HostSystemUpdateStagingException;
use App\Models\HostSystemUpdateExecution;
use App\Models\HostSystemUpdatePackage;
use App\Services\SupportCenter\HostSystemUpdateVendorStrategyResolver as VendorStrategyResolver;
use Throwable;

/**
 * 更新执行前环境预检：磁盘空间、关键目录可写、待替换路径可写。
 */
final class HostSystemUpdateEnvironmentPrecheckService
{
    public function __construct(
        private readonly HostSystemUpdateExecutionContextResolver $contextResolver,
        private readonly HostSystemUpdateProtectedPathGuard $protectedPathGuard,
        private readonly VendorStrategyResolver $vendorStrategyResolver,
        private readonly HostSystemUpdateComposerDependencyInstaller $composerDependencyInstaller,
        private readonly HostSystemUpdateOpcache $opcache,
        private readonly ProcessHostSystemUpdateArtisanCommandHandler $processHandler,
        private readonly HostSystemUpdateExecutionLogger $executionLogger,
    ) {}

    public function run(HostSystemUpdateExecution $execution): void
    {
        $package = $this->contextResolver->resolveReadyPackage($execution);
        $manifest = $this->contextResolver->manifest($execution);

        $this->executionLogger->logTransition(
            $execution,
            'precheck',
            (string) __('dberp.system_update.precheck.log.started'),
        );

        $this->assertDiskSpaceAvailable($package);
        $this->assertWritableDirectories();
        $this->assertWritableManifestTargets($manifest);
        $this->assertWritableVendorIfNeeded($manifest);
        $this->assertFreshPhpProcessAvailable();
        $runtimeContext = $this->runtimeCacheContext();

        $this->executionLogger->logTransition(
            $execution,
            'precheck',
            (string) __('dberp.system_update.precheck.log.completed'),
            $runtimeContext,
        );
    }

    private function assertFreshPhpProcessAvailable(): void
    {
        try {
            $result = $this->processHandler->execute(
                'about',
                ['--only=environment'],
                30,
            );
        } catch (Throwable) {
            throw HostSystemUpdateStagingException::precheckFailed(
                (string) __('dberp.system_update.precheck.fresh_php_process_unavailable'),
                'precheck_fresh_php_process_unavailable',
            );
        }

        if (! $result->succeeded()) {
            throw HostSystemUpdateStagingException::precheckFailed(
                (string) __('dberp.system_update.precheck.fresh_php_process_unavailable'),
                'precheck_fresh_php_process_unavailable',
            );
        }
    }

    /** @return array<string, mixed> */
    private function runtimeCacheContext(): array
    {
        $resetRequired = $this->opcache->shouldReset();
        $diagnostics = $this->opcacheDiagnosticContext();
        $requiresReliableReset = $resetRequired
            && array_key_exists('opcache_validate_timestamps', $diagnostics)
            && $diagnostics['opcache_validate_timestamps'] === false;
        $preflightReset = false;

        if ($requiresReliableReset) {
            try {
                $preflightReset = $this->opcache->reset();
            } catch (Throwable) {
                $preflightReset = false;
            }

            if (! $preflightReset) {
                throw HostSystemUpdateStagingException::precheckFailed(
                    (string) __('dberp.system_update.precheck.runtime_cache_reset_unavailable', [
                        'reason' => $this->opcacheFailureReason(),
                    ]),
                    'precheck_runtime_cache_reset_unavailable',
                );
            }
        }

        return array_merge(
            [
                'opcache_reset_required' => $resetRequired,
                'opcache_reset_preflight' => $preflightReset,
                'opcache_reset_best_effort' => ! $requiresReliableReset,
                'fresh_process_barrier' => true,
            ],
            $diagnostics,
        );
    }

    private function opcacheFailureReason(): string
    {
        if (method_exists($this->opcache, 'failureReason')) {
            $reason = $this->opcache->failureReason();

            if (is_string($reason) && $reason !== '') {
                return $reason;
            }
        }

        return 'opcache_reset_failed';
    }

    /** @return array<string, mixed> */
    private function opcacheDiagnosticContext(): array
    {
        if (! method_exists($this->opcache, 'diagnosticContext')) {
            return [];
        }

        $context = $this->opcache->diagnosticContext();

        return is_array($context) ? $context : [];
    }

    private function assertDiskSpaceAvailable(HostSystemUpdatePackage $package): void
    {
        $packageBytes = max(0, (int) ($package->local_size ?? $package->package_size ?? 0));
        $multiplier = max(2, (int) config('dberp.system_update.disk_space_multiplier', 3));
        $minimumBytes = max(
            (int) config('dberp.system_update.minimum_free_disk_bytes', 512 * 1024 * 1024),
            $packageBytes * $multiplier,
        );

        foreach ($this->diskRootsToCheck() as $root) {
            $free = @disk_free_space($root);

            if ($free === false) {
                throw HostSystemUpdateStagingException::precheckFailed(
                    (string) __('dberp.system_update.precheck.disk_space_unavailable'),
                    'precheck_disk_space_unavailable',
                );
            }

            if ((int) $free < $minimumBytes) {
                throw HostSystemUpdateStagingException::precheckFailed(
                    (string) __('dberp.system_update.precheck.insufficient_disk_space', [
                        'required_mb' => (string) (int) ceil($minimumBytes / 1024 / 1024),
                    ]),
                    'precheck_insufficient_disk_space',
                );
            }
        }
    }

    /**
     * @return list<string>
     */
    private function diskRootsToCheck(): array
    {
        $roots = [
            base_path(),
            storage_path(),
        ];

        return array_values(array_unique($roots));
    }

    private function assertWritableDirectories(): void
    {
        $directories = [
            HostSystemUpdateWorkspaceStorage::baseDirectory(),
            HostSystemUpdateWorkspaceStorage::packagesDirectory(),
            base_path('bootstrap/cache'),
        ];

        foreach ($directories as $directory) {
            $this->assertDirectoryWritable($directory, 'directory');
        }
    }

    /**
     * @param  array<string, mixed>  $manifest
     */
    private function assertWritableManifestTargets(array $manifest): void
    {
        $paths = [];

        foreach ([
            HostSystemUpdateManifestContract::FILES,
            HostSystemUpdateManifestContract::DELETE_FILES,
        ] as $field) {
            $items = $manifest[$field] ?? [];

            if (! is_array($items)) {
                continue;
            }

            foreach ($items as $path) {
                if (is_string($path) && trim($path) !== '') {
                    $paths[] = $path;
                }
            }
        }

        foreach (array_values(array_unique($paths)) as $relativePath) {
            $this->protectedPathGuard->assertSafeUpdateTargetPath($relativePath, 'precheck');
            $normalized = $this->protectedPathGuard->normalizeRelativePath($relativePath);

            if ($normalized === null) {
                continue;
            }

            $absolute = $this->protectedPathGuard->projectAbsolutePath($normalized);

            if (is_file($absolute)) {
                $this->assertFileWritable($absolute, $normalized);

                continue;
            }

            $this->assertDirectoryWritable(dirname($absolute), $normalized);
        }
    }

    /**
     * @param  array<string, mixed>  $manifest
     */
    private function assertWritableVendorIfNeeded(array $manifest): void
    {
        $strategy = $this->vendorStrategyResolver->resolveStrategy($manifest);

        if ($strategy !== VendorStrategyResolver::STRATEGY_INCLUDED_VENDOR) {
            if ($strategy === VendorStrategyResolver::STRATEGY_INCLUDED_VENDOR_BUNDLE
                || $strategy === VendorStrategyResolver::STRATEGY_COMPOSER_REQUIRED) {
                $this->assertDirectoryWritable(base_path('vendor'), 'vendor');

                if ($strategy === VendorStrategyResolver::STRATEGY_COMPOSER_REQUIRED
                    && ! $this->composerDependencyInstaller->isComposerAvailable()) {
                    throw HostSystemUpdateStagingException::precheckFailed(
                        'composer_required 更新策略要求服务器提供可执行 Composer。',
                        'precheck_composer_unavailable',
                    );
                }

                return;
            }

            return;
        }

        $vendorDirectory = base_path('vendor');
        $this->assertDirectoryWritable($vendorDirectory, 'vendor');

        foreach ($this->vendorStrategyResolver->resolveVendorFiles($manifest) as $relativePath) {
            $this->protectedPathGuard->assertSafeUpdateTargetPath($relativePath, 'vendor');
            $normalized = $this->protectedPathGuard->normalizeRelativePath($relativePath);

            if ($normalized === null) {
                continue;
            }

            $absolute = $this->protectedPathGuard->projectAbsolutePath($normalized);
            $parent = dirname($absolute);
            $this->assertDirectoryWritable($parent, $normalized);

            if (is_file($absolute)) {
                $this->assertFileWritable($absolute, $normalized);
            }
        }
    }

    private function assertDirectoryWritable(string $directory, string $label): void
    {
        if (! is_dir($directory)) {
            if (! @mkdir($directory, 0755, true) && ! is_dir($directory)) {
                throw HostSystemUpdateStagingException::precheckFailed(
                    (string) __('dberp.system_update.precheck.directory_not_writable', ['path' => $label]),
                    'precheck_directory_not_writable',
                );
            }
        }

        if (! is_writable($directory)) {
            throw HostSystemUpdateStagingException::precheckFailed(
                (string) __('dberp.system_update.precheck.directory_not_writable', ['path' => $label]),
                'precheck_directory_not_writable',
            );
        }
    }

    private function assertFileWritable(string $file, string $label): void
    {
        if (! is_writable($file)) {
            throw HostSystemUpdateStagingException::precheckFailed(
                (string) __('dberp.system_update.precheck.file_not_writable', ['path' => $label]),
                'precheck_file_not_writable',
            );
        }
    }
}
