<?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\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->assertRuntimeRequirements($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');
        }

        foreach ($checksums as $path => $checksum) {
            if (! is_string($checksum)
                || preg_match('/^(?:sha256:)?[a-f0-9]{64}$/i', trim($checksum)) !== 1) {
                throw HostSystemUpdateManifestException::validationFailed(
                    (string) __('dberp.system_update.manifest.checksums_invalid'),
                    'manifest_checksums_invalid',
                );
            }
        }

        $files = $manifest[HostSystemUpdateManifestContract::FILES] ?? [];

        foreach (is_array($files) ? $files : [] as $path) {
            if (! is_string($path) || ! array_key_exists($path, $checksums)) {
                throw HostSystemUpdateManifestException::validationFailed(
                    (string) __('dberp.system_update.manifest.checksums_incomplete', ['path' => (string) $path]),
                    'manifest_checksums_incomplete',
                );
            }
        }
    }

    /**
     * @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 array<string, mixed> $manifest */
    private function assertRuntimeRequirements(array $manifest): void
    {
        $runtime = $manifest[HostSystemUpdateManifestContract::RUNTIME] ?? null;

        if ($runtime === null) {
            return;
        }

        if (! is_array($runtime)) {
            throw HostSystemUpdateManifestException::validationFailed(
                'runtime 必须为对象。',
                'manifest_runtime_invalid',
            );
        }

        $lockHash = $runtime['composer_lock_sha256'] ?? null;

        if ($lockHash !== null && (! is_string($lockHash) || preg_match('/^[a-f0-9]{64}$/i', $lockHash) !== 1)) {
            throw HostSystemUpdateManifestException::validationFailed(
                'runtime.composer_lock_sha256 无效。',
                'manifest_runtime_lock_hash_invalid',
            );
        }

        $packages = $runtime['required_packages'] ?? [];

        if (! is_array($packages)) {
            throw HostSystemUpdateManifestException::validationFailed(
                'runtime.required_packages 必须为对象。',
                'manifest_runtime_packages_invalid',
            );
        }

        foreach ($packages as $name => $version) {
            if (! is_string($name) || preg_match('#^[a-z0-9_.-]+/[a-z0-9_.-]+$#i', $name) !== 1 || ! is_string($version)) {
                throw HostSystemUpdateManifestException::validationFailed(
                    'runtime.required_packages 包含无效条目。',
                    'manifest_runtime_packages_invalid',
                );
            }
        }

        $classes = $runtime['required_classes'] ?? [];

        if (! is_array($classes)) {
            throw HostSystemUpdateManifestException::validationFailed(
                'runtime.required_classes 必须为数组。',
                'manifest_runtime_classes_invalid',
            );
        }

        foreach ($classes as $class) {
            if (! is_string($class) || preg_match('/^[A-Za-z_][A-Za-z0-9_]*(\\\\[A-Za-z_][A-Za-z0-9_]*)+$/', $class) !== 1) {
                throw HostSystemUpdateManifestException::validationFailed(
                    'runtime.required_classes 包含无效类名。',
                    'manifest_runtime_classes_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);
    }
}
