<?php

declare(strict_types=1);

require_once __DIR__ . '/OperatorStateUpdateBoundary.php';

final class MyronRemoteUpdateManifestValidator
{
    public const SUPPORTED_MANIFEST_VERSION = 1;

    public function __construct(
        private readonly MyronVersionComparator $versionComparator,
    ) {
    }

    /**
     * @param array<string, mixed> $manifest
     * @return array<string, mixed>
     */
    public function validate(array $manifest, string $expectedChannel, string $installedVersion): array
    {
        $product = strtolower(trim((string) ($manifest['product'] ?? '')));
        if ($product !== MyronReleaseMetadata::PRODUCT_KEY) {
            throw new InvalidArgumentException('Update manifest product must be myroncms.');
        }

        if ((int) ($manifest['manifest_version'] ?? 0) !== self::SUPPORTED_MANIFEST_VERSION) {
            throw new InvalidArgumentException('Update manifest version is not supported.');
        }

        $channel = strtolower(trim((string) ($manifest['channel'] ?? '')));
        if ($channel !== $expectedChannel) {
            throw new InvalidArgumentException('Update manifest channel does not match configured channel.');
        }

        $latestVersion = trim((string) ($manifest['latest_version'] ?? ''));
        if ($latestVersion !== '' && strtolower($latestVersion[0]) === 'v') {
            $latestVersion = 'v' . substr($latestVersion, 1);
        } elseif ($latestVersion !== '') {
            $latestVersion = 'v' . $latestVersion;
        }
        $this->versionComparator->normalize($latestVersion);
        $policy = $this->normalizeReleasePolicy($manifest);
        $minimumMyronCms = trim((string) ($manifest['minimum_myroncms'] ?? ''));
        if ($minimumMyronCms !== '' && !$this->versionComparator->isAtLeast($installedVersion, $minimumMyronCms)) {
            throw new RuntimeException('Update requires MyronCMS ' . $minimumMyronCms . ' or newer.');
        }

        $minimumPhp = trim((string) ($manifest['minimum_php'] ?? ''));
        if ($minimumPhp !== '' && version_compare(PHP_VERSION, $minimumPhp, '<')) {
            throw new RuntimeException('Update requires PHP ' . $minimumPhp . ' or newer.');
        }

        $package = $manifest['package'] ?? null;
        if (!is_array($package)) {
            throw new InvalidArgumentException('Update manifest must declare package metadata.');
        }

        $packageUrl = trim((string) ($package['url'] ?? ''));
        if ($packageUrl === '' && empty($policy['gated'])) {
            throw new InvalidArgumentException('Update package URL is required for public update packages.');
        }
        if ($packageUrl !== '' && !$this->isHttpsUrl($packageUrl)) {
            throw new InvalidArgumentException('Update package URL must use HTTPS.');
        }
        if (!empty($policy['gated']) && $packageUrl !== '' && str_contains((string) parse_url($packageUrl, PHP_URL_PATH), '/releases/')) {
            throw new InvalidArgumentException('Gated update packages must use tokenized download URLs.');
        }

        $checksum = strtolower(trim((string) ($package['checksum_sha256'] ?? '')));
        if (!preg_match('/^[a-f0-9]{64}$/', $checksum)) {
            throw new InvalidArgumentException('Update package checksum must be a 64-character SHA-256 hex string.');
        }

        $releaseNotes = $manifest['release_notes'] ?? null;
        if (!is_array($releaseNotes)) {
            throw new InvalidArgumentException('Update manifest must declare release notes.');
        }

        $summary = trim((string) ($releaseNotes['summary'] ?? ''));
        $notesUrl = trim((string) ($releaseNotes['url'] ?? ''));
        if ($summary === '' || !$this->isHttpsUrl($notesUrl)) {
            throw new InvalidArgumentException('Update release notes must include summary and HTTPS URL.');
        }
        if (str_word_count($summary) > 45 || $this->containsPrivateReleaseLanguage($releaseNotes)) {
            throw new InvalidArgumentException('Update release notes must be brief and customer-safe.');
        }

        return [
            'product' => $product,
            'manifestVersion' => self::SUPPORTED_MANIFEST_VERSION,
            'channel' => $channel,
            'latestVersion' => $latestVersion,
            'releaseDate' => trim((string) ($manifest['release_date'] ?? '')),
            'minimumPhp' => $minimumPhp,
            'minimumMyroncms' => $minimumMyronCms,
            'package' => [
                'url' => $packageUrl,
                'filename' => trim((string) ($package['filename'] ?? basename(parse_url($packageUrl, PHP_URL_PATH) ?: ''))),
                'sizeBytes' => (int) ($package['size_bytes'] ?? 0),
                'checksumSha256' => $checksum,
                'gated' => (bool) ($package['gated'] ?? $policy['gated']),
            ],
            'releaseNotes' => [
                'summary' => $summary,
                'url' => $notesUrl,
                'jsonUrl' => trim((string) ($releaseNotes['json_url'] ?? '')),
            ],
            'severity' => trim((string) ($manifest['severity'] ?? 'recommended')),
            'requiresBackup' => (bool) ($manifest['requires_backup'] ?? true),
            'requiresManualReview' => (bool) ($manifest['requires_manual_review'] ?? true),
            'migrationRequired' => (bool) ($manifest['migration_required'] ?? false),
            'updateClass' => $policy['updateClass'],
            'requiredCapability' => $policy['requiredCapability'],
            'packageTier' => $policy['packageTier'],
            'licensePolicy' => $policy,
            'raw' => $manifest,
        ];
    }

