"""
# Serializers.

Handle data validation.
"""
from __future__ import annotations

import datetime
from typing import TYPE_CHECKING, Any, Generic, Optional, TypeVar, Union

from fastapi import Request
from pydantic import BaseModel
from pydantic.fields import SHAPE_LIST, ModelField

from sap.beanie.document import Document, TDoc

from . import utils

# SerializerType = TypeVar("SerializerType", bound=BaseModel)

if TYPE_CHECKING:
    from pydantic.typing import AbstractSetIntStr, DictStrAny, MappingIntStrAny


class CursorInfo:
    """Contains information on how the list should paginated."""

    offset: int = 0
    limit: int = 10
    sort: str = "-doc_meta.created"

    def __init__(self, request: Request) -> None:
        """Initialize the cursor info."""
        cursor_str = request.query_params.get("cursor", "")
        try:
            limit, offset = utils.base64_url_decode(cursor_str).split(",")
        except ValueError:
            return
        self.limit, self.offset = int(limit), int(offset)

    def get_beanie_query_params(self) -> dict[str, Any[int, str]]:
        """Return params to apply to the database query when using beanie."""
        return {
            "limit": self.limit,
            "skip": self.offset,
            "sort": self.sort,
        }

    def get_next(self) -> Optional[str]:
        """Get the cursor to paginate forward."""
        offset = self.offset + self.limit
        return utils.base64_url_encode(f"{self.limit},{offset}")

    def get_previous(self) -> Optional[str]:
        """Get the cursor to paginate backward."""
        offset = self.offset - self.limit
        if offset <= 0:
            return None
        return utils.base64_url_encode(f"{self.limit},{offset}")


class ObjectSerializer(Generic[TDoc], BaseModel):
    """Serialize an object for retrieve or list."""

    @classmethod
    def get_id(cls, instance: TDoc) -> str:
        """Return the Mongo ID of the object."""
        return str(instance.id)

    @classmethod
    def get_created(cls, instance: TDoc) -> datetime.datetime:
        """Return the user creation date."""
        assert instance.doc_meta.created  # let mypy know that this cannot be null
        return instance.doc_meta.created

    @classmethod
    def get_updated(cls, instance: TDoc) -> datetime.datetime:
        """Return the user creation date."""
        assert instance.doc_meta.updated  # let mypy know that this cannot be null
        return instance.doc_meta.updated

    @classmethod
    def _get_instance_data(
        cls, instance: TDoc, exclude: Union["AbstractSetIntStr", "MappingIntStrAny"] = None
    ) -> dict[str, Any]:
        """Retrieve the serializer value from the instance and getters."""
        data = {}
        exclude = exclude or set()
        for field_name, field in cls.__fields__.items():
            if field_name in exclude:
                continue
            elif hasattr(cls, f"get_{field_name}"):
                data[field_name] = getattr(cls, f"get_{field_name}")(instance=instance)
            elif issubclass(field.type_, ObjectSerializer):
                related_object = getattr(instance, field_name, None)
                if field.shape == SHAPE_LIST:
                    data[field_name] = (
                        field.type_.read_list(related_object, exclude=field.field_info.exclude)
                        if related_object
                        else []
                    )
                else:
                    data[field_name] = (
                        field.type_.read(related_object, exclude=field.field_info.exclude) if related_object else None
                    )
            else:
                data[field_name] = getattr(instance, field_name)
        return data

    @classmethod
    def read(cls, instance: TDoc, exclude: Union["AbstractSetIntStr", "MappingIntStrAny"] = None) -> "SerializerType":
        """Serialize a single object instance."""
        return cls(**cls._get_instance_data(instance, exclude=exclude))

    @classmethod
    def read_list(
        cls, instance_list: list[TDoc], exclude: Union["AbstractSetIntStr", "MappingIntStrAny"] = None
    ) -> list["SerializerType"]:
        """Serialize a list of objects."""
        return [cls.read(instance, exclude=exclude) for instance in instance_list]

    @classmethod
    def read_page(
        cls,
        instance_list: list[TDoc],
        cursor_info: CursorInfo,
        request: Request,
    ) -> PaginatedData["SerializerType"]:
        """Serialize a list of objects."""

        # TODO: implemented proper cursor pagination, for now fake it till you make it.

        next = cursor_info.get_next()
        previous = cursor_info.get_previous()
        return PaginatedData(
            count=0,
            next=str(request.url.include_query_params(cursor=next)) if next else None,
            previous=str(request.url.include_query_params(cursor=previous)) if previous else None,
            data=cls.read_list(instance_list),
        )


