import asyncio
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,
    StringConstraints,
    validate_call,
)
from typing_extensions import Annotated

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

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):
    _instances: ClassVar[dict] = {}
    stub_cls: 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:
            await 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.")

    @validate_call(config=dict(arbitrary_types_allowed=True))
    async def perform_bulk_rpc(
        self,
        rpc_method_name: str,
        request_messages: List[Message],
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """Perform the bulk remote-precedure-call.

        Parameters:
            rpc_method_name: The name of the RPC method (e.g., `Retrieve`).
            request_message: The request message instances for each call.
            timeout: RPC timeout of each call.
            wait_for_ready: Wait for Channel to be ready in case is not.
        """
        if config.CHANNEL is None:
            await 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)))
            # Send all requests concurrently
            responses = await asyncio.gather(
                *[
                    RPCMethod(
                        request,
                        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,
                    )
                    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."""

    fetch_one_request: ClassVar
    fetch_bulk_request: ClassVar
    fetch_all_request: ClassVar

    @validate_call
    async def fetch_one(
        self,
        identifier: Optional[str | UUID4] = None,
        slug: Optional[str] = None,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = 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_rpc(
            rpc_method_name=RPCMethodNames.FETCH_ONE,
            request_message=request,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )

    @validate_call
    async def fetch_bulk(
        self,
        identifiers: List[Optional[str | UUID4]],
        invoker_unit: InvokerUnits,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        request = self.fetch_bulk_request(
            identifier=[str(i) for i in identifiers], invoker_unit=invoker_unit
        )

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

    @validate_call
    async def fetch_one_for_search(
        self,
        identifier: Optional[str | UUID4] = None,
        slug: Optional[str] = None,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = 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_rpc(
            rpc_method_name=RPCMethodNames.FETCH_ONE,
            request_message=request,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )

    @validate_call
    async def fetch_all(
        self,
        order_by: str,
        page_size: int,
        page_number: int,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """Fetch all records from a database model."""
        request = self.fetch_all_request(
            order_by=order_by, page_size=page_size, page_number=page_number
        )

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

    @validate_call
    async def fetch_all_for_sitemap(
        self,
        order_by: str,
        page_size: int,
        page_number: int,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """Fetch all records from a database model for sitemap use."""
        request = self.fetch_all_request(
            order_by=order_by,
            page_size=page_size,
            page_number=page_number,
            invoker_unit=InvokerUnits.PLACETAG_SITEMAP,
        )

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

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

        if not isinstance(self, Package):
            return await self.perform_rpc(
                rpc_method_name=RPCMethodNames.FETCH_COMMENTS,
                request_message=request,
                timeout=timeout,
                wait_for_ready=wait_for_ready,
            )
        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,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """Comment on a course/lesson."""
        # Assign attributes
        request = RequestMessages.COMMENT(name=name, content=content)

        # Since these are optional don't set if None
        if owner_identifier:
            request.owner_identifier = str(owner_identifier)
        if owner_slug:
            request.owner_slug = owner_slug
        if user_identifier:
            request.user_identifier = str(user_identifier)

        if not isinstance(self, Package):
            return await self.perform_rpc(
                rpc_method_name=RPCMethodNames.COMMENT,
                request_message=request,
                request_id=request_id,
                timeout=timeout,
                wait_for_ready=wait_for_ready,
            )
        raise ValidationError("Cannot comment on a package.")


class CourseBase(_RPCBase):
    """API: `fetch_one()` `fetch_bulk()` `fetch_one_for_search()` `fetch_all()`
    `fetch_all_for_sitemap()` `fetch_comments()` `comment()`, `fetch_rates()`."""

    fetch_one_request: ClassVar = RequestMessages.FETCH_COURSE
    fetch_bulk_request: ClassVar = RequestMessages.FETCH_BULK_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,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """Fetch all `rates` of a course object."""
        # Assign attributes
        request = RequestMessages.FETCH_COURSE_RATES()
        if identifier:
            request.identifier = str(identifier)
        if slug:
            request.slug = slug

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


class Course(CourseBase):
    """API: `fetch_one()` `fetch_bulk()` `fetch_one_for_search()` `fetch_all()`
    `fetch_all_for_sitemap()` `fetch_comments()` `comment()`, `fetch_rates()`
    `fetch_normals()` `fetch_roadmaps()` `like()` `unlike()` `rate()`.
    """

    stub_cls: ClassVar = Stubs.COURSE

    @validate_call
    async def fetch_normals(
        self,
        order_by: str,
        page_size: int,
        page_number: int,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """Fetch all normal courses."""
        request = self.fetch_all_request(
            order_by=order_by,
            page_size=page_size,
            page_number=page_number,
            item_type="course",
        )
        return await self.perform_rpc(
            rpc_method_name=RPCMethodNames.FETCH_ALL,
            request_message=request,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )

    @validate_call
    async def fetch_roadmaps(
        self,
        order_by: str,
        page_size: int,
        page_number: int,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """Fetch all roadmap courses."""
        request = self.fetch_all_request(
            order_by=order_by,
            page_size=page_size,
            page_number=page_number,
            item_type="roadmap",
        )
        return await self.perform_rpc(
            rpc_method_name=RPCMethodNames.FETCH_ALL,
            request_message=request,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )

    @validate_call
    async def like(
        self,
        course_identifier: Optional[str | UUID4] = None,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
        course_slug: Optional[str] = None,
    ):
        # Since these are optional don't set if None
        request = RequestMessages.LIKE()
        if course_identifier:
            request.owner_identifier = str(course_identifier)
        if course_slug:
            request.owner_slug = course_slug
        return await self.perform_rpc(
            rpc_method_name=RPCMethodNames.LIKE,
            request_message=request,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )

    @validate_call
    async def unlike(
        self,
        course_identifier: Optional[str | UUID4] = None,
        course_slug: Optional[str] = None,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        # Since these are optional don't set if None
        request = RequestMessages.UNLIKE()
        if course_identifier:
            request.owner_identifier = str(course_identifier)
        if course_slug:
            request.owner_slug = course_slug
        return await self.perform_rpc(
            rpc_method_name=RPCMethodNames.UNLIKE,
            request_message=request,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )

    @validate_call
    async def rate(
        self,
        user_identifier: str | UUID4,
        rate: float,
        course_identifier: Optional[str | UUID4] = None,
        course_slug: Optional[str] = None,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        # Assign attributes
        request = RequestMessages.RATE(user_identifier=str(user_identifier), rate=rate)

        # Since these are optional don't set if None
        if course_identifier:
            request.owner_identifier = str(course_identifier)
        if course_slug:
            request.owner_slug = course_slug

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


class ExpertCourse(Course):
    """API: `fetch_one()` `fetch_bulk()` `fetch_one_for_search()` `fetch_all()`
    `fetch_all_for_sitemap()` `fetch_comments()` `comment()`, `fetch_rates()`
    `fetch_normals()` `fetch_roadmaps()` `like()` `unlike()` `rate()`.
    """

    stub_cls: ClassVar = Stubs.EXPERT_COURSE


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

    stub_cls: ClassVar = Stubs.MASTER_COURSE

    @validate_call
    async def fetch_state(
        self,
        identifier: Optional[str | UUID4],
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """Fetch `state` info of a master course object."""
        # Assign attributes
        request = RequestMessages.FETCH_COURSE_STATE(identifier=str(identifier))
        return await self.perform_rpc(
            rpc_method_name=RPCMethodNames.FETCH_STATE,
            request_message=request,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )

    @validate_call
    async def fetch_installment_term(
        self,
        identifier: Optional[str | UUID4],
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """Fetch `installment_term` info of a master course object."""
        # Assign attributes
        request = RequestMessages.FETCH_COURSE_INSTALLMENT_TERM(
            identifier=str(identifier)
        )
        return await self.perform_rpc(
            rpc_method_name=RPCMethodNames.FETCH_INSTALLMENT_TERM,
            request_message=request,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )

    @validate_call
    async def create_notification(
        self,
        identifier: Optional[str | UUID4],
        first_name: str,
        last_name: str,
        phone_number: IranianPhoneNumber,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """create a new notification for a master course object."""
        # Assign attributes
        request = RequestMessages.CREATE_NOTIFICATION(
            phone_number=phone_number,
            first_name=first_name,
            last_name=last_name,
            master_course=str(identifier),
        )
        return await self.perform_rpc(
            rpc_method_name=RPCMethodNames.CREATE_NOTIFICATION,
            request_message=request,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )

    @validate_call
    async def create_pre_registration(
        self,
        identifier: Optional[str | UUID4],
        user_identifier: str,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """create a new pre_registration for a master course object."""
        # Assign attributes
        request = RequestMessages.CREATE_PRE_REGISTRATION(
            master_course=str(identifier), user_identifier=user_identifier
        )
        return await self.perform_rpc(
            rpc_method_name=RPCMethodNames.CREATE_PRE_REGISTRATION,
            request_message=request,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )


class Lesson(_RPCBase):
    """Configure the host and port of the course_lesson server.

    Parameters:
        host (str): The host address of the course_lesson server.
        port (int): The port number of the course_lesson server.
        api_key (str): The api_key of the course_lesson server.

    Methods: `fetch_one()`, `featch_all()`, `comment()`."""

    stub_cls: ClassVar = Stubs.LESSON
    fetch_one_request: ClassVar = RequestMessages.FETCH_LESSON
    fetch_bulk_request: ClassVar = RequestMessages.FETCH_BULK_LESSON
    fetch_all_request: ClassVar = RequestMessages.FETCH_ALL_LESSON


class Package(_RPCBase):
    """Configure the host and port of the course_lesson server.

    Parameters:
        host (str): The host address of the course_lesson server.
        port (int): The port number of the course_lesson server.
        api_key (str): The api_key of the course_lesson server.

    Methods: `fetch_one()`, `featch_all()`."""

    stub_cls: ClassVar = Stubs.PAKCAGE
    fetch_one_request: ClassVar = RequestMessages.FETCH_PACKAGE
    fetch_all_request: ClassVar = RequestMessages.FETCH_ALL_PACKAGE


class Category(RPCClient):
    """Configure the host and port of the course_lesson server.

    Parameters:
        host (str): The host address of the course_lesson server.
        port (int): The port number of the course_lesson server.
        api_key (str): The api_key of the course_lesson server.

    Methods: `hierarchical_fetch_all()`."""

    stub_cls: ClassVar = Stubs.CATEGORY

    async def hierarchical_fetch_all(
        self,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """Fetch all records in a hierarchical structure."""
        return await self.perform_rpc(
            rpc_method_name=RPCMethodNames.HIERARCHICAL_FETCH_ALL,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )

    @validate_call
    async def fetch_one(
        self,
        identifier: Optional[str | UUID4] = None,
        slug: Optional[str] = None,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """Fetch a single record from a database model."""
        request = RequestMessages.FETCH_CATEGORY()
        if identifier:
            request.identifier = str(identifier)
        if slug:
            request.slug = slug
        return await self.perform_rpc(
            rpc_method_name=RPCMethodNames.FETCH_ONE,
            request_message=request,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )


class TopBanner(RPCClient):
    """Configure the host and port of the course_lesson server.

    Parameters:
        host (str): The host address of the course_lesson server.
        port (int): The port number of the course_lesson server.
        api_key (str): The api_key of the course_lesson server.

    Methods: `fetch_current()`."""

    stub_cls: ClassVar = Stubs.TOP_BANNER

    @validate_call
    async def fetch_current(
        self,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """Fetch the current top banner object."""
        return await self.perform_rpc(
            rpc_method_name=RPCMethodNames.FETCH_CURRENT,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )


class General(RPCClient):
    """Configure the host and port of the course_lesson server.

    Parameters:
        host (str): The host address of the course_lesson server.
        port (int): The port number of the course_lesson server.
        api_key (str): The api_key of the course_lesson server.

    Methods: `fetch_one_for_asset()`, `fetch_one_for_basket()`, `fetch_one_for_asset_manager()`,
    `fetch_bulk_for_asset()`, `fetch_bulk_for_basket()`, `fetch_bulk_for_invoice()`."""

    stub_cls: ClassVar = Stubs.GENERAL

    @validate_call
    async def fetch_one_for_asset(
        self,
        identifier: Optional[str | UUID4],
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """Fetch item by it's `identifeir` regardless of its type."""
        # Assign attributes
        request = RequestMessages.FETCH_ITEM(
            identifier=str(identifier), invoker_unit=InvokerUnits.ACCOUNT_ASSET
        )
        return await self.perform_rpc(
            rpc_method_name=RPCMethodNames.FETCH_ONE,
            request_message=request,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )

    @validate_call
    async def fetch_one_for_basket(
        self,
        identifier: Optional[str | UUID4],
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """Fetch item by it's `identifeir` regardless of its type."""
        # Assign attributes
        request = RequestMessages.FETCH_ITEM(
            identifier=str(identifier), invoker_unit=InvokerUnits.CORE_BASKET
        )
        return await self.perform_rpc(
            rpc_method_name=RPCMethodNames.FETCH_ONE,
            request_message=request,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )

    @validate_call
    async def fetch_one_for_asset_manager(
        self,
        identifier: Optional[str | UUID4],
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """Fetch item by it's `identifeir` regardless of its type."""
        # Assign attributes
        request = RequestMessages.FETCH_ITEM(
            identifier=str(identifier), invoker_unit=InvokerUnits.CORE_ASSET_MANAGEMENT
        )
        return await self.perform_rpc(
            rpc_method_name=RPCMethodNames.FETCH_ONE,
            request_message=request,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )

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

        return await self.perform_bulk_rpc(
            rpc_method_name=RPCMethodNames.FETCH_ONE,
            request_messages=request_messages,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )

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

        return await self.perform_bulk_rpc(
            rpc_method_name=RPCMethodNames.FETCH_ONE,
            request_messages=request_messages,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )

    @validate_call
    async def fetch_bulk_for_invoice(
        self,
        identifiers: List[Optional[str | UUID4]],
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """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=str(identifier), invoker_unit=InvokerUnits.INVOICE_INQUIRY
            )
            for identifier in identifiers
        ]

        return await self.perform_bulk_rpc(
            rpc_method_name=RPCMethodNames.FETCH_ONE,
            request_messages=request_messages,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )


class Consultant(RPCClient):
    """Configure the host and port of the course_lesson server.

    Parameters:
        host (str): The host address of the course_lesson server.
        port (int): The port number of the course_lesson server.
        api_key (str): The api_key of the course_lesson server.

    Methods: `create()`."""

    stub_cls: ClassVar = Stubs.CONSULTANT

    @validate_call
    async def create(
        self,
        phone_number: IranianPhoneNumber,
        full_name: Optional[str],
        item_type: Optional[str] = None,
        item_identifier: Optional[str | UUID4] = None,
        preferred_contact_time: Optional[str] = None,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """create a new Consultant request."""
        # Assign attributes
        request = RequestMessages.CREATE_CONSULTANT_REQUEST(
            phone_number=phone_number, full_name=full_name
        )
        if item_type:
            request.item_type = item_type
        if item_identifier:
            request.item_identifier = str(item_identifier)
        if preferred_contact_time:
            request.preferred_contact_time = preferred_contact_time
        return await self.perform_rpc(
            rpc_method_name=RPCMethodNames.CREATE,
            request_message=request,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )
