<?php
declare(strict_types=1);

namespace SafeTun\Backend\Crypto;

/** Exact detached-signature contract shared with internal/updater. */
final class ReleaseManifest
{
    public const DOMAIN = 'SAFETUN-RELEASE-MANIFEST-v1';
    public const PREFIX = "SAFETUN-RELEASE-MANIFEST-v1\0";
    private const MAX_BYTES = 1048576;

    public static function signatureInput(string $rawManifest): string
    {
        if ($rawManifest === '' || strlen($rawManifest) > self::MAX_BYTES) {
            throw new \InvalidArgumentException('Release manifest size is invalid.');
        }
        return self::PREFIX . $rawManifest;
    }

    public static function decodeSignature(string $value): string
    {
        if (strlen($value) === SODIUM_CRYPTO_SIGN_BYTES) {
            return $value;
        }
        $trimmed = trim($value);
        if (str_starts_with($trimmed, 'base64:')) {
            $trimmed = substr($trimmed, 7);
        }
        return Base64Url::decode($trimmed, SODIUM_CRYPTO_SIGN_BYTES);
    }

    public static function verifyDetached(string $rawManifest, string $rawSignature, string $publicKey): void
    {
        if (strlen($rawSignature) !== SODIUM_CRYPTO_SIGN_BYTES
            || strlen($publicKey) !== SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES
            || !sodium_crypto_sign_verify_detached(
                $rawSignature,
                self::signatureInput($rawManifest),
                $publicKey,
            )) {
            throw new \InvalidArgumentException('Release manifest signature is invalid.');
        }
    }

    /** @return array{v:int,domain:string,kid:string,payload:string,sig:string} */
    public static function wrap(string $kid, string $rawManifest, string $rawSignature): array
    {
        self::validateKeyId($kid);
        if (strlen($rawSignature) !== SODIUM_CRYPTO_SIGN_BYTES) {
            throw new \InvalidArgumentException('Release signature length is invalid.');
        }
        self::signatureInput($rawManifest);
        return [
            'v' => 1,
            'domain' => self::DOMAIN,
            'kid' => $kid,
            'payload' => Base64Url::encode($rawManifest),
            'sig' => Base64Url::encode($rawSignature),
        ];
    }

    /** @param array<string,mixed> $envelope @param array<string,string> $publicKeysByKid */
    public static function verifyEnvelope(array $envelope, array $publicKeysByKid): string
    {
        $actual = array_keys($envelope);
        $expected = ['v', 'domain', 'kid', 'payload', 'sig'];
        sort($actual, SORT_STRING);
        sort($expected, SORT_STRING);
        if ($actual !== $expected || ($envelope['v'] ?? null) !== 1
            || ($envelope['domain'] ?? null) !== self::DOMAIN
            || !is_string($envelope['kid'] ?? null)
            || !is_string($envelope['payload'] ?? null)
            || !is_string($envelope['sig'] ?? null)) {
            throw new \InvalidArgumentException('Release manifest envelope is malformed.');
        }
        self::validateKeyId($envelope['kid']);
        if (!array_key_exists($envelope['kid'], $publicKeysByKid)) {
            throw new \InvalidArgumentException('Release signing key ID is unknown.');
        }
        $raw = Base64Url::decode($envelope['payload']);
        $signature = Base64Url::decode($envelope['sig'], SODIUM_CRYPTO_SIGN_BYTES);
        self::verifyDetached($raw, $signature, $publicKeysByKid[$envelope['kid']]);
        return $raw;
    }

    private static function validateKeyId(string $kid): void
    {
        if (!preg_match('/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/D', $kid)) {
            throw new \InvalidArgumentException('Release key ID is invalid.');
        }
    }
}