SerializerType = TypeVar("SerializerType", bound=ObjectSerializer)


class PaginatedData(Generic[SerializerType], BaseModel):
    """Represent the structure of an API paginated list response."""

    object: str = "list"
    count: int
    next: Optional[str]
    previous: Optional[str]
    data: list[Any]


class WriteObjectSerializer(Generic[TDoc], BaseModel):
    """Serialize an object for create or update."""

    instance: Optional[TDoc] = None

    async def run_async_validators(self) -> None:
        """Check that data pass DB validation."""

        field: ModelField
        embedded_serializers = {}
        for field_name, field in self.__fields__.items():
            if issubclass(field.type_, WriteObjectSerializer):
                embedded_serializers[field_name] = field

        field_serializer: WriteObjectSerializer[TDoc]
        for field_name in embedded_serializers:
            if field_serializer := getattr(self, field_name):
                if self.instance:
                    field_serializer.instance = getattr(self.instance, field_name)
                await field_serializer.run_async_validators()

    def dict(
        self,
        *,
        include: Optional[Union["AbstractSetIntStr", "MappingIntStrAny"]] = None,
        exclude: Optional[Union["AbstractSetIntStr", "MappingIntStrAny"]] = None,
        by_alias: bool = False,
        skip_defaults: Optional[bool] = None,
        exclude_unset: bool = False,
        exclude_defaults: bool = False,
        exclude_none: bool = False,
    ) -> "DictStrAny":
        """Dump the serializer data with exclusion of unwanted fields."""
        # Exclude from dumping
        exclude = (exclude or {}) | {"instance": True}

        # Some fields are only excluded from being cascade dumps to dict,
        # but their original value is still needed
        exclude_doc_dumps = {}

        # Embedded documents need to be converted to object after dumps
        embedded_serializers = {}

        for field_name, field in self.__fields__.items():
            if field_name in exclude:
                continue

            if issubclass(field.type_, Document):
                exclude_doc_dumps[field_name] = True

            if issubclass(field.type_, WriteObjectSerializer):
                embedded_serializers[field_name] = field.type_.__fields__["instance"].type_

            # Some fields are excluded as they are only needed for create
            if field.field_info.extra.get("exclude_update") and self.instance:
                exclude[field_name] = True

            # Some fields are excluded as they are only needed for update
            if field.field_info.extra.get("exclude_create") and not self.instance:
                exclude[field_name] = True

        exclude = exclude | exclude_doc_dumps

        result = super().dict(
            include=include,
            exclude=exclude,
            by_alias=by_alias,
            skip_defaults=skip_defaults,
            exclude_unset=exclude_unset,
            exclude_defaults=exclude_defaults,
            exclude_none=exclude_none,
        )

        for field_name in exclude_doc_dumps:
            result[field_name] = getattr(self, field_name)

        instance_embedded: BaseModel
        for field_name, field_model in embedded_serializers.items():
            if not result[field_name]:
                continue
            if instance_embedded := getattr(self.instance, field_name, None):
                result[field_name] = instance_embedded.copy(update=result[field_name])
            else:
                result[field_name] = field_model(**result[field_name])

        return result

    async def create(self, **kwargs: Any) -> TDoc:
        """Create the object in the database using the data extracted by the serializer."""
        instance_class: type[TDoc] = self.__fields__["instance"].type_
        self.instance = await instance_class(**self.dict()).create()
        return self.instance

    async def update(self, **kwargs: Any) -> TDoc:
        """Update the object in the database using the data extracted by the serializer."""
        assert self.instance
        self.instance = self.instance.copy(update=self.dict())
        await self.instance.save()
        return self.instance
