<?php

declare(strict_types=1);

final class MyronMarketplaceEnvelopeVerifier
{
    public const SIGNATURE_ALGO_HMAC_SHA256 = 'hmac-sha256';
    public const SIGNATURE_ALGO_ED25519 = 'ed25519';

    public function __construct(
        private readonly MyronMarketplaceKeyResolver $keyResolver,
        private readonly string $installedVersion = '',
    ) {
    }

    public function verify(ZipArchive $zip, MyronMarketplaceEnvelope $envelope): void
    {
        $this->verifyEnvelopeHash($envelope);
        $this->verifyInnerManifestHash($zip, $envelope);
        $this->verifySignature($envelope);
        $this->verifyCompatibility($envelope);
    }

    /**
     * @param array<string, mixed> $envelope
     */
    public static function signingInputFor(array $envelope): string
    {
        $normalized = self::withoutSignature($envelope);
        $signing = is_array($envelope['signing'] ?? null) ? $envelope['signing'] : [];
        $envelopeHash = hash('sha256', self::canonicalJson($normalized));
        $innerHash = strtolower(trim((string) ($signing['innerManifestSha256'] ?? '')));
        $packageId = trim((string) ($envelope['packageId'] ?? ''));
        $version = trim((string) ($envelope['version'] ?? ''));

        return $envelopeHash . ':' . $innerHash . ':' . $packageId . ':' . $version;
    }

    /**
     * @param array<string, mixed> $envelope
     */
    public static function signEnvelope(array $envelope, string $secret): string
    {
        return self::signEnvelopeHmac($envelope, $secret);
    }

    /**
     * @param array<string, mixed> $envelope
     */
    public static function signEnvelopeHmac(array $envelope, string $secret): string
    {
        return self::base64UrlEncode(hash_hmac('sha256', self::signingInputFor($envelope), $secret, true));
    }

    /**
     * @param array<string, mixed> $envelope
     */
    public static function signEnvelopeEd25519(array $envelope, string $privateKey): string
    {
        if (!function_exists('sodium_crypto_sign_detached')) {
            throw new RuntimeException('marketplace_crypto_unavailable: sodium ed25519 signing is unavailable.');
        }

        $secretKey = self::decodeEd25519PrivateKey($privateKey);
        return self::base64UrlEncode(sodium_crypto_sign_detached(self::signingInputFor($envelope), $secretKey));
    }

    /**
     * @param array<string, mixed> $value
     */
    public static function envelopeHash(array $value): string
    {
        return hash('sha256', self::canonicalJson(self::withoutSignature($value)));
    }

    private function verifyEnvelopeHash(MyronMarketplaceEnvelope $envelope): void
    {
        $actual = self::envelopeHash($envelope->raw());
        if (!hash_equals($envelope->signingValue('envelopeSha256'), $actual)) {
            throw new RuntimeException('envelope_signature_invalid: marketplace envelope hash mismatch.');
        }
    }

    private function verifyInnerManifestHash(ZipArchive $zip, MyronMarketplaceEnvelope $envelope): void
    {
        $contents = $zip->getFromName($envelope->innerManifestPayloadPath());
        if (!is_string($contents)) {
            throw new RuntimeException('envelope_signature_invalid: marketplace inner manifest is missing.');
        }

        $actual = hash('sha256', $contents);
        if (!hash_equals($envelope->signingValue('innerManifestSha256'), $actual)) {
            throw new RuntimeException('envelope_signature_invalid: marketplace inner manifest hash mismatch.');
        }
    }

    private function verifySignature(MyronMarketplaceEnvelope $envelope): void
    {
        $algo = $envelope->signingValue('signatureAlgo');
        if ($algo === self::SIGNATURE_ALGO_HMAC_SHA256) {
            $secret = $this->keyResolver->resolveHmacSecret($envelope->signingValue('keyId'));
            $expected = self::signEnvelopeHmac($envelope->raw(), $secret);
            if (!hash_equals($envelope->signingValue('signature'), $expected)) {
                throw new RuntimeException('envelope_signature_invalid: marketplace hmac signature could not be verified.');
            }
            return;
        }

        if ($algo === self::SIGNATURE_ALGO_ED25519) {
            $this->verifyEd25519Signature($envelope);
            return;
        }

        throw new RuntimeException('envelope_signature_algorithm_unsupported: ' . $algo);
    }

