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,
    BasketRequestMessages,
    BasketRPCMethodNames,
    ControllerStubs,
    TradeRequestMessages,
    TradeRPCMethodNames,
)


class ValidationError(Exception):
    pass


class ResponseCodeError(Exception):
    pass


class Core(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"

    __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, controller_stub
    ):
        """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:
            controller_stub.channel = channel
            controller_stub.ready()
            RPCMethod = getattr(controller_stub, rpc_method_name, None)

            if RPCMethod:
                response = await RPCMethod(request_message)

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


class Basket(Core):
    @validate_call
    async def create(
        self,
        owner_identifier: Optional[str] = None,
        basket_identifier: Optional[str] = None,
    ):
        """Create a new basket instance."""
        # Assign attributes to the request message
        if owner_identifier:
            BasketRequestMessages.CREATE.owner_identifier = owner_identifier
        if basket_identifier:
            BasketRequestMessages.CREATE.basket_identifier = basket_identifier

        # Perform the create operation
        return await self.perform_operation(
            BasketRPCMethodNames.CREATE,  # The name of the RPC method
            BasketRequestMessages.CREATE,  # The request message
            ControllerStubs.BASKET,  # The controller stub
        )

    @validate_call
    async def retrieve(self, basket_identifier: str):
        """Retrieve a basket instance."""
        # Assign attributes to the request message
        BasketRequestMessages.RTRIEVE.basket_identifier = basket_identifier

        # Perform the create operation
        return await self.perform_operation(
            BasketRPCMethodNames.RTRIEVE,  # The name of the RPC method
            BasketRequestMessages.RTRIEVE,  # The request message
            ControllerStubs.BASKET,  # The controller stub
        )

    @validate_call
    async def add_item(
        self,
        item_identifier: str,
        basket_identifier: str,
        extra_data: Optional[dict] = None,
    ):
        """Add item to an existing basket."""
        # Set the keyword attribute in the request message
        BasketRequestMessages.ADD_ITEM.item_identifier = item_identifier
        BasketRequestMessages.ADD_ITEM.basket_identifier = basket_identifier
        if extra_data:
            BasketRequestMessages.ADD_ITEM.extra_data.update(extra_data)

        # Perform the search operation
        return await self.perform_operation(
            BasketRPCMethodNames.ADD_ITEM,  # The name of the RPC method for searching
            BasketRequestMessages.ADD_ITEM,  # The request message for searching
            ControllerStubs.BASKET,  # The controller stub for searching
        )

    @validate_call
    async def remove_item(
        self,
        item_identifier: str,
        basket_identifier: str,
        extra_data: Optional[dict] = None,
    ):
        """Remove an item from an existing basket."""
        # Set the keyword attribute in the request message
        BasketRequestMessages.REMOVE_ITEM.item_identifier = item_identifier
        BasketRequestMessages.REMOVE_ITEM.basket_identifier = basket_identifier
        if extra_data:
            BasketRequestMessages.REMOVE_ITEM.extra_data.update(extra_data)

        # Perform the search operation
        return await self.perform_operation(
            BasketRPCMethodNames.REMOVE_ITEM,  # The name of the RPC method for searching
            BasketRequestMessages.REMOVE_ITEM,  # The request message for searching
            ControllerStubs.BASKET,  # The controller stub for searching
        )

    @validate_call
    async def assign_owner(self, owner_identifier: str, basket_identifier: str):
        """Assign the specified owner to the specified basket."""
        # Set the keyword attribute in the request message
        BasketRequestMessages.ASSIGN_OWNER.owner_identifier = owner_identifier
        BasketRequestMessages.ASSIGN_OWNER.basket = basket_identifier

        # Perform the search operation
        return await self.perform_operation(
            BasketRPCMethodNames.ASSIGN_OWNER,  # The name of the RPC method for searching
            BasketRequestMessages.ASSIGN_OWNER,  # The request message for searching
            ControllerStubs.BASKET,  # The controller stub for searching
        )

    @validate_call
    async def apply_discount(self, basket_identifier: str, discount_token: str):
        """Assign the specified owner to the specified basket."""
        # Set the keyword attribute in the request message
        BasketRequestMessages.APPLY_DISCOUNT.discount_token = discount_token
        BasketRequestMessages.APPLY_DISCOUNT.basket = basket_identifier

        # Perform the search operation
        return await self.perform_operation(
            BasketRPCMethodNames.APPLY_DISCOUNT,  # The name of the RPC method for searching
            BasketRequestMessages.APPLY_DISCOUNT,  # The request message for searching
            ControllerStubs.BASKET,  # The controller stub for searching
        )


class Trade(Core):
    @validate_call
    async def start(self, basket_identifier: str):
        """Start the trade process (paying a basket)."""

        # Set the keyword attribute in the START request message
        TradeRequestMessages.START.basket = basket_identifier

        # call rpc method
        return await self.perform_operation(
            TradeRPCMethodNames.START, TradeRequestMessages.START, ControllerStubs.TRADE
        )

    @validate_call
    async def end(
        self,
        gateway_status: str,
        payment_identifier: str,
        extra_data: Optional[dict] = None,
    ):
        """End the trade process (verify payment)."""

        # Set the keyword attribute in the END request message
        TradeRequestMessages.END.gateway_status = gateway_status
        TradeRequestMessages.END.payment_identifier = payment_identifier
        if extra_data:
            TradeRequestMessages.END.extra_data = extra_data

        # call rpc method
        return await self.perform_operation(
            TradeRPCMethodNames.END, TradeRequestMessages.END, ControllerStubs.TRADE
        )
