<?php

declare(strict_types=1);

namespace App\Services\SupportCenter;

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

/**
 * 更新执行前环境预检：磁盘空间、关键目录可写、待替换路径可写。
 */
final class HostSystemUpdateEnvironmentPrecheckService
{
    public function __construct(
        private readonly HostSystemUpdateExecutionContextResolver $contextResolver,
        private readonly HostSystemUpdateProtectedPathGuard $protectedPathGuard,
        private readonly VendorStrategyResolver $vendorStrategyResolver,
        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->executionLogger->logTransition(
            $execution,
            'precheck',
            (string) __('dberp.system_update.precheck.log.completed'),
        );
    }

    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) {
            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',
            );
        }
    }
}