    private function verifyEd25519Signature(MyronMarketplaceEnvelope $envelope): void
    {
        if (!function_exists('sodium_crypto_sign_verify_detached')) {
            throw new RuntimeException('marketplace_crypto_unavailable: sodium ed25519 verification is unavailable.');
        }

        $signature = self::base64UrlDecode($envelope->signingValue('signature'));
        if (strlen($signature) !== SODIUM_CRYPTO_SIGN_BYTES) {
            throw new RuntimeException('envelope_signature_invalid: marketplace ed25519 signature length is invalid.');
        }

        $publicKey = self::base64UrlDecode($this->keyResolver->resolveEd25519PublicKey($envelope->signingValue('keyId')));
        if (strlen($publicKey) !== SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES) {
            throw new RuntimeException('marketplace_trust_root_invalid: ed25519 public key length is invalid.');
        }

        if (!sodium_crypto_sign_verify_detached($signature, self::signingInputFor($envelope->raw()), $publicKey)) {
            throw new RuntimeException('envelope_signature_invalid: marketplace ed25519 signature could not be verified.');
        }
    }

    private function verifyCompatibility(MyronMarketplaceEnvelope $envelope): void
    {
        $installedVersion = $this->installedVersion;
        if ($installedVersion === '' && class_exists(MyronReleaseMetadata::class)) {
            $installedVersion = MyronReleaseMetadata::VERSION;
        }
        if ($installedVersion === '') {
            return;
        }

        $range = trim((string) ($envelope->compatibility()['myroncmsVersionRange'] ?? ''));
        if ($range === '') {
            throw new RuntimeException('Marketplace envelope compatibility range is missing.');
        }

        $comparator = class_exists(MyronVersionComparator::class) ? new MyronVersionComparator() : null;
        if (!$comparator instanceof MyronVersionComparator) {
            return;
        }

        foreach (preg_split('/\s+/', $range) ?: [] as $constraint) {
            $constraint = trim($constraint);
            if ($constraint === '') {
                continue;
            }
            if (!$this->matchesConstraint($comparator, $installedVersion, $constraint)) {
                throw new RuntimeException('Marketplace package is not compatible with this MyronCMS version.');
            }
        }
    }

    private function matchesConstraint(MyronVersionComparator $comparator, string $installedVersion, string $constraint): bool
    {
        if (!preg_match('/^(>=|>|<=|<|=)?(.+)$/', $constraint, $matches)) {
            throw new RuntimeException('Marketplace envelope compatibility range is invalid.');
        }

        $operator = $matches[1] !== '' ? $matches[1] : '=';
        $version = trim($matches[2]);
        $comparison = $comparator->compare($installedVersion, $version);

        return match ($operator) {
            '>=' => $comparison >= 0,
            '>' => $comparison > 0,
            '<=' => $comparison <= 0,
            '<' => $comparison < 0,
            '=' => $comparison === 0,
            default => false,
        };
    }

    /**
     * @param array<string, mixed> $value
     * @return array<string, mixed>
     */
    private static function withoutSignature(array $value): array
    {
        if (is_array($value['signing'] ?? null)) {
            unset($value['signing']['signature']);
            unset($value['signing']['envelopeSha256']);
        }

        return $value;
    }

    /**
     * @param mixed $value
     */
    private static function canonicalJson(mixed $value): string
    {
        $normalized = self::sortKeysRecursively($value);
        return json_encode($normalized, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);
    }

    /**
     * @param mixed $value
     * @return mixed
     */
    private static function sortKeysRecursively(mixed $value): mixed
    {
        if (!is_array($value)) {
            return $value;
        }

        $isList = array_keys($value) === range(0, count($value) - 1);
        if ($isList) {
            return array_map([self::class, 'sortKeysRecursively'], $value);
        }

        ksort($value, SORT_STRING);
        foreach ($value as $key => $item) {
            $value[$key] = self::sortKeysRecursively($item);
        }

        return $value;
    }

    private static function base64UrlEncode(string $raw): string
    {
        return rtrim(strtr(base64_encode($raw), '+/', '-_'), '=');
    }

    private static function base64UrlDecode(string $encoded): string
    {
        $encoded = trim($encoded);
        if ($encoded === '') {
            return '';
        }
        $padded = strtr($encoded, '-_', '+/');
        $padding = strlen($padded) % 4;
        if ($padding > 0) {
            $padded .= str_repeat('=', 4 - $padding);
        }
        $decoded = base64_decode($padded, true);
        if (!is_string($decoded)) {
            throw new RuntimeException('marketplace_base64_invalid');
        }

        return $decoded;
    }

    private static function decodeEd25519PrivateKey(string $privateKey): string
    {
        $secretKey = self::base64UrlDecode($privateKey);
        if (strlen($secretKey) === SODIUM_CRYPTO_SIGN_SECRETKEYBYTES) {
            return $secretKey;
        }
        if (strlen($secretKey) === SODIUM_CRYPTO_SIGN_SEEDBYTES) {
            return sodium_crypto_sign_secretkey(sodium_crypto_sign_seed_keypair($secretKey));
        }

        throw new RuntimeException('marketplace_private_key_invalid: ed25519 private key length is invalid.');
    }
}
