<?php

declare(strict_types=1);

namespace EasyChat\Library\Update;

final class ReleaseManifestVerifier
{
    public const KEY_ID = 'easychat-release-2026-01';
    public const SCHEMA = 'easychat.update.v1';

    private const ARTIFACT_CONTRACT = [
        'opencart_oc23_oc3' => ['opencart', 'cms_native', ['2.3', '3']],
        'opencart_oc4' => ['opencart', 'cms_native_guided', ['4']],
        'opencart_oc4_migration' => ['opencart', 'cms_native_guided', ['4']],
        'wordpress' => ['wordpress', 'cms_native', []],
        'webasyst' => ['webasyst', 'cms_native', []],
        'bitrix' => ['bitrix', 'cms_native', []],
        'generic' => ['generic', 'managed_runtime', []],
    ];

    private string $publicKeyFile;

    public function __construct(?string $publicKeyFile = null)
    {
        $this->publicKeyFile = $publicKeyFile ?: SITE_PATH . '/keys/easychat-release-2026-01-public.pem';
    }

     
    public function verify(array $envelope): array
    {
        if (($envelope['schema'] ?? '') !== self::SCHEMA
            || !is_array($envelope['payload'] ?? null)
            || !is_array($envelope['signature'] ?? null)
        ) {
            throw new \RuntimeException('update_manifest_invalid');
        }

        $signature = $envelope['signature'];
        if (($signature['algorithm'] ?? '') !== 'rsa-sha256'
            || ($signature['key_id'] ?? '') !== self::KEY_ID
        ) {
            throw new \RuntimeException('update_signature_policy_rejected');
        }

        $signatureBytes = base64_decode((string)($signature['value'] ?? ''), true);
        $publicPem = is_file($this->publicKeyFile) && !is_link($this->publicKeyFile)
            ? file_get_contents($this->publicKeyFile)
            : false;
        $publicKey = is_string($publicPem) && function_exists('openssl_pkey_get_public')
            ? openssl_pkey_get_public($publicPem)
            : false;
        $publicDetails = $publicKey !== false ? openssl_pkey_get_details($publicKey) : false;
        if (!is_string($signatureBytes)
            || strlen($signatureBytes) !== 384
            || $publicKey === false
            || !is_array($publicDetails)
            || (int)($publicDetails['type'] ?? -1) !== OPENSSL_KEYTYPE_RSA
            || (int)($publicDetails['bits'] ?? 0) !== 3072
        ) {
            throw new \RuntimeException('update_signature_unavailable');
        }

        $payload = $this->validatePayload($envelope['payload']);
        $verified = openssl_verify(
            ReleaseManifestCanonicalizer::json($envelope['payload']),
            $signatureBytes,
            $publicKey,
            OPENSSL_ALGO_SHA256
        );
        if ($verified !== 1) {
            throw new \RuntimeException('update_signature_invalid');
        }

        return $payload;
    }

     
    private function validatePayload(array $payload): array
    {
        $version = trim((string)($payload['version'] ?? ''));
        $minimum = trim((string)($payload['minimum_updater_version'] ?? ''));
        $publishedAt = trim((string)($payload['published_at'] ?? ''));
        $releaseNotes = trim((string)($payload['release_notes_url'] ?? ''));
        $expectedReleaseNotes = 'https://docs.easychat24.ru/docs/releases/'
            . str_replace('.', '-', preg_replace('/[-+].*$/', '', $version)) . '/';
        $publishedTimestamp = strtotime($publishedAt);
        if (!$this->isVersion($version)
            || !$this->isVersion($minimum)
            || ($payload['channel'] ?? '') !== 'stable'
            || !preg_match('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/D', $publishedAt)
            || $publishedTimestamp === false
            || $publishedTimestamp > time() + 300
            || !hash_equals($expectedReleaseNotes, $releaseNotes)
            || !is_array($payload['artifacts'] ?? null)
            || count($payload['artifacts']) !== count(self::ARTIFACT_CONTRACT)
        ) {
            throw new \RuntimeException('update_payload_invalid');
        }

        $artifacts = [];
        foreach ($payload['artifacts'] as $artifact) {
            if (!is_array($artifact)) {
                throw new \RuntimeException('update_artifact_invalid');
            }
            $key = (string)($artifact['key'] ?? '');
            if (!isset(self::ARTIFACT_CONTRACT[$key]) || isset($artifacts[$key])) {
                throw new \RuntimeException('update_artifact_not_allowed');
            }
            [$platform, $mode, $cmsMajor] = self::ARTIFACT_CONTRACT[$key];
            $filename = (string)($artifact['filename'] ?? '');
            $sha256 = strtolower((string)($artifact['sha256'] ?? ''));
            $size = $artifact['size_bytes'] ?? null;
            if (($artifact['platform'] ?? '') !== $platform
                || ($artifact['install_mode'] ?? '') !== $mode
                || ($artifact['supported_cms_major'] ?? null) !== $cmsMajor
                || !preg_match('/^[A-Za-z0-9][A-Za-z0-9._-]{1,159}$/D', $filename)
                || basename($filename) !== $filename
                || !preg_match('/^[a-f0-9]{64}$/D', $sha256)
                || !is_int($size)
                || $size < 1024
                || $size > 100 * 1024 * 1024
                || ($artifact['download_path'] ?? '') !== '/download/' . $key
            ) {
                throw new \RuntimeException('update_artifact_contract_mismatch');
            }
            $artifact['sha256'] = $sha256;
            $artifact['download_url'] = 'https://easychat24.ru' . $artifact['download_path'];
            $artifacts[$key] = $artifact;
        }

        $payload['artifacts_by_key'] = $artifacts;
        return $payload;
    }

    private function isVersion(string $version): bool
    {
        return (bool)preg_match('/^(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)(?:[-+][A-Za-z0-9.]+)?$/D', $version);
    }
}
