<?php

declare(strict_types=1);

namespace App\Services\SupportCenter;

use App\Exceptions\SupportCenter\HostSystemUpdateManifestException;
use App\Models\HostSystemUpdatePackage;
use App\Support\DberpProductVersion;
use Composer\InstalledVersions;
use Illuminate\Foundation\Application;

/**
 * DBErp Phase 1 manifest.json 业务与安全校验。
 */
final class HostSystemUpdateManifestValidator
{
    public function __construct(
        private readonly DberpProductVersion $productVersion,
        private readonly HostSystemUpdateProtectedPathGuard $protectedPathGuard,
        private readonly HostSystemUpdatePackageTypeGuard $packageTypeGuard,
        private readonly HostSystemUpdateMinUpgradableVersionGuard $minUpgradableVersionGuard,
    ) {}

    /**
     * @param  array<string, mixed>  $manifest
     */
    public function validate(array $manifest, HostSystemUpdatePackage $package): void
    {
        $this->assertRequiredFields($manifest);
        $this->assertProductCode($manifest);
        $this->assertUpdateVersions($manifest);
        $this->assertPackageType($manifest);
        $this->minUpgradableVersionGuard->assertMeetsMinimumForManifest($manifest);
        $this->assertReleaseChannel($manifest, $package);
        $this->assertRequires($manifest);
        $this->assertPathCollections($manifest);
        $this->assertBackupRequired($manifest);
    }

    /**
     * @param  array<string, mixed>  $manifest
     */
    private function assertRequiredFields(array $manifest): void
    {
        foreach (HostSystemUpdateManifestContract::REQUIRED_FIELDS as $field) {
            if (! array_key_exists($field, $manifest)) {
                throw HostSystemUpdateManifestException::validationFailed(
                    (string) __('dberp.system_update.manifest.required_field_missing', ['field' => $field]),
                    'manifest_required_field_missing',
                );
            }
        }

        if ($manifest[HostSystemUpdateManifestContract::FILES] === null
            || ! is_array($manifest[HostSystemUpdateManifestContract::FILES])) {
            throw HostSystemUpdateManifestException::validationFailed(
                (string) __('dberp.system_update.manifest.files_invalid'),
                'manifest_files_invalid',
            );
        }

        if ($manifest[HostSystemUpdateManifestContract::CHECKSUMS] === null
            || ! is_array($manifest[HostSystemUpdateManifestContract::CHECKSUMS])) {
            throw HostSystemUpdateManifestException::validationFailed(
                (string) __('dberp.system_update.manifest.checksums_invalid'),
                'manifest_checksums_invalid',
            );
        }

        if ($manifest[HostSystemUpdateManifestContract::BACKUP_REQUIRED] === null) {
            throw HostSystemUpdateManifestException::validationFailed(
                (string) __('dberp.system_update.manifest.backup_required_missing'),
                'manifest_backup_required_missing',
            );
        }
    }

    /**
     * @param  array<string, mixed>  $manifest
     */
    private function assertProductCode(array $manifest): void
    {
        $productCode = $manifest[HostSystemUpdateManifestContract::PRODUCT_CODE] ?? null;

        if ($productCode !== 'dberp') {
            throw HostSystemUpdateManifestException::validationFailed(
                (string) __('dberp.system_update.manifest.product_code_mismatch'),
                'manifest_product_code_mismatch',
            );
        }
    }

    /**
     * @param  array<string, mixed>  $manifest
     */
    private function assertUpdateVersions(array $manifest): void
    {
        $current = $this->productVersion->updateVersion();
        $toUpdateVersion = $manifest[HostSystemUpdateManifestContract::TO_UPDATE_VERSION] ?? null;

        if (! is_int($toUpdateVersion) || $toUpdateVersion <= $current) {
            throw HostSystemUpdateManifestException::validationFailed(
                (string) __('dberp.system_update.manifest.to_update_version_not_greater', [
                    'current' => (string) $current,
                    'target' => (string) ($toUpdateVersion ?? 'null'),
                ]),
                'manifest_to_update_version_not_greater',
            );
        }
    }

    /**
     * @param  array<string, mixed>  $manifest
     */
    private function assertPackageType(array $manifest): void
    {
        $packageType = $manifest[HostSystemUpdateManifestContract::PACKAGE_TYPE] ?? null;

        $this->packageTypeGuard->assertInstallPackageType(
            is_string($packageType) ? $packageType : null,
            'manifest',
        );
    }

    /**
     * @param  array<string, mixed>  $manifest
     */
    private function assertReleaseChannel(array $manifest, HostSystemUpdatePackage $package): void
    {
        $manifestChannel = $manifest[HostSystemUpdateManifestContract::RELEASE_CHANNEL] ?? null;
        $packageChannel = $package->release_channel;

        if ($manifestChannel === null || $packageChannel === null || trim((string) $packageChannel) === '') {
            return;
        }

        if (strtolower((string) $manifestChannel) !== strtolower((string) $packageChannel)) {
            throw HostSystemUpdateManifestException::validationFailed(
                (string) __('dberp.system_update.manifest.release_channel_mismatch'),
                'manifest_release_channel_mismatch',
            );
        }
    }

