from typing import ClassVar, Optional

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

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


class ValidationError(Exception):
    pass


class ResponseCodeError(Exception):
    pass


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

    Parameters:
        host (str): The host address of the search server.
        port (int): The port number of the search server.
    """

    host: str = "127.0.0.1"
    port: str | int = "50051"

    _instances: ClassVar[dict] = {}

    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]

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

        Parameters:
            rpc_method_name (str): The name of the RPC method (e.g., `Comment`).
            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:
                response = await RPCMethod(request_message)

                # Clear the request message for future use if is not None
                if request_message is not None:
                    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.")


class RPCBase(RPCClient):
    """Base class for RPC operations, extending RPCClient for handling RPC calls."""

    @validate_call
    async def fetch_one(
        self, identifier: Optional[str | UUID4] = None, slug: Optional[str] = None
    ):
        """Fetch a single record from a database model."""
        request = self.fetch_one_request()
        if identifier:
            request.identifier = identifier  # Assign attributes
        if slug:
            request.slug = slug  # Assign attributes

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

    @validate_call
    async def fetch_one_for_search(
        self, identifier: Optional[str | UUID4] = None, slug: Optional[str] = None
    ):
        """Fetch a single record from a database model with invoker_unit set to `search`."""
        request = self.fetch_one_request(invoker_unit=InvokerUnits.SEARCH)
        if identifier:
            request.identifier = identifier  # Assign attributes
        if slug:
            request.slug = slug  # Assign attributes

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

    @validate_call
    async def fetch_all(self, order_by: str, page_size: int, page_number: int):
        """Fetch all records from a database model."""
        request = self.fetch_all_request(
            page_size=page_size, order_by=order_by, page_number=page_number
        )
        return await self.perform_operation(RPCMethodNames.FETCH_ALL, request)

    @validate_call
    async def fetch_all_for_sitemap(
        self, order_by: str, page_size: int, page_number: int
    ):
        """Fetch all records from a database model for sitemap use."""
        request = self.fetch_all_request(
            page_size=page_size,
            order_by=order_by,
            page_number=page_number,
            invoker_unit=InvokerUnits.PLACETAG_SITEMAP,
        )
        return await self.perform_operation(RPCMethodNames.FETCH_ALL, request)


class Blog(RPCBase):
    """`fetch_one()`, `featch_all()`."""

    controller_stub: ClassVar = ControllerStubs.BLOG
    fetch_one_request: ClassVar = RequestMessages.FETCH_BLOG
    fetch_all_request: ClassVar = RequestMessages.FETCH_ALL_BLOG


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

    controller_stub: ClassVar = ControllerStubs.CATEGORY
    fetch_one_request: ClassVar = RequestMessages.FETCH_CATEGORY

    @validate_call
    async def fetch_one(
        self, identifier: Optional[str | UUID4] = None, slug: Optional[str] = None
    ):
        """Fetch category data."""
        request = self.fetch_one_request()
        if identifier:
            request.identifier = identifier  # Assign attributes
        if slug:
            request.slug = slug  # Assign attributes

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