"""Pydantic support for TypeID."""

import re
from typing import Any, cast
from uuid import UUID

from pydantic import GetCoreSchemaHandler, ValidationInfo, ValidatorFunctionWrapHandler, WrapSerializer, WrapValidator
from pydantic.annotated_handlers import GetJsonSchemaHandler
from pydantic.json_schema import JsonSchemaValue
from pydantic_core import core_schema
from pydantic_core.core_schema import SerializerFunctionWrapHandler

from .base32 import encode as encode_base32
from .const import DOMAIN_PATTERN, SUFFIX_PATTERN
from .typeid import TypeID

stable_random_uuid_index = 0


BASE_RANDOM_UUID = UUID("0193cede-6e79-70d7-99b6-4618ee5c0000")


# It's nice if the example UUID's are stable, because the openapi specification will then also be stable.
def _stable_random_uuid7() -> UUID:
    """Generate a stable random UUID to be used in examples."""

    global stable_random_uuid_index  # noqa: PLW0603 using global is OK for this purpose
    b = bytearray(BASE_RANDOM_UUID.bytes)
    b[len(b) - 1] += stable_random_uuid_index & 255
    b[len(b) - 2] += (stable_random_uuid_index >> 8) & 255
    res = UUID(bytes=bytes(b))
    stable_random_uuid_index += 1
    return res


def create_typeid_example(type: str, domain: str | None = None) -> str:
    """Create an example TypeID for use in documentation etc."""
    return f"{type}_{domain if domain else 'example.com'}_{encode_base32(cast('UUID', _stable_random_uuid7()).bytes)}"


class TypeIdType:
    """A field metadata class to indicate a specific allowed type in a TypeID."""

    type: str
    domain: str | None = None

    def __init__(self, type: str, domain: str | None = None):
        self.type = type
        self.domain = domain

    def __get_pydantic_json_schema__(
        self,
        core_schema: core_schema.CoreSchema,
        handler: GetJsonSchemaHandler,
    ) -> JsonSchemaValue:
        """Pydantic JSON Schema."""
        return {
            "type": "string",
            "pattern": f"{self.type}_{self.domain if self.domain else DOMAIN_PATTERN}_{SUFFIX_PATTERN}",
            "example": create_typeid_example(self.type, self.domain),
        }

    def __get_pydantic_core_schema__(self, source: Any, handler: GetCoreSchemaHandler) -> core_schema.CoreSchema:
        """Pydantic Core Schema."""
        str_schema: core_schema.StringSchema = {
            "type": "str",
            "pattern": f"{self.type}_{self.domain if self.domain else DOMAIN_PATTERN}_{SUFFIX_PATTERN}",
            "to_lower": True,
            "max_length": 90,
        }

        from_str_schema = core_schema.chain_schema(
            [str_schema, core_schema.no_info_plain_validator_function(TypeID.from_string)],
        )

        return core_schema.json_or_python_schema(
            json_schema=core_schema.union_schema(
                [core_schema.is_instance_schema(TypeID), from_str_schema],
                serialization=core_schema.plain_serializer_function_ser_schema(str, return_schema=str_schema),
            ),
            python_schema=core_schema.union_schema(
                [core_schema.is_instance_schema(TypeID), from_str_schema],
                serialization=core_schema.plain_serializer_function_ser_schema(str, return_schema=str_schema),
            ),
        )

    def __hash__(self) -> int:
        """Pydantic hash function."""
        return hash((self.type, self.domain))


UUID_PATTERN = re.compile(r"^[\da-f]{8}-([\da-f]{4}-){3}[\da-f]{12}$")


def typeid_validator(type: str, domain: str | None) -> WrapValidator:
    """Create a validator for a TypeID that converts a UUID to a TypeID with the given type and domain."""

    def _validate(tid: TypeID) -> TypeID:
        if tid.type != type:
            raise ValueError(f"TypeID {tid!r} does not match type {type!r}")
        if domain is not None and tid.domain != domain:
            raise ValueError(f"TypeID {tid!r} does not match domain {domain!r}")
        return tid

    def _convert_uuid_to_typeid(v: Any, _handler: ValidatorFunctionWrapHandler, _info: ValidationInfo):
        """Convert a UUID into a TypeID."""
        if isinstance(v, TypeID):
            return _validate(v)
        if isinstance(v, UUID):
            assert domain is not None
            return _validate(TypeID.from_uuid(v, type, domain))
        if isinstance(v, str) and bool(UUID_PATTERN.match(v.lower())):
            assert domain is not None
            return _validate(TypeID.from_uuid(UUID(v), type, domain))
        # Assume str
        return _validate(TypeID.from_string(v))

    return WrapValidator(_convert_uuid_to_typeid)


def _typeid_serializer(v: TypeID, nxt: SerializerFunctionWrapHandler) -> str:
    return f"{nxt(str(v))}"


TypeIDSerializer = WrapSerializer(_typeid_serializer, when_used="json")
