"""
Serialize and Deserialize in JSON format.
"""

from typing import Any, AnyStr, overload, Optional, Union, cast

from .compat import SerdeError, T
from .de import Deserializer, from_dict
from .se import Serializer, to_dict

# Lazy numpy imports to improve startup time

try:  # pragma: no cover
    import orjson

    def json_dumps(obj: Any, **opts: Any) -> str:
        if "option" in opts:
            opts["option"] |= orjson.OPT_NON_STR_KEYS
        else:
            opts["option"] = orjson.OPT_SERIALIZE_NUMPY | orjson.OPT_NON_STR_KEYS
        return cast(str, orjson.dumps(obj, **opts).decode())

    def json_loads(s: Union[str, bytes], **opts: Any) -> Any:
        return orjson.loads(s, **opts)

except ImportError:
    import json

    def json_dumps(obj: Any, **opts: Any) -> str:
        if "default" not in opts:
            from .numpy import encode_numpy

            opts["default"] = encode_numpy
        # compact output
        ensure_ascii = opts.pop("ensure_ascii", False)
        separators = opts.pop("separators", (",", ":"))
        return json.dumps(obj, ensure_ascii=ensure_ascii, separators=separators, **opts)

    def json_loads(s: Union[str, bytes], **opts: Any) -> Any:
        return json.loads(s, **opts)


__all__ = ["from_json", "to_json", "deserialize_json_numbers"]


class JsonSerializer(Serializer[str]):
    @classmethod
    def serialize(cls, obj: Any, **opts: Any) -> str:
        return json_dumps(obj, **opts)


class JsonDeserializer(Deserializer[AnyStr]):
    @classmethod
    def deserialize(cls, data: AnyStr, **opts: Any) -> Any:
        return json_loads(data, **opts)


def deserialize_json_numbers(value: Any) -> float:
    """
    Convert JSON numbers to float, accepting both ints and floats and rejecting non-numeric input.
    Useful when a JSON payload omits a decimal point but the target field is typed as float.
    """
    if isinstance(value, bool):
        raise SerdeError(f"Expected JSON number but got boolean {value!r}")
    if isinstance(value, (int, float)):
        return float(value)
    raise SerdeError(f"Expected JSON number but got {type(value).__name__}")


def to_json(
    obj: Any,
    cls: Optional[Any] = None,
    se: type[Serializer[str]] = JsonSerializer,
    reuse_instances: bool = False,
    convert_sets: bool = True,
    skip_none: bool = False,
    **opts: Any,
) -> str:
    """
    Serialize the object into JSON str. [orjson](https://github.com/ijl/orjson)
    will be used if installed.

    You can pass any serializable `obj`. If you supply other keyword arguments,
    they will be passed in `dumps` function.
    By default, numpy objects are serialized, this behaviour can be customized with the `option`
    argument with [orjson](https://github.com/ijl/orjson#numpy), or the `default` argument with
    Python standard json library.

    * `skip_none`: When set to True, any field in the class with a None value is excluded from the
    serialized output. Defaults to False.

    If you want to use another json package, you can subclass `JsonSerializer` and implement
    your own logic.
    """
    return se.serialize(
        to_dict(
            obj,
            c=cls,
            reuse_instances=reuse_instances,
            convert_sets=convert_sets,
            skip_none=skip_none,
        ),
        **opts,
    )


@overload
def from_json(
    c: type[T],
    s: AnyStr,
    de: type[Deserializer[AnyStr]] = JsonDeserializer,
    coerce_numbers: bool = True,
    **opts: Any,
) -> T: ...


# For Union, Optional etc.
@overload
def from_json(
    c: Any,
    s: AnyStr,
    de: type[Deserializer[AnyStr]] = JsonDeserializer,
    coerce_numbers: bool = True,
    **opts: Any,
) -> Any: ...


def from_json(
    c: Any,
    s: AnyStr,
    de: type[Deserializer[AnyStr]] = JsonDeserializer,
    coerce_numbers: bool = True,
    **opts: Any,
) -> Any:
    """
    Deserialize from JSON into the object. [orjson](https://github.com/ijl/orjson) will be used
    if installed.

    `c` is a class object and `s` is JSON bytes or str. If you supply other keyword arguments,
    they will be passed in `loads` function.

    * `coerce_numbers`: When True (default), ints from JSON are coerced to floats when the target
      type is float. Strings are never coerced.

    If you want to use another json package, you can subclass `JsonDeserializer` and implement your
    own logic.
    """
    deserialize_numbers = deserialize_json_numbers if coerce_numbers else None
    return from_dict(
        c,
        de.deserialize(s, **opts),
        reuse_instances=False,
        deserialize_numbers=deserialize_numbers,
    )