    /**
     * @param array<string, mixed> $manifest
     * @return array{updateClass:string,requiredCapability:string,gated:bool,packageTier:string}
     */
    private function normalizeReleasePolicy(array $manifest): array
    {
        $knownClasses = [
            'security_patch' => '',
            'maintenance_patch' => '',
            'feature_update' => 'updates.feature',
            'pro_feature_update' => 'updates.pro',
            'agency_enterprise_update' => 'updates.agency',
        ];
        $policy = is_array($manifest['license_policy'] ?? null) ? $manifest['license_policy'] : [];
        $updateClass = strtolower(trim((string) ($manifest['update_class'] ?? $policy['update_class'] ?? 'maintenance_patch')));
        if (!array_key_exists($updateClass, $knownClasses)) {
            throw new InvalidArgumentException('Update manifest declares an unknown release policy.');
        }

        $requiredCapability = trim((string) ($manifest['required_capability'] ?? $policy['required_capability'] ?? $knownClasses[$updateClass]));
        if (!in_array($updateClass, ['security_patch', 'maintenance_patch'], true) && $requiredCapability === '') {
            throw new InvalidArgumentException('Gated update classes must declare a required capability.');
        }

        $gated = (bool) ($policy['gated'] ?? !in_array($updateClass, ['security_patch', 'maintenance_patch'], true));
        $packageTier = strtolower(trim((string) ($manifest['package_tier'] ?? $policy['package_tier'] ?? $this->packageTierForCapability($requiredCapability))));
        if (!in_array($packageTier, ['core', 'personal', 'pro', 'agency', 'enterprise', 'partner'], true)) {
            throw new InvalidArgumentException('Update manifest declares an unknown package tier.');
        }
        return [
            'updateClass' => $updateClass,
            'requiredCapability' => $requiredCapability,
            'gated' => $gated,
            'packageTier' => $packageTier,
        ];
    }

    /**
     * @param array<string, mixed> $releaseNotes
     */
    private function containsPrivateReleaseLanguage(array $releaseNotes): bool
    {
        $serialized = strtolower(json_encode($releaseNotes, JSON_UNESCAPED_SLASHES) ?: '');
        $forbiddenPrivateReference = 'dev-' . 'references';
        foreach ([$forbiddenPrivateReference, 'myroncms-dev', 'private rationale', 'product rationale', 'basement'] as $forbidden) {
            if (str_contains($serialized, $forbidden)) {
                return true;
            }
        }

        return false;
    }

    private function packageTierForCapability(string $capability): string
    {
        return MyronOperatorStateUpdateBoundary::packageTierForCapability($capability);
    }

    private function isHttpsUrl(string $url): bool
    {
        return filter_var($url, FILTER_VALIDATE_URL) !== false
            && strtolower((string) parse_url($url, PHP_URL_SCHEME)) === 'https';
    }
}
