from typing import ClassVar, Optional, Type, TypeVar
from uuid import UUID

from google.protobuf.json_format import MessageToDict
from google.protobuf.message import Message
from pydantic import UUID4, BaseModel, validate_call

from . import config
from .api_constants import (
    SUCCESS_CODE,
    InvokerUnits,
    RequestMessages,
    RPCMethodNames,
    Stubs,
)
from .schemas import BlogList as BlogListSchema
from .schemas import BlogRetrieve as BlogRetrieveSchema
from .schemas import BlogSearch as BlogSearchSchema
from .schemas import BlogSitemap as BlogSitemapSchema
from .schemas import CategoryList as CategoryListSchema
from .schemas import Comment as CommentSchema
from .schemas import Rate as RateSchema


class ValidationError(Exception):
    pass


class ResponseCodeError(Exception):
    pass


M = TypeVar("M", bound=BaseModel)


class RPCClient(BaseModel):
    _instances: ClassVar[dict] = {}
    stub: ClassVar

    def __new__(cls, *args, **kwargs):
        # Ensure that each subclass has its own singleton instance
        if cls not in cls._instances:
            cls._instances[cls] = super().__new__(cls)
        return cls._instances[cls]

    @validate_call(config=dict(arbitrary_types_allowed=True))
    async def perform_rpc(
        self,
        rpc_method_name: str,
        request_message: Optional[Message] = None,
        response_model: Optional[Type[M]] = None,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """Perform the remote-precedure-call.

        Parameters:
            rpc_method_name: The name of the RPC method (e.g., `Retrieve`).
            request_message: The request message instance.
            timeout: RPC timeout.
            wait_for_ready: Wait for Channel to be ready in case is not.
        """
        if config.CHANNEL is None:
            await config.construct_channel()

        RPCMethod = getattr(self.stub_cls(config.CHANNEL), rpc_method_name, None)

        if RPCMethod:
            metadata = [("api-key", config.grpc_settings.API_KEY)]
            if request_id:
                metadata.append(("x-request-id", str(request_id)))
            response = await RPCMethod(
                request_message,
                metadata=metadata,
                timeout=config.grpc_settings.TIMEOUT if timeout is None else timeout,
                wait_for_ready=config.grpc_settings.WAIT_FOR_READY
                if wait_for_ready is None
                else wait_for_ready,
            )

            # Raise error if response is not successful
            if response.code != SUCCESS_CODE:
                raise ResponseCodeError(response.code)

            response_dict = MessageToDict(response, preserving_proto_field_name=True)
            data = response_dict.get("data", [])

            if not response_model:
                return response_dict

            return [response_model.model_validate(item) for item in data]
        else:
            raise ValidationError("Invalid RPC method name.")


class Blog(RPCClient):
    """`fetch_one()`, `fetch_all()`, `fetch_comments()`, `comment()`,
    `fetch_rates()`, `rate()`."""

    stub_cls: ClassVar = Stubs.BLOG

    @validate_call
    async def fetch_one(
        self,
        identifier: Optional[str | UUID4] = None,
        slug: Optional[str] = None,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """Fetch a single record from a database model."""
        request = RequestMessages.FETCH_BLOG()
        if identifier:
            request.identifier = identifier
        if slug:
            request.slug = slug

        return await self.perform_rpc(
            rpc_method_name=RPCMethodNames.FETCH_ONE,
            request_message=request,
            response_model=BlogRetrieveSchema,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )

    @validate_call
    async def fetch_one_for_search(
        self,
        identifier: Optional[str | UUID4] = None,
        slug: Optional[str] = None,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """Fetch a single record from a database model with invoker_unit set to `search`."""
        request = RequestMessages.FETCH_BLOG(invoker_unit=InvokerUnits.SEARCH)
        if identifier:
            request.identifier = identifier
        if slug:
            request.slug = slug

        return await self.perform_rpc(
            rpc_method_name=RPCMethodNames.FETCH_ONE,
            request_message=request,
            response_model=BlogSearchSchema,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )

    @validate_call
    async def fetch_all(
        self,
        order_by: str,
        page_size: int,
        page_number: int,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """Fetch all records from a database model."""
        request = RequestMessages.FETCH_ALL_BLOG(
            page_size=page_size, order_by=order_by, page_number=page_number
        )
        return await self.perform_rpc(
            rpc_method_name=RPCMethodNames.FETCH_ALL,
            request_message=request,
            response_model=BlogListSchema,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )

    @validate_call
    async def fetch_all_for_sitemap(
        self,
        order_by: str,
        page_size: int,
        page_number: int,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """Fetch all records from a database model for sitemap use."""
        request = RequestMessages.FETCH_ALL_BLOG(
            page_size=page_size,
            order_by=order_by,
            page_number=page_number,
            invoker_unit=InvokerUnits.PLACETAG_SITEMAP,
        )
        return await self.perform_rpc(
            rpc_method_name=RPCMethodNames.FETCH_ALL,
            request_message=request,
            response_model=BlogSitemapSchema,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )

    @validate_call
    async def fetch_author_blogs(
        self,
        author_identifier: str | UUID4,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """Fetch all blogs written by a specific author."""
        request = RequestMessages.FETCH_AUTHOR_BLOGS(
            author_identifier=str(author_identifier)
        )
        return await self.perform_rpc(
            rpc_method_name=RPCMethodNames.FETCH_AUTHOR_BLOGS,
            request_message=request,
            response_model=BlogListSchema,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )

    @validate_call
    async def fetch_comments(
        self,
        identifier: Optional[str | UUID4] = None,
        slug: Optional[str] = None,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """Fetch all `comments` of a blog item."""
        request = RequestMessages.FETCH_BLOG_COMMENTS()
        if identifier:
            request.identifier = str(identifier)
        if slug:
            request.slug = slug

        return await self.perform_rpc(
            rpc_method_name=RPCMethodNames.FETCH_COMMENTS,
            request_message=request,
            response_model=CommentSchema,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )

    @validate_call
    async def comment(
        self,
        name: str,
        content: str,
        user_identifier: str | UUID4,
        owner_identifier: Optional[str | UUID4] = None,
        owner_slug: Optional[str] = None,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """Comment on a blog item."""
        request = RequestMessages.COMMENT(
            name=name, content=content, user_identifier=str(user_identifier)
        )
        if owner_identifier:
            request.owner_identifier = str(owner_identifier)
        if owner_slug:
            request.owner_slug = owner_slug

        return await self.perform_rpc(
            rpc_method_name=RPCMethodNames.COMMENT,
            request_message=request,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )

    @validate_call
    async def fetch_rates(
        self,
        identifier: Optional[str | UUID4] = None,
        slug: Optional[str] = None,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """Fetch all `rates` of a blog item."""
        request = RequestMessages.FETCH_BLOG_RATES()
        if identifier:
            request.identifier = str(identifier)
        if slug:
            request.slug = slug

        return await self.perform_rpc(
            rpc_method_name=RPCMethodNames.FETCH_RATES,
            request_message=request,
            response_model=RateSchema,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )

    @validate_call
    async def rate(
        self,
        user_identifier: str | UUID4,
        rate: float,
        owner_identifier: Optional[str | UUID4] = None,
        owner_slug: Optional[str] = None,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """Rate a blog item."""
        request = RequestMessages.RATE(user_identifier=str(user_identifier), rate=rate)
        if owner_identifier:
            request.owner_identifier = str(owner_identifier)
        if owner_slug:
            request.owner_slug = owner_slug

        return await self.perform_rpc(
            rpc_method_name=RPCMethodNames.RATE,
            request_message=request,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )


class Category(RPCClient):
    """`fetch_one()`, `fetch_all()`."""

    stub_cls: ClassVar = Stubs.CATEGORY

    @validate_call
    async def fetch_all(
        self,
        order_by: str,
        page_size: int,
        page_number: int,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """Fetch all categories."""
        request = RequestMessages.FETCH_ALL_CATEGORY(
            page_size=page_size, order_by=order_by, page_number=page_number
        )

        return await self.perform_rpc(
            rpc_method_name=RPCMethodNames.FETCH_ALL,
            request_message=request,
            response_model=CategoryListSchema,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )

    @validate_call
    async def fetch_one(
        self,
        identifier: Optional[str | UUID4] = None,
        slug: Optional[str] = None,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """Fetch category data."""
        request = RequestMessages.FETCH_CATEGORY()
        if identifier:
            request.identifier = identifier
        if slug:
            request.slug = slug

        return await self.perform_rpc(
            rpc_method_name=RPCMethodNames.FETCH_ONE,
            request_message=request,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )
