<?php

declare(strict_types=1);

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();
        }

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

    /**
     * @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::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::NOTES => $manifest[HostSystemUpdateManifestContract::NOTES] ?? null,
        ];
    }

    /**
     * @return array<string, string>|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);
            }
        }

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

    /**
     * @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);
    }
}
