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 (
    CONTROLLER_STUB,
    SUCCESS_CODE,
    RequestMessages,
    RPCMethodNames,
)


class ValidationError(Exception):
    pass


class ResponseCodeError(Exception):
    pass


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

    Parameters:
        host (str): The host address of the search server.
        port (int | str): 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):
        """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.")

    @validate_call
    async def retrieve(self, token_identifier: str):
        """Retrieve discount using its `token_identifier`."""

        # Assign attributes
        RequestMessages.RETRIEVE.token_identifier = token_identifier

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

    @validate_call
    async def validate(
        self,
        token_identifier: str,
        user_identifier: Optional[str] = None,
    ):
        """Validate if a discount token is valid."""

        # Assign attributes
        RequestMessages.VALIDATE.token_identifier = token_identifier
        if user_identifier:
            RequestMessages.VALIDATE.user_identifier = user_identifier

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

    @validate_call
    async def calculate_by_token(
        self,
        amount: int,
        token_identifier: str,
        user_identifier: Optional[str] = None,
    ):
        """Calculate the discounted amount for provided token"""

        # Assign attributes
        RequestMessages.CALCULATE_BY_TOKEN.amount = amount
        RequestMessages.CALCULATE_BY_TOKEN.token_identifier = token_identifier
        if user_identifier:
            RequestMessages.CALCULATE_BY_TOKEN.user_identifier = user_identifier

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

    @validate_call
    async def calculate_by_item(
        self,
        amount: int,
        discount_type: str,
        item_identifier: str,
    ):
        """Calculate the lowest discounted amount for an item."""

        # Assign attributes
        RequestMessages.CALCULATE_BY_ITEM.amount = amount
        RequestMessages.CALCULATE_BY_ITEM.discount_type = discount_type
        RequestMessages.CALCULATE_BY_ITEM.item_identifier = item_identifier

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