from typing import ClassVar, Optional
from uuid import UUID

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

from . import config
from .api_constants import (
    SUCCESS_CODE,
    Entities,
    RequestMessages,
    RPCMethodNames,
    Stubs,
)


class ValidationError(Exception):
    pass


class ResponseCodeError(Exception):
    pass


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,
        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:
            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)

            return MessageToDict(response, preserving_proto_field_name=True)
        else:
            raise ValidationError("Invalid RPC method name.")


class Base(RPCClient):
    place: Optional[str] = None
    tag: Optional[str] = None

    @validate_call
    async def search(
        self,
        place: Optional[str] = None,
        tag: Optional[str] = None,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """Search for records with provided `place` and `tag` from a database model."""
        # Get place, tag from self if aren't provided here
        place, tag = place or str(self.place), tag or str(self.tag)

        # Assign attributes
        request = RequestMessages.SEARCH(place=place, tag=tag)

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


class Media(Base):
    """`search()`, `create()`."""

    stub_cls: ClassVar = Stubs.MEDIA

    @validate_call
    async def create(
        self,
        media_type: str,
        media_identifier: str,
        place: Optional[str] = None,
        tag: Optional[str] = None,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        # Get place, tag from self if aren't provided here
        place, tag = place or str(self.place), tag or str(self.tag)

        # Assign attributes
        RequestMessages.ADD_MEDIA.media_type = media_type
        RequestMessages.ADD_MEDIA.media_type = media_identifier
        RequestMessages.ADD_MEDIA.media_type = place
        RequestMessages.ADD_MEDIA.media_type = tag

        return await self.perform_rpc(
            rpc_method_name=RPCMethodNames.CREATE,
            request_message=RequestMessages.ADD_MEDIA,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )


class Content(Base):
    """`search()`, `create()`."""

    stub_cls: ClassVar = Stubs.CONTENT

    @validate_call
    async def create(
        self,
        header: str,
        content: str,
        order: Optional[int] = None,
        place: Optional[str] = None,
        tag: Optional[str] = None,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        # Get place, tag from self if aren't provided here
        place, tag = place or str(self.place), tag or str(self.tag)

        # Assign attributes
        if order:
            RequestMessages.ADD_MEDIA.order = order
        RequestMessages.ADD_MEDIA.header = header
        RequestMessages.ADD_MEDIA.content = content
        RequestMessages.ADD_MEDIA.place = place
        RequestMessages.ADD_MEDIA.media_type = tag

        return await self.perform_rpc(
            rpc_method_name=RPCMethodNames.CREATE,
            request_message=RequestMessages.ADD_CONTENT,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )


class Seo(Base):
    """`search()`, `create()`."""

    stub_cls: ClassVar = Stubs.SEO

    @validate_call
    async def create(
        self,
        seo_title: str,
        url: str,
        canonical_tag: str,
        meta_description: str,
        no_index: bool,
        no_follow: bool,
        og_image_media_identifier: str,
        og_image_width: int,
        og_image_height: int,
        place: Optional[str] = None,
        tag: Optional[str] = None,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        # Get place, tag from self if aren't provided here
        place, tag = place or str(self.place), tag or str(self.tag)

        # Assign attributes
        RequestMessages.ADD_SEO.seo_title = seo_title
        RequestMessages.ADD_SEO.url = url
        RequestMessages.ADD_SEO.canonical_tag = canonical_tag
        RequestMessages.ADD_SEO.meta_description = meta_description
        RequestMessages.ADD_SEO.visit_page = og_image_media_identifier
        RequestMessages.ADD_SEO.register_user_identifier = og_image_width
        RequestMessages.ADD_SEO.seo_page_type = og_image_height
        RequestMessages.ADD_SEO.no_follow = no_follow
        RequestMessages.ADD_SEO.no_index = no_index
        RequestMessages.ADD_SEO.place = place
        RequestMessages.ADD_SEO.tag = tag

        return await self.perform_rpc(
            rpc_method_name=RPCMethodNames.CREATE,
            request_message=RequestMessages.ADD_SEO,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )

    @validate_call
    async def get_blogs_sitemap(
        self,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        request = RequestMessages.SITEMAP(entity=Entities.BLOG)
        return await self.perform_rpc(
            rpc_method_name=RPCMethodNames.GET_SITEMAP,
            request_message=request,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )

    @validate_call
    async def get_courses_sitemap(
        self,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        request = RequestMessages.SITEMAP(entity=Entities.COURSE)
        return await self.perform_rpc(
            rpc_method_name=RPCMethodNames.GET_SITEMAP,
            request_message=request,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )

    @validate_call
    async def get_lessons_sitemap(
        self,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        request = RequestMessages.SITEMAP(entity=Entities.LESSON)
        return await self.perform_rpc(
            rpc_method_name=RPCMethodNames.GET_SITEMAP,
            request_message=request,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )

    @validate_call
    async def get_master_courses_sitemap(
        self,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        request = RequestMessages.SITEMAP(entity=Entities.MASTER_COURSE)
        return await self.perform_rpc(
            rpc_method_name=RPCMethodNames.GET_SITEMAP,
            request_message=request,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )
