import asyncio
from typing import ClassVar, List, Optional

from google.protobuf.json_format import MessageToDict
from grpc.aio import insecure_channel
from pydantic import (
    UUID4,
    BaseModel,
    StringConstraints,
    validate_call,
)
from typing_extensions import Annotated

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

IranianPhoneNumber = Annotated[
    str,
    StringConstraints(
        strip_whitespace=True, pattern=r"^(?:\+98|098|98|0098|)0?9\d{9}$"
    ),
]


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"
    api_key: str

    _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:
                metadata = [("api-key", self.api_key)]
                response = await RPCMethod(request_message, metadata=metadata)

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

    async def perform_bulk_operation(self, rpc_method_name, request_messages: List):
        """Perform bulk operation (multiple requests at once).

        Parameters:
            rpc_method_name (str): The name of the RPC method (e.g., `fetch_one`).
            request_messages (List[gRPC message]): A list of request message instances.
        """
        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)]

                # Send all requests concurrently
                responses = await asyncio.gather(
                    *[
                        RPCMethod(request, metadata=metadata)
                        for request in request_messages
                    ]
                )

                # Process responses
                response_dicts = []
                for response in responses:
                    if response.code != SUCCESS_CODE:
                        raise ResponseCodeError(response.code)
                    response_dicts.append(
                        MessageToDict(response, preserving_proto_field_name=True)
                    )

                return response_dicts
            else:
                raise ValidationError("Invalid RPC method name.")


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

    comment_request: ClassVar = RequestMessages.COMMENT

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

        return await self.perform_operation(
            RPCMethodNames.FETCH_ONE, self.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."""
        self.fetch_all_request.order_by = order_by
        self.fetch_all_request.page_size = page_size
        self.fetch_all_request.page_number = page_number

        return await self.perform_operation(
            RPCMethodNames.FETCH_ALL, self.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."""
        self.fetch_all_request.order_by = order_by
        self.fetch_all_request.page_size = page_size
        self.fetch_all_request.page_number = page_number
        self.fetch_all_request.invoker_unit = InvokerUnits.GATEWAY_SITEMAP

        return await self.perform_operation(
            RPCMethodNames.FETCH_ALL, self.fetch_all_request
        )

    @validate_call
    async def fetch_comments(
        self, identifier: Optional[str | UUID4] = None, slug: Optional[str] = None
    ):
        """Fetch a all `comments` of an item object."""
        # Assign attributes
        if identifier:
            RequestMessages.FETCH_COURSE_COMMENTS.identifier = identifier
        if slug:
            RequestMessages.FETCH_COURSE_COMMENTS.slug = slug

        if not isinstance(self, Package):
            return await self.perform_operation(
                RPCMethodNames.FETCH_COMMENTS, RequestMessages.FETCH_COURSE_COMMENTS
            )
        raise ValidationError("package has no comments.")

    @validate_call
    async def comment(
        self,
        name: str,
        content: str,
        owner_identifier: Optional[str | UUID4] = None,
        owner_slug: Optional[str] = None,
        user_identifier: Optional[str | UUID4] = None,
    ):
        """Comment on a course/lesson."""
        # Assign attributes
        self.comment_request.name = name
        self.comment_request.content = content
        # Since these are optional don't set if None
        if owner_identifier:
            self.comment_request.owner_identifier = owner_identifier
        if owner_slug:
            self.comment_request.owner_slug = owner_slug
        if user_identifier:
            self.comment_request.user_identifier = user_identifier

        if not isinstance(self, Package):
            return await self.perform_operation(
                RPCMethodNames.COMMENT,
                self.comment_request,
            )
        raise ValidationError("Cannot comment on a package.")


class CourseBase(RPCBase):
    """`fetch_rates()`, `fetch_comments()`."""

    fetch_one_request: ClassVar = RequestMessages.FETCH_COURSE
    fetch_all_request: ClassVar = RequestMessages.FETCH_ALL_COURSE

    @validate_call
    async def fetch_rates(
        self, identifier: Optional[str | UUID4] = None, slug: Optional[str] = None
    ):
        """Fetch a all `rates` of a course object."""
        # Assign attributes
        if identifier:
            RequestMessages.FETCH_COURSE_RATES.identifier = identifier
        if slug:
            RequestMessages.FETCH_COURSE_RATES.slug = slug

        return await self.perform_operation(
            RPCMethodNames.FETCH_RATES, RequestMessages.FETCH_COURSE_RATES
        )


class Course(CourseBase):
    """`fetch_one()`, `featch_all()`, `comment()`, `like()`, `unlike()`, `fetch_chapters()`, `fetch_rates()`, `fetch_comments()`."""

    controller_stub: ClassVar = ControllerStubs.COURSE

    @validate_call
    async def fetch_normals(self, order_by: str, page_size: int, page_number: int):
        """Fetch all normal courses."""
        self.fetch_all_request.order_by = order_by
        self.fetch_all_request.page_size = page_size
        self.fetch_all_request.page_number = page_number
        self.fetch_all_request.item_type = "course"

        return await self.perform_operation(
            RPCMethodNames.FETCH_ALL, self.fetch_all_request
        )

    @validate_call
    async def fetch_roadmaps(self, order_by: str, page_size: int, page_number: int):
        """Fetch all roadmap courses."""
        self.fetch_all_request.order_by = order_by
        self.fetch_all_request.page_size = page_size
        self.fetch_all_request.page_number = page_number
        self.fetch_all_request.item_type = "roadmap"

        return await self.perform_operation(
            RPCMethodNames.FETCH_ALL, self.fetch_all_request
        )

    @validate_call
    async def like(
        self,
        course_identifier: Optional[str | UUID4] = None,
        course_slug: Optional[str] = None,
    ):
        # Since these are optional don't set if None
        if course_identifier:
            RequestMessages.LIKE.owner_identifier = course_identifier
        if course_slug:
            RequestMessages.LIKE.owner_slug = course_slug

        return await self.perform_operation(RPCMethodNames.LIKE, RequestMessages.LIKE)

    @validate_call
    async def unlike(
        self,
        course_identifier: Optional[str | UUID4] = None,
        course_slug: Optional[str] = None,
    ):
        # Since these are optional don't set if None
        if course_identifier:
            RequestMessages.UNLIKE.owner_identifier = course_identifier
        if course_slug:
            RequestMessages.UNLIKE.owner_slug = course_slug

        return await self.perform_operation(
            RPCMethodNames.UNLIKE, RequestMessages.UNLIKE
        )

    @validate_call
    async def fetch_chapters(
        self,
        course_identifier: Optional[str | UUID4] = None,
        course_slug: Optional[str] = None,
    ):
        # Since these are optional don't set if None
        if course_identifier:
            RequestMessages.FETCH_COURSE_CHAPTERS.identifier = course_identifier
        if course_slug:
            RequestMessages.FETCH_COURSE_CHAPTERS.slug = course_slug

        return await self.perform_operation(
            RPCMethodNames.FETCH_COURSE_CHAPTERS, RequestMessages.FETCH_COURSE_CHAPTERS
        )

    @validate_call
    async def rate(
        self,
        user_identifier: str | UUID4,
        rate: float,
        course_identifier: Optional[str | UUID4] = None,
        course_slug: Optional[str] = None,
    ):
        # Assign attributes
        RequestMessages.RATE.user_identifier = user_identifier
        RequestMessages.RATE.rate = rate
        # Since these are optional don't set if None
        if course_identifier:
            RequestMessages.RATE.owner_identifier = course_identifier
        if course_slug:
            RequestMessages.RATE.owner_slug = course_slug

        return await self.perform_operation(RPCMethodNames.RATE, RequestMessages.RATE)


class MasterCourse(Course):
    """`fetch_one()`, `featch_all()`, `comment()`, `like()`, `unlike()`, `fetch_rates()`, `fetch_comments()`."""

    controller_stub: ClassVar = ControllerStubs.MASTER_COURSE


class Lesson(RPCBase):
    """`fetch_one()`, `featch_all()`, `comment()`."""

    controller_stub: ClassVar = ControllerStubs.LESSON
    fetch_one_request: ClassVar = RequestMessages.FETCH_LESSON
    fetch_all_request: ClassVar = RequestMessages.FETCH_ALL_LESSON


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

    controller_stub: ClassVar = ControllerStubs.PAKCAGE
    fetch_one_request: ClassVar = RequestMessages.FETCH_PACKAGE
    fetch_all_request: ClassVar = RequestMessages.FETCH_ALL_PACKAGE


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

    controller_stub: ClassVar = ControllerStubs.CATEGORY

    async def hierarchical_fetch_all(self):
        """Fetch all records in a hierarchical structure."""
        return await self.perform_operation(RPCMethodNames.HIERARCHICAL_FETCH_ALL)

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

        return await self.perform_operation(
            RPCMethodNames.FETCH_ONE, RequestMessages.FETCH_CATEGORY
        )


class TopBanner(RPCClient):
    """`hierarchical_fetch_all()`."""

    controller_stub: ClassVar = ControllerStubs.TOP_BANNER

    @validate_call
    async def fetch_current(self):
        """Fetch the current top banner object."""
        return await self.perform_operation(RPCMethodNames.FETCH_CURRENT)


class General(RPCClient):
    """`fetch_one()`, `fetch_bulk()`."""

    controller_stub: ClassVar = ControllerStubs.GENERAL

    @validate_call
    async def fetch_one_for_asset(self, identifier: Optional[str | UUID4]):
        """Fetch item by it's `identifeir` regardless of its type."""
        # Assign attributes
        RequestMessages.FETCH_ITEM.identifier = identifier
        RequestMessages.FETCH_ITEM.invoker_unit = InvokerUnits.ACCOUNT_ASSET

        return await self.perform_operation(
            RPCMethodNames.FETCH_ONE, RequestMessages.FETCH_ITEM
        )

    @validate_call
    async def fetch_one_for_basket(self, identifier: Optional[str | UUID4]):
        """Fetch item by it's `identifeir` regardless of its type."""
        # Assign attributes
        RequestMessages.FETCH_ITEM.identifier = identifier
        RequestMessages.FETCH_ITEM.invoker_unit = InvokerUnits.CORE_BASKET

        return await self.perform_operation(
            RPCMethodNames.FETCH_ONE, RequestMessages.FETCH_ITEM
        )

    @validate_call
    async def fetch_one_for_asset_manager(self, identifier: Optional[str | UUID4]):
        """Fetch item by it's `identifeir` regardless of its type."""
        # Assign attributes
        RequestMessages.FETCH_ITEM.identifier = identifier
        RequestMessages.FETCH_ITEM.invoker_unit = InvokerUnits.CORE_ASSET_MANAGEMENT

        return await self.perform_operation(
            RPCMethodNames.FETCH_ONE, RequestMessages.FETCH_ITEM
        )

    @validate_call
    async def fetch_bulk_for_asset(self, identifiers: List[Optional[str | UUID4]]):
        """Fetch multiple items by their identifiers."""
        # Prepare a list of request messages for all identifiers
        request_messages = [
            RequestMessages.FETCH_BULK_ITEM(
                identifier=identifier, invoker_unit=InvokerUnits.ACCOUNT_ASSET
            )
            for identifier in identifiers
        ]

        return await self.perform_bulk_operation(
            RPCMethodNames.FETCH_ONE, request_messages
        )

    @validate_call
    async def fetch_bulk_for_basket(self, identifiers: List[Optional[str | UUID4]]):
        """Fetch multiple items by their identifiers."""
        # Prepare a list of request messages for all identifiers
        request_messages = [
            RequestMessages.FETCH_BULK_ITEM(
                identifier=identifier, invoker_unit=InvokerUnits.CORE_BASKET
            )
            for identifier in identifiers
        ]

        return await self.perform_bulk_operation(
            RPCMethodNames.FETCH_ONE, request_messages
        )

    @validate_call
    async def fetch_bulk_for_invoice(self, identifiers: List[Optional[str | UUID4]]):
        """Fetch multiple items by their identifiers for use in invoice inquiry."""
        # Prepare a list of request messages for all identifiers
        request_messages = [
            RequestMessages.FETCH_BULK_ITEM(
                identifier=identifier, invoker_unit=InvokerUnits.INVOICE_INQUIRY
            )
            for identifier in identifiers
        ]

        return await self.perform_bulk_operation(
            RPCMethodNames.FETCH_ONE, request_messages
        )


class Consultant(RPCClient):
    """`create()`."""

    controller_stub: ClassVar = ControllerStubs.CONSULTANT

    @validate_call
    async def create(
        self,
        phone_number: IranianPhoneNumber,
        full_name: str,
        item_type: Optional[str] = None,
        item_identifier: Optional[str | UUID4] = None,
    ):
        """create a new Consultant request."""
        # Assign attributes
        RequestMessages.CREATE_CONSULTANT_REQUEST.phone_number = phone_number
        RequestMessages.CREATE_CONSULTANT_REQUEST.full_name = full_name
        if item_type:
            RequestMessages.CREATE_CONSULTANT_REQUEST.item_type = item_type
        if item_identifier:
            RequestMessages.CREATE_CONSULTANT_REQUEST.item_identifier = item_identifier

        return await self.perform_operation(
            RPCMethodNames.CREATE, RequestMessages.CREATE_CONSULTANT_REQUEST
        )
