from typing import ClassVar, List, Optional
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,
    ItemTypes,
    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,
        stub_cls,
        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:
            await config.construct_channel()

        RPCMethod = getattr(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 IndexedContent(RPCClient):
    @validate_call
    async def create(
        self,
        title: str,
        content: str,
        item_identifier: str | UUID4,
        item_type: ItemTypes,
        keywords: Optional[List[str]] = None,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """Create a new keyworded indexed content.
        Return:
            The search results in Darsman gRPC response format.
        """
        request = RequestMessages.CREATE(
            title=title,
            content=content,
            item_identifier=str(item_identifier),
            item_type=item_type.value,
        )
        if keywords:
            request.keywords.extend(keywords)

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

    async def create_blog(
        self,
        title: str,
        content: str,
        identifier: str | UUID4,
        keywords: Optional[List[str]] = None,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        return await self.create(
            title=title,
            content=content,
            item_identifier=identifier,
            item_type=ItemTypes.BLOG,
            keywords=keywords,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )

    async def create_course(
        self,
        title: str,
        content: str,
        identifier: str | UUID4,
        keywords: Optional[List[str]] = None,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        return await self.create(
            title=title,
            content=content,
            item_identifier=identifier,
            item_type=ItemTypes.COURSE,
            keywords=keywords,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )

    async def create_master_course(
        self,
        title: str,
        content: str,
        identifier: str | UUID4,
        keywords: Optional[List[str]] = None,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        return await self.create(
            title=title,
            content=content,
            item_identifier=identifier,
            item_type=ItemTypes.MASTER_COURSE,
            keywords=keywords,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )

    async def create_lesson(
        self,
        title: str,
        content: str,
        identifier: str | UUID4,
        keywords: Optional[List[str]] = None,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        return await self.create(
            title=title,
            content=content,
            item_identifier=identifier,
            item_type=ItemTypes.LESSON,
            keywords=keywords,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )

    @validate_call
    async def search(
        self,
        search_phrase: str,
        item_types: List[ItemTypes],
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """Search for a phrase across specified item types.

        Args:
            search_phrase (str): The phrase to search for.
            item_types (List[ItemTypes]): List of item types to filter the search.

        Return:
            The search results in Darsman gRPC response format.
        """
        request = RequestMessages.SEARCH(search_phrase=search_phrase)
        request.item_types.extend(item_types)

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

    async def search_blog(
        self,
        search_phrase: str,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        return await self.search(
            search_phrase=search_phrase,
            item_types=[ItemTypes.BLOG],
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )

    async def search_course_lesson(
        self,
        search_phrase: str,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        return await self.search(
            search_phrase=search_phrase,
            item_types=[
                ItemTypes.COURSE,
                ItemTypes.LESSON,
                ItemTypes.MASTER_COURSE,
            ],
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )
