"""Bounded RFC 8785 JSON canonicalization for licensing documents.

Licensing documents deliberately exclude floating-point numbers. Restricting
numbers to the interoperable JSON safe-integer range removes cross-runtime
rounding ambiguity while retaining every counter and timestamp used here.
"""

from __future__ import annotations

import json
from typing import Any, Iterable, Mapping

from .constants import MAX_SAFE_JSON_INTEGER
from .errors import LicenseErrorCode, LicensingError


def _reject_float(_value: str) -> None:
    raise LicensingError(
        LicenseErrorCode.NON_CANONICAL_VALUE,
        "floating-point numbers are not allowed in signed licensing documents",
    )


def _reject_constant(value: str) -> None:
    raise LicensingError(
        LicenseErrorCode.NON_CANONICAL_VALUE,
        f"non-finite JSON constant is not allowed: {value}",
    )


def _object_without_duplicates(pairs: Iterable[tuple[str, Any]]) -> dict[str, Any]:
    result: dict[str, Any] = {}
    for key, value in pairs:
        if key in result:
            raise LicensingError(
                LicenseErrorCode.DUPLICATE_JSON_KEY,
                f"duplicate JSON object key: {key}",
            )
        result[key] = value
    return result


def _validate_string(value: str) -> None:
    for character in value:
        codepoint = ord(character)
        if 0xD800 <= codepoint <= 0xDFFF:
            raise LicensingError(
                LicenseErrorCode.NON_CANONICAL_VALUE,
                "unpaired Unicode surrogate is not allowed",
            )


def _validate_tree(value: Any, *, depth: int = 0) -> None:
    if depth > 64:
        raise LicensingError(
            LicenseErrorCode.INVALID_DOCUMENT,
            "JSON document exceeds the maximum nesting depth",
        )
    if value is None or isinstance(value, bool):
        return
    if isinstance(value, int):
        if abs(value) > MAX_SAFE_JSON_INTEGER:
            raise LicensingError(
                LicenseErrorCode.NON_CANONICAL_VALUE,
                "integer exceeds the interoperable JSON safe-integer range",
            )
        return
    if isinstance(value, float):
        _reject_float(repr(value))
    if isinstance(value, str):
        _validate_string(value)
        return
    if isinstance(value, (list, tuple)):
        for item in value:
            _validate_tree(item, depth=depth + 1)
        return
    if isinstance(value, Mapping):
        for key, item in value.items():
            if not isinstance(key, str):
                raise LicensingError(
                    LicenseErrorCode.NON_CANONICAL_VALUE,
                    "JSON object keys must be strings",
                )
            _validate_string(key)
            _validate_tree(item, depth=depth + 1)
        return
    raise LicensingError(
        LicenseErrorCode.NON_CANONICAL_VALUE,
        f"unsupported JSON value type: {type(value).__name__}",
    )


def parse_bounded_json(
    document: str | bytes | bytearray,
    *,
    maximum_bytes: int,
) -> Any:
    """Parse strict UTF-8 JSON with duplicate-key and numeric checks."""

    if isinstance(maximum_bytes, bool) or not isinstance(maximum_bytes, int):
        raise TypeError("maximum_bytes must be an integer")
    if maximum_bytes < 1:
        raise ValueError("maximum_bytes must be at least one")
    if isinstance(document, str):
        raw = document.encode("utf-8")
    elif isinstance(document, (bytes, bytearray)):
        raw = bytes(document)
    else:
        raise TypeError("document must be a string or bytes-like value")
    if len(raw) > maximum_bytes:
        raise LicensingError(
            LicenseErrorCode.DOCUMENT_TOO_LARGE,
            f"JSON document exceeds the maximum size of {maximum_bytes} bytes",
        )
    try:
        text = raw.decode("utf-8")
    except UnicodeDecodeError as exc:
        raise LicensingError(
            LicenseErrorCode.INVALID_DOCUMENT,
            "JSON document is not valid UTF-8",
        ) from exc
    try:
        value = json.loads(
            text,
            object_pairs_hook=_object_without_duplicates,
            parse_float=_reject_float,
            parse_int=int,
            parse_constant=_reject_constant,
        )
    except LicensingError:
        raise
    except (json.JSONDecodeError, TypeError, ValueError) as exc:
        raise LicensingError(
            LicenseErrorCode.INVALID_DOCUMENT,
            f"invalid JSON document: {exc}",
        ) from exc
    _validate_tree(value)
    return value


def _key_order(value: str) -> bytes:
    return value.encode("utf-16-be")


def _encode_string(value: str) -> bytes:
    _validate_string(value)
    return json.dumps(
        value,
        ensure_ascii=False,
        allow_nan=False,
        separators=(",", ":"),
    ).encode("utf-8")


def _encode(value: Any) -> bytes:
    if value is None:
        return b"null"
    if value is True:
        return b"true"
    if value is False:
        return b"false"
    if isinstance(value, int) and not isinstance(value, bool):
        return str(value).encode("ascii")
    if isinstance(value, str):
        return _encode_string(value)
    if isinstance(value, (list, tuple)):
        return b"[" + b",".join(_encode(item) for item in value) + b"]"
    if isinstance(value, Mapping):
        encoded_items: list[bytes] = []
        for key in sorted(value, key=_key_order):
            encoded_items.append(_encode_string(key) + b":" + _encode(value[key]))
        return b"{" + b",".join(encoded_items) + b"}"
    raise LicensingError(
        LicenseErrorCode.NON_CANONICAL_VALUE,
        f"unsupported JSON value type: {type(value).__name__}",
    )


def canonicalize_json(value: Any) -> bytes:
    """Return the RFC 8785 canonical byte representation of a bounded value."""

    _validate_tree(value)
    return _encode(value)


__all__ = ["canonicalize_json", "parse_bounded_json"]