    /**
     * @param  array<string, mixed>  $manifest
     */
    private function assertRequires(array $manifest): void
    {
        $requires = $manifest[HostSystemUpdateManifestContract::REQUIRES] ?? null;

        if (! is_array($requires)) {
            return;
        }

        if (isset($requires['php']) && ! $this->satisfiesVersion(PHP_VERSION, $requires['php'])) {
            throw HostSystemUpdateManifestException::validationFailed(
                (string) __('dberp.system_update.manifest.php_requirement_unmet', ['required' => $requires['php']]),
                'manifest_php_requirement_unmet',
            );
        }

        if (isset($requires['laravel']) && ! $this->satisfiesVersion(Application::VERSION, $requires['laravel'])) {
            throw HostSystemUpdateManifestException::validationFailed(
                (string) __('dberp.system_update.manifest.laravel_requirement_unmet', ['required' => $requires['laravel']]),
                'manifest_laravel_requirement_unmet',
            );
        }

        $filamentVersion = $this->installedPackageVersion('filament/filament');

        if (isset($requires['filament']) && $filamentVersion !== null
            && ! $this->satisfiesVersion($filamentVersion, $requires['filament'])) {
            throw HostSystemUpdateManifestException::validationFailed(
                (string) __('dberp.system_update.manifest.filament_requirement_unmet', ['required' => $requires['filament']]),
                'manifest_filament_requirement_unmet',
            );
        }

        $livewireVersion = $this->installedPackageVersion('livewire/livewire');

        if (isset($requires['livewire']) && $livewireVersion !== null
            && ! $this->satisfiesVersion($livewireVersion, $requires['livewire'])) {
            throw HostSystemUpdateManifestException::validationFailed(
                (string) __('dberp.system_update.manifest.livewire_requirement_unmet', ['required' => $requires['livewire']]),
                'manifest_livewire_requirement_unmet',
            );
        }
    }

    /**
     * @param  array<string, mixed>  $manifest
     */
    private function assertPathCollections(array $manifest): void
    {
        $this->assertPathList($manifest[HostSystemUpdateManifestContract::FILES] ?? [], 'files');
        $this->assertPathList($manifest[HostSystemUpdateManifestContract::DELETE_FILES] ?? [], 'delete_files');
        $this->assertPathList($manifest[HostSystemUpdateManifestContract::MIGRATIONS] ?? [], 'migrations');
        $this->assertPathList($manifest[HostSystemUpdateManifestContract::PROTECTED_PATHS] ?? [], 'protected_paths');

        $commands = $manifest[HostSystemUpdateManifestContract::COMMANDS] ?? [];

        if ($commands !== null && ! is_array($commands)) {
            throw HostSystemUpdateManifestException::validationFailed(
                (string) __('dberp.system_update.manifest.commands_invalid'),
                'manifest_commands_invalid',
            );
        }

        $checksums = $manifest[HostSystemUpdateManifestContract::CHECKSUMS] ?? [];

        if (! is_array($checksums)) {
            throw HostSystemUpdateManifestException::validationFailed(
                (string) __('dberp.system_update.manifest.checksums_invalid'),
                'manifest_checksums_invalid',
            );
        }

        foreach (array_keys($checksums) as $path) {
            if (! is_string($path)) {
                throw HostSystemUpdateManifestException::validationFailed(
                    (string) __('dberp.system_update.manifest.checksums_invalid'),
                    'manifest_checksums_invalid',
                );
            }

            $this->protectedPathGuard->assertSafeRelativePath($path, 'checksums');
        }
    }

    /**
     * @param  array<string, mixed>  $manifest
     */
    private function assertBackupRequired(array $manifest): void
    {
        $backupRequired = $manifest[HostSystemUpdateManifestContract::BACKUP_REQUIRED] ?? null;

        if (! is_bool($backupRequired)) {
            throw HostSystemUpdateManifestException::validationFailed(
                (string) __('dberp.system_update.manifest.backup_required_invalid'),
                'manifest_backup_required_invalid',
            );
        }
    }

    /**
     * @param  list<string>|null  $paths
     */
    private function assertPathList(?array $paths, string $context): void
    {
        if ($paths === null) {
            return;
        }

        if (! is_array($paths)) {
            throw HostSystemUpdateManifestException::validationFailed(
                (string) __('dberp.system_update.manifest.path_list_invalid', ['context' => $context]),
                'manifest_path_list_invalid',
            );
        }

        foreach ($paths as $path) {
            if (! is_string($path)) {
                throw HostSystemUpdateManifestException::validationFailed(
                    (string) __('dberp.system_update.manifest.path_list_invalid', ['context' => $context]),
                    'manifest_path_list_invalid',
                );
            }

            if ($context === 'protected_paths') {
                $this->protectedPathGuard->assertProtectedPathDeclaration($path, $context);
            } else {
                $this->protectedPathGuard->assertSafeRelativePath($path, $context);
            }
        }
    }

    private function satisfiesVersion(string $current, string $constraint): bool
    {
        $minimum = ltrim(trim($constraint), '^~>=<vV');

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

        return version_compare($current, $minimum, '>=');
    }

    private function installedPackageVersion(string $packageName): ?string
    {
        if (! class_exists(InstalledVersions::class) || ! InstalledVersions::isInstalled($packageName)) {
            return null;
        }

        return InstalledVersions::getPrettyVersion($packageName);
    }
}
