"""Internal utilities for working with Pydantic types and data."""

from __future__ import annotations

import json
from contextlib import suppress
from functools import lru_cache
from typing import TYPE_CHECKING

import pydantic
from packaging.version import Version
from pydantic import ValidationError

if TYPE_CHECKING:
    from typing import Any, Final

    from pydantic import BaseModel

IS_PYDANTIC_V2: Final[bool] = Version(pydantic.VERSION).major >= 2


@lru_cache
def gql_typename(cls: type[BaseModel]) -> str:
    """Get the GraphQL typename for a Pydantic model."""
    if (field := cls.model_fields.get("typename__")) and (typename := field.default):
        return typename
    raise TypeError(f"Cannot extract GraphQL typename from: {cls.__qualname__!r}.")


if IS_PYDANTIC_V2:
    import pydantic_core  # pydantic_core is only installed by pydantic v2

    def from_json(s: str | bytes) -> Any:
        """Quickly deserialize a JSON string to a Python object."""
        return pydantic_core.from_json(s)

    def to_json(v: Any) -> str:
        """Quickly serialize a (possibly Pydantic) object to a JSON string."""
        return pydantic_core.to_json(v, by_alias=True, round_trip=True).decode("utf-8")

    def pydantic_isinstance(
        v: Any, classinfo: type[BaseModel] | tuple[type[BaseModel], ...]
    ) -> bool:
        """Return True if the object could be parsed into the given Pydantic type.

        This is like a more lenient version of `isinstance()` for use with Pydantic.
        In Pydantic v2, should be fast since the underlying implementation is in Rust,
        and it may be preferable over `try:...except ValidationError:...`.

        See: https://docs.pydantic.dev/latest/api/pydantic_core/#pydantic_core.SchemaValidator.isinstance_python
        """
        if isinstance(classinfo, tuple):
            return any(
                cls.__pydantic_validator__.isinstance_python(v) for cls in classinfo
            )
        cls = classinfo
        return cls.__pydantic_validator__.isinstance_python(v)

else:
    # Pydantic v1 fallback implementations.
    # These may be noticeably slower, but their primary goal is to ensure
    # compatibility with Pydantic v1 so long as we need to support it.

    from pydantic.json import pydantic_encoder  # Only valid in pydantic v1

    def from_json(s: str | bytes) -> Any:
        return json.loads(s)

    def to_json(v: Any) -> str:
        return json.dumps(v, default=pydantic_encoder)

    def pydantic_isinstance(
        v: Any, classinfo: type[BaseModel] | tuple[type[BaseModel], ...]
    ) -> bool:
        classes = classinfo if isinstance(classinfo, tuple) else (classinfo,)
        for cls in classes:
            with suppress(ValidationError):
                cls.model_validate(v)
                return True
        return False
