from typing import ClassVar, Optional

from google.protobuf.json_format import MessageToDict
from grpc.aio import insecure_channel
from pydantic import BaseModel, validate_call

from .api_constants import (
    SUCCESS_CODE,
    ControllerStubs,
    Entities,
    RequestMessages,
    RPCMethodNames,
)


class ValidationError(Exception):
    pass


class ResponseCodeError(Exception):
    pass


class Base(BaseModel):
    """
    Configure the host and port of the search server.

    Parameters:
        host (str): The host address of the search server.
        port (int | str): The port number of the search server.
        api_key (str): The api_key of the search server.
        place (str): The place you want to makes operations about.
        tag (str): The tag of the place you want to work with.
    """

    host: str = "127.0.0.1"
    port: str | int = "50051"
    api_key: str
    place: Optional[str] = None
    tag: Optional[str] = None

    __instance: ClassVar = None

    def __new__(cls, *args, **kwargs):
        if cls.__instance is None:
            cls.__instance = super().__new__(cls)
        return cls.__instance

    async def perform_operation(self, rpc_method_name, request_message):
        """Perform the operation.

        Parameters:
            rpc_method_name (str): The name of the RPC method (e.g., `Create`).
            request_message (gRPC message): The request message instance.
        """
        async with insecure_channel(f"{self.host}:{self.port}") as channel:
            self.controller_stub.channel = channel
            self.controller_stub.ready()
            RPCMethod = getattr(self.controller_stub, rpc_method_name, None)

            if RPCMethod:
                metadata = [("api-key", self.api_key)]
                response = await RPCMethod(request_message, metadata=metadata)

                # Clear the request message for future use
                request_message.Clear()

                # 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.")

    @validate_call
    async def search(
        self,
        place: Optional[str] = None,
        tag: Optional[str] = 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 self.place, tag or self.tag

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

        return await self.perform_operation(RPCMethodNames.SEARCH, request)


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

    controller_stub: ClassVar = ControllerStubs.MEDIA

    @validate_call
    async def create(
        self,
        media_type: str,
        media_identifier: str,
        place: Optional[str] = None,
        tag: Optional[str] = None,
    ):
        # Get place, tag from self if aren't provided here
        place, tag = place or self.place, tag or 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_operation(
            RPCMethodNames.CREATE, RequestMessages.ADD_MEDIA
        )


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

    controller_stub: ClassVar = ControllerStubs.CONTENT

    @validate_call
    async def create(
        self,
        header: str,
        content: str,
        order: Optional[int] = None,
        place: Optional[str] = None,
        tag: Optional[str] = None,
    ):
        # Get place, tag from self if aren't provided here
        place, tag = place or self.place, tag or 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_operation(
            RPCMethodNames.CREATE, RequestMessages.ADD_CONTENT
        )


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

    controller_stub: ClassVar = ControllerStubs.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,
    ):
        # Get place, tag from self if aren't provided here
        place, tag = place or self.place, tag or 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_operation(
            RPCMethodNames.CREATE, RequestMessages.ADD_SEO
        )

    @validate_call
    async def get_blogs_sitemap(self):
        request = RequestMessages.SITEMAP(entity=Entities.BLOG)
        return await self.perform_operation(RPCMethodNames.GET_SITEMAP, request)

    @validate_call
    async def get_courses_sitemap(self):
        request = RequestMessages.SITEMAP(entity=Entities.COURSE)
        return await self.perform_operation(RPCMethodNames.GET_SITEMAP, request)

    @validate_call
    async def get_lessons_sitemap(self):
        request = RequestMessages.SITEMAP(entity=Entities.LESSON)
        return await self.perform_operation(RPCMethodNames.GET_SITEMAP, request)

    @validate_call
    async def get_master_courses_sitemap(self):
        request = RequestMessages.SITEMAP(entity=Entities.MASTER_COURSE)
        return await self.perform_operation(RPCMethodNames.GET_SITEMAP, request)
