<?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 ZipArchive;

/**
 * 从已校验更新包（zip / tar.gz）读取 manifest.json（不解压执行）。
 *
 * NOTE: Support Center Phase 1 不提供 manifest API；manifest 仅存在于包内 manifest.json。
 */
final class HostSystemUpdateManifestReader
{
    public function readFromPackage(HostSystemUpdatePackage $package): array
    {
        $this->assertPackageVerified($package);

        $path = $package->local_path;

        if (! is_string($path) || $path === '' || ! is_file($path)) {
            throw HostSystemUpdateManifestException::packageFileMissing();
        }

        return $this->readFromArchivePath($path);
    }

    /**
     * @return array<string, mixed>
     */
    public function readFromArchivePath(string $archivePath): array
    {
        if (! is_file($archivePath)) {
            throw HostSystemUpdateManifestException::packageFileMissing();
        }

        $format = HostSystemUpdatePackageArchiveFormat::detectFromFile($archivePath);

        if (! HostSystemUpdatePackageArchiveFormat::isSupported($format)) {
            throw HostSystemUpdateManifestException::unsupportedArchiveFormat($format);
        }

        return match ($format) {
            HostSystemUpdatePackageArchiveFormat::TAR_GZ => $this->readFromTarGzPath($archivePath),
            default => $this->readFromZipPath($archivePath),
        };
    }

    /**
     * @return array<string, mixed>
     */
    public function readFromZipPath(string $zipPath): array
    {
        if (! is_file($zipPath)) {
            throw HostSystemUpdateManifestException::packageFileMissing();
        }

        $zip = new ZipArchive;
        $opened = $zip->open($zipPath);

        if ($opened !== true) {
            throw HostSystemUpdateManifestException::archiveOpenFailed();
        }

        try {
            $manifestIndex = $this->locateManifestIndexInZip($zip);

            if ($manifestIndex === null) {
                throw HostSystemUpdateManifestException::manifestMissing();
            }

            $contents = $zip->getFromIndex($manifestIndex);

            if ($contents === false || trim($contents) === '') {
                throw HostSystemUpdateManifestException::manifestMissing();
            }

            return $this->decodeManifestContents($contents);
        } finally {
            $zip->close();
        }
    }

    /**
     * @return array<string, mixed>
     */
    private function readFromTarGzPath(string $tarGzPath): array
    {
        try {
            $accessor = new HostSystemUpdateTarGzPharAccessor($tarGzPath);
            $phar = $accessor->phar();
            $entry = HostSystemUpdateManifestContract::MANIFEST_ENTRY;

            if (! isset($phar[$entry])) {
                throw HostSystemUpdateManifestException::manifestMissing();
            }

            $contents = $phar[$entry]->getContent();

            if (! is_string($contents) || trim($contents) === '') {
                throw HostSystemUpdateManifestException::manifestMissing();
            }

            return $this->decodeManifestContents($contents);
        } catch (HostSystemUpdateManifestException $exception) {
            throw $exception;
        } catch (\Throwable) {
            throw HostSystemUpdateManifestException::archiveOpenFailed();
        }
    }

    private function assertPackageVerified(HostSystemUpdatePackage $package): void
    {
        if (! $package->verified || $package->download_status !== HostSystemUpdatePackage::STATUS_VERIFIED) {
            throw HostSystemUpdateManifestException::packageNotVerified();
        }
    }

    private function locateManifestIndexInZip(ZipArchive $zip): ?int
    {
        for ($index = 0; $index < $zip->numFiles; $index++) {
            $name = $zip->getNameIndex($index);

            if (! is_string($name)) {
                continue;
            }

            $normalized = ltrim(str_replace('\\', '/', $name), './');

            if ($normalized === HostSystemUpdateManifestContract::MANIFEST_ENTRY) {
                return $index;
            }
        }

        return null;
    }

    /**
     * @return array<string, mixed>
     */
    private function decodeManifestContents(string $contents): array
    {
        try {
            /** @var mixed $decoded */
            $decoded = json_decode($contents, true, 512, JSON_THROW_ON_ERROR);
        } catch (\JsonException $exception) {
            throw HostSystemUpdateManifestException::invalidJson($exception);
        }

        if (! is_array($decoded)) {
            throw HostSystemUpdateManifestException::invalidJson();
        }

        $this->assertRawManifestFieldTypes($decoded);

        return $this->normalizeManifest($decoded);
    }

    /** @param array<string, mixed> $manifest */
    private function assertRawManifestFieldTypes(array $manifest): void
    {
        foreach ([
            HostSystemUpdateManifestContract::PRODUCT_CODE,
            HostSystemUpdateManifestContract::FROM_VERSION,
            HostSystemUpdateManifestContract::TO_VERSION,
            HostSystemUpdateManifestContract::PACKAGE_TYPE,
            HostSystemUpdateManifestContract::RELEASE_CHANNEL,
            HostSystemUpdateManifestContract::MIN_UPGRADABLE_VERSION,
            HostSystemUpdateManifestContract::ROLLBACK_STRATEGY,
        ] as $field) {
            if (array_key_exists($field, $manifest)
                && (! is_string($manifest[$field]) || trim($manifest[$field]) === '')) {
                throw HostSystemUpdateManifestException::validationFailed(
                    "manifest.{$field} 格式无效。",
                    'manifest_field_type_invalid',
                );
            }
        }

        foreach ([
            HostSystemUpdateManifestContract::FROM_UPDATE_VERSION,
            HostSystemUpdateManifestContract::TO_UPDATE_VERSION,
            HostSystemUpdateManifestContract::MIN_UPGRADABLE_UPDATE_VERSION,
        ] as $field) {
            if (array_key_exists($field, $manifest) && ! is_int($manifest[$field])) {
                throw HostSystemUpdateManifestException::validationFailed(
                    "manifest.{$field} 必须为整数。",
                    'manifest_field_type_invalid',
                );
            }
        }

        foreach ([
            HostSystemUpdateManifestContract::FORCE_UPDATE,
            HostSystemUpdateManifestContract::BACKUP_REQUIRED,
            HostSystemUpdateManifestContract::MAINTENANCE_MODE_REQUIRED,
        ] as $field) {
            if (array_key_exists($field, $manifest) && ! is_bool($manifest[$field])) {
                throw HostSystemUpdateManifestException::validationFailed(
                    "manifest.{$field} 必须为布尔值。",
                    'manifest_field_type_invalid',
                );
            }
        }

        if (array_key_exists(HostSystemUpdateManifestContract::REQUIRES, $manifest)
            && ! is_array($manifest[HostSystemUpdateManifestContract::REQUIRES])) {
            throw HostSystemUpdateManifestException::validationFailed(
                'manifest.requires 必须为对象。',
                'manifest_requires_invalid',
            );
        }

        foreach ([
            HostSystemUpdateManifestContract::FILES,
            HostSystemUpdateManifestContract::DELETE_FILES,
            HostSystemUpdateManifestContract::PROTECTED_PATHS,
            HostSystemUpdateManifestContract::MIGRATIONS,
            HostSystemUpdateManifestContract::COMMANDS,
        ] as $field) {
            if (! array_key_exists($field, $manifest)) {
                continue;
            }

            $value = $manifest[$field];

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

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

        if (array_key_exists(HostSystemUpdateManifestContract::CHECKSUMS, $manifest)) {
            $checksums = $manifest[HostSystemUpdateManifestContract::CHECKSUMS];

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

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

        if (array_key_exists(HostSystemUpdateManifestContract::RUNTIME, $manifest)
            && ! is_array($manifest[HostSystemUpdateManifestContract::RUNTIME])) {
            throw HostSystemUpdateManifestException::validationFailed(
                'manifest.runtime 必须为对象。',
                'manifest_runtime_invalid',
            );
        }
    }

    /**
     * @param  array<string, mixed>  $manifest
     * @return array<string, mixed>
     */
    private function normalizeManifest(array $manifest): array
    {
        return [
            HostSystemUpdateManifestContract::PRODUCT_CODE => $this->nullableString($manifest[HostSystemUpdateManifestContract::PRODUCT_CODE] ?? null),
            HostSystemUpdateManifestContract::FROM_VERSION => $this->nullableString($manifest[HostSystemUpdateManifestContract::FROM_VERSION] ?? null),
            HostSystemUpdateManifestContract::FROM_UPDATE_VERSION => $this->nullableInt($manifest[HostSystemUpdateManifestContract::FROM_UPDATE_VERSION] ?? null),
            HostSystemUpdateManifestContract::TO_VERSION => $this->nullableString($manifest[HostSystemUpdateManifestContract::TO_VERSION] ?? null),
            HostSystemUpdateManifestContract::TO_UPDATE_VERSION => $this->nullableInt($manifest[HostSystemUpdateManifestContract::TO_UPDATE_VERSION] ?? null),
            HostSystemUpdateManifestContract::PACKAGE_TYPE => $this->nullableString($manifest[HostSystemUpdateManifestContract::PACKAGE_TYPE] ?? null),
            HostSystemUpdateManifestContract::RELEASE_CHANNEL => $this->nullableString($manifest[HostSystemUpdateManifestContract::RELEASE_CHANNEL] ?? null),
            HostSystemUpdateManifestContract::FORCE_UPDATE => $this->nullableBool($manifest[HostSystemUpdateManifestContract::FORCE_UPDATE] ?? null),
            HostSystemUpdateManifestContract::MIN_UPGRADABLE_VERSION => $this->nullableString($manifest[HostSystemUpdateManifestContract::MIN_UPGRADABLE_VERSION] ?? null),
            HostSystemUpdateManifestContract::MIN_UPGRADABLE_UPDATE_VERSION => $this->nullableInt($manifest[HostSystemUpdateManifestContract::MIN_UPGRADABLE_UPDATE_VERSION] ?? null),
            HostSystemUpdateManifestContract::REQUIRES => $this->normalizeRequires($manifest[HostSystemUpdateManifestContract::REQUIRES] ?? null),
            HostSystemUpdateManifestContract::FILES => $this->normalizeStringList($manifest[HostSystemUpdateManifestContract::FILES] ?? null),
            HostSystemUpdateManifestContract::DELETE_FILES => $this->normalizeStringList($manifest[HostSystemUpdateManifestContract::DELETE_FILES] ?? null),
            HostSystemUpdateManifestContract::PROTECTED_PATHS => $this->normalizeStringList($manifest[HostSystemUpdateManifestContract::PROTECTED_PATHS] ?? null),
            HostSystemUpdateManifestContract::MIGRATIONS => $this->normalizeStringList($manifest[HostSystemUpdateManifestContract::MIGRATIONS] ?? null),
            HostSystemUpdateManifestContract::COMMANDS => $this->normalizeStringList($manifest[HostSystemUpdateManifestContract::COMMANDS] ?? null),
            HostSystemUpdateManifestContract::CHECKSUMS => $this->normalizeChecksumMap($manifest[HostSystemUpdateManifestContract::CHECKSUMS] ?? null),
            HostSystemUpdateManifestContract::BACKUP_REQUIRED => $this->nullableBool($manifest[HostSystemUpdateManifestContract::BACKUP_REQUIRED] ?? null),
            HostSystemUpdateManifestContract::MAINTENANCE_MODE_REQUIRED => $this->nullableBool($manifest[HostSystemUpdateManifestContract::MAINTENANCE_MODE_REQUIRED] ?? null),
            HostSystemUpdateManifestContract::ROLLBACK_STRATEGY => $this->nullableString($manifest[HostSystemUpdateManifestContract::ROLLBACK_STRATEGY] ?? null),
            HostSystemUpdateManifestContract::VENDOR => $this->normalizeVendor($manifest[HostSystemUpdateManifestContract::VENDOR] ?? null),
            HostSystemUpdateManifestContract::RUNTIME => $this->normalizeRuntime($manifest[HostSystemUpdateManifestContract::RUNTIME] ?? null),
            HostSystemUpdateManifestContract::NOTES => $manifest[HostSystemUpdateManifestContract::NOTES] ?? null,
        ];
    }

    /**
     * @return array<string, string|int>|null
     */
    private function normalizeRequires(mixed $value): ?array
    {
        if (! is_array($value)) {
            return null;
        }

        $requires = [];

        foreach (['php', 'laravel', 'filament', 'livewire'] as $key) {
            $item = $value[$key] ?? null;

            if (is_string($item) && trim($item) !== '') {
                $requires[$key] = trim($item);
            }
        }

        foreach (['min_version', 'min_display_version'] as $key) {
            $item = $this->nullableString($value[$key] ?? null);

            if ($item !== null) {
                $requires[$key] = $item;
            }
        }

        foreach (['min_update_version', 'min_upgradable_update_version'] as $key) {
            $item = $this->nullableInt($value[$key] ?? null);

            if ($item !== null) {
                $requires[$key] = $item;
            }
        }

        return $requires === [] ? null : $requires;
    }

    /**
     * @return array<string, mixed>|string|null
     */
    private function normalizeVendor(mixed $value): array|string|null
    {
        if (is_string($value)) {
            return $this->nullableString($value);
        }

        if (! is_array($value)) {
            return null;
        }

        $vendor = [];

        foreach ([
            'strategy',
            'bundle_path',
            'bundle_sha256',
            'composer_lock_sha256',
            'dependency_policy',
            'decision_reason',
            'dependency_fingerprint_sha256',
        ] as $key) {
            $item = $this->nullableString($value[$key] ?? null);

            if ($item !== null) {
                $vendor[$key] = $key === 'bundle_path'
                    ? str_replace('\\', '/', $item)
                    : $item;
            }
        }

        foreach (['files', 'vendor_path'] as $key) {
            $item = $value[$key] ?? null;

            if (is_string($item)) {
                $normalized = $this->nullableString($item);

                if ($normalized !== null) {
                    $vendor[$key] = str_replace('\\', '/', $normalized);
                }

                continue;
            }

            if (is_array($item)) {
                $vendor[$key] = $this->normalizeStringList($item) ?? [];
            }
        }

        return $vendor === [] ? null : $vendor;
    }

    /**
     * @return array<string, mixed>|null
     */
    private function normalizeRuntime(mixed $value): ?array
    {
        if (! is_array($value)) {
            return null;
        }

        $runtime = [];
        $lockHash = $this->nullableString($value['composer_lock_sha256'] ?? null);

        if ($lockHash !== null) {
            $runtime['composer_lock_sha256'] = strtolower($lockHash);
        }

        if (is_array($value['required_packages'] ?? null)) {
            $packages = [];

            foreach ($value['required_packages'] as $name => $version) {
                if (is_string($name) && is_string($version)) {
                    $packages[$name] = $version;
                }
            }

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

        if (is_array($value['required_classes'] ?? null)) {
            $classes = [];

            foreach ($value['required_classes'] as $class) {
                $normalized = $this->nullableString($class);

                if ($normalized !== null) {
                    $classes[] = $normalized;
                }
            }

            $runtime['required_classes'] = array_values(array_unique($classes));
        }

        return $runtime === [] ? null : $runtime;
    }

    /**
     * @return list<string>|null
     */
    private function normalizeStringList(mixed $value): ?array
    {
        if ($value === null) {
            return null;
        }

        if (! is_array($value)) {
            return null;
        }

        $items = [];

        foreach ($value as $item) {
            if (is_string($item) && trim($item) !== '') {
                $items[] = trim(str_replace('\\', '/', $item));
            }
        }

        return $items === [] ? [] : array_values(array_unique($items));
    }

    /**
     * @return array<string, string>|null
     */
    private function normalizeChecksumMap(mixed $value): ?array
    {
        if (! is_array($value)) {
            return null;
        }

        if ($value === []) {
            return [];
        }

        $map = [];

        foreach ($value as $path => $checksum) {
            if (! is_string($path) || ! is_string($checksum)) {
                continue;
            }

            $trimmedPath = trim(str_replace('\\', '/', $path));
            $trimmedChecksum = trim($checksum);

            if ($trimmedPath === '' || $trimmedChecksum === '') {
                continue;
            }

            $map[$trimmedPath] = $trimmedChecksum;
        }

        return $map;
    }

    private function nullableString(mixed $value): ?string
    {
        if (! is_string($value)) {
            return null;
        }

        $trimmed = trim($value);

        return $trimmed === '' ? null : $trimmed;
    }

    private function nullableInt(mixed $value): ?int
    {
        if ($value === null || $value === '') {
            return null;
        }

        if (is_int($value)) {
            return $value;
        }

        if (is_numeric($value)) {
            return (int) $value;
        }

        return null;
    }

    private function nullableBool(mixed $value): ?bool
    {
        if ($value === null) {
            return null;
        }

        return filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
    }
}
