from typing import ClassVar

from google.protobuf.json_format import MessageToDict
from grpc.aio import insecure_channel
from pydantic import UUID4, BaseModel, validate_call

from .api_constants import (
    SUCCESS_CODE,
    InvoiceControllerStubs,
    InvoiceRequestMessages,
    InvoiceRPCMethodNames,
    PaymentControllerStubs,
    PaymentRequestMessages,
    PaymentRPCMethodNames,
)


class ValidationError(Exception):
    pass


class ResponseCodeError(Exception):
    pass


class Base(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

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

                # 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 inquiry(self, tracking_code: str | UUID4):
        """
        Inquiry an invoice/payment using `tracking_code`.

        Args:
            tracking_code (str): The tracking code of the invoice/payment.

        Return:
            Essentialy the invoice/payment info from the response from the server.
        """
        # Assign the tracking code to the request message
        self.inquiry_request.tracking_code = tracking_code

        # Perform the Inquiry operation
        return await self.perform_operation(
            self.inquiry_method_name,  # RPC method name
            self.inquiry_request,  # Request message
            self.inquiry_controller_stub,  # Controller stub
        )


class Invoice(Base):
    inquiry_request: ClassVar = InvoiceRequestMessages.INQUIRY
    inquiry_method_name: ClassVar = InvoiceRPCMethodNames.INQUIRY
    inquiry_controller_stub: ClassVar = InvoiceControllerStubs.INQUIRY

    @validate_call
    async def create(
        self,
        owner_identifier: str | UUID4,
        amount: int,
        discount_amount: int | None = None,
        description: str | None = None,
        extra_data: dict | None = None,
        basket_identifier: str | UUID4 = None,
    ):
        """
        Create an invoice.

        Args:
            owner_identifier: The identifier of the owner.
            amount: The amount of the invoice.
            discount_amount: The discount_amount of the invoice.
            description: The description about the invoice.
            extra_data: The extra data of the invoice.
            basket_identifier: The basket_identifier invoice is being created for.

        Return:
            Message containing success or the error code.
        """
        # Assign attributes to the CREATE request message
        InvoiceRequestMessages.CREATE.owner_identifier = str(owner_identifier)
        InvoiceRequestMessages.CREATE.amount = amount
        if extra_data:
            InvoiceRequestMessages.CREATE.extra_data = extra_data
        if basket_identifier:
            InvoiceRequestMessages.CREATE.basket_identifier = str(basket_identifier)
        if discount_amount:
            InvoiceRequestMessages.CREATE.discount_amount = discount_amount
        if description:
            InvoiceRequestMessages.CREATE.description = description

        # Perform Create operation
        # The `perform_operation` method handles the RPC call
        return await self.perform_operation(
            InvoiceRPCMethodNames.CREATE,  # The RPC method name
            InvoiceRequestMessages.CREATE,  # The request message
            InvoiceControllerStubs.CREATE,  # The controller stub
        )

    @validate_call
    async def fetch_user_invoices(self, owner_identifier: str | UUID4):
        """
        List the invoices of a user.

        Return:
            Essentialy the user invoices list info from the response from the server.
        """
        # Assign the tracking code to the request message
        InvoiceRequestMessages.FETCH_USER_INVOICES.owner_identifier = owner_identifier

        # Perform the Inquiry operation
        return await self.perform_operation(
            InvoiceRPCMethodNames.FETCH_USER_INSTANCES,  # RPC method name
            InvoiceRequestMessages.FETCH_USER_INVOICES,  # Request message
            InvoiceControllerStubs.FETCH_USER_INVOICES,  # Controller stub
        )

    # These two methods moved to Payment class but also kept here for backward compatibility
    @validate_call
    async def payment_request(
        self,
        invoice_identifier: str | UUID4,
        gateway_title: str,
        description: str | None = None,
    ):
        """Pay an invoice.

        Args:
            invoice_identifier (str): The `identifier` of the invoice.
            gateway_title (str): The gateway `title` used for the payment.

            Optionals:
                description (str): The description of the payment.

        Return:
            Essentialy the `payment_url` from the response from the server.
        """
        # Assign attributes to the request message
        PaymentRequestMessages.REQUEST.invoice = invoice_identifier
        PaymentRequestMessages.REQUEST.gateway = gateway_title
        if description:
            PaymentRequestMessages.REQUEST.description = description

        # Perform the payment operation using the perform_operation method
        return await self.perform_operation(
            PaymentRPCMethodNames.REQUEST,  # The RPC method name
            PaymentRequestMessages.REQUEST,  # The request message
            PaymentControllerStubs.INVOICE,  # The controller stub
        )

    @validate_call
    async def verify_payment(self, gateway_status: str, payment_identifier: str):
        """Verify a payment.

        Args:
            gateway_status (str): The status payment gateway claims for the payment.
            payment_identifier (str): The tracking_code of the payment.
        """
        # Assign attributes to the request message
        PaymentRequestMessages.VERIFY.gateway_status = gateway_status
        PaymentRequestMessages.VERIFY.payment_identifier = payment_identifier

        # Perform the payment operation using the perform_operation method
        return await self.perform_operation(
            PaymentRPCMethodNames.VERIFY,  # The RPC method name
            PaymentRequestMessages.VERIFY,  # The request message
            PaymentControllerStubs.INVOICE,  # The controller stub
        )


class Payment(Base):
    inquiry_request: ClassVar = PaymentRequestMessages.INQUIRY
    inquiry_method_name: ClassVar = PaymentRPCMethodNames.INQUIRY
    inquiry_controller_stub: ClassVar = PaymentControllerStubs.INQUIRY

    @validate_call
    async def request(
        self,
        invoice_identifier: str | UUID4,
        gateway_title: str,
        description: str | None = None,
    ):
        """Send a payemnt request.

        Args:
            invoice_identifier (str): The `identifier` of the invoice to pay.
            gateway_title (str): The gateway `title` used for the payment.

            Optionals:
                description (str): The description of the payment.

        Return:
            Essentialy the `payment_url` from the response from the server.
        """
        # Assign attributes to the request message
        PaymentRequestMessages.REQUEST.invoice = invoice_identifier
        PaymentRequestMessages.REQUEST.gateway = gateway_title
        if description:
            PaymentRequestMessages.REQUEST.description = description

        # Perform the payment operation using the perform_operation method
        return await self.perform_operation(
            PaymentRPCMethodNames.REQUEST,  # The RPC method name
            PaymentRequestMessages.REQUEST,  # The request message
            PaymentControllerStubs.INVOICE,  # The controller stub
        )

    @validate_call
    async def verify(self, gateway_status: str, payment_identifier: str):
        """Verify a payment.

        Args:
            gateway_status (str): The status payment gateway claims for the payment.
            payment_identifier (str): The tracking_code of the payment.
        """
        # Assign attributes to the request message
        PaymentRequestMessages.VERIFY.gateway_status = gateway_status
        PaymentRequestMessages.VERIFY.payment_identifier = payment_identifier

        # Perform the payment operation using the perform_operation method
        return await self.perform_operation(
            PaymentRPCMethodNames.VERIFY,  # The RPC method name
            PaymentRequestMessages.VERIFY,  # The request message
            PaymentControllerStubs.INVOICE,  # The controller stub
        )
