<?php
declare(strict_types=1);

namespace SafeTun\Backend\Crypto;

final class CanonicalJson
{
    public static function encode(mixed $value): string
    {
        $normalized = self::normalize($value);
        try {
            return json_encode(
                $normalized,
                JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION,
            );
        } catch (\JsonException $exception) {
            throw new \InvalidArgumentException('Value cannot be encoded as canonical JSON.', 0, $exception);
        }
    }

    private static function normalize(mixed $value): mixed
    {
        if (is_array($value)) {
            if (array_is_list($value)) {
                return array_map([self::class, 'normalize'], $value);
            }
            foreach (array_keys($value) as $key) {
                if (!is_string($key)) {
                    throw new \InvalidArgumentException('Canonical JSON object keys must be strings.');
                }
            }
            ksort($value, SORT_STRING);
            foreach ($value as $key => $child) {
                $value[$key] = self::normalize($child);
            }
            return $value;
        }
        if (is_object($value) || is_resource($value) || (!is_null($value) && !is_scalar($value))) {
            throw new \InvalidArgumentException('Unsupported canonical JSON value.');
        }
        if (is_float($value) && (!is_finite($value))) {
            throw new \InvalidArgumentException('Non-finite number in canonical JSON.');
        }
        return $value;
    }
}
