from typing import ClassVar, Optional, Type, TypeVar
from uuid import UUID

from google.protobuf.json_format import MessageToDict
from google.protobuf.message import Message
from pydantic import UUID4, BaseModel, HttpUrl, validate_call

from . import config
from .api_constants import (
    SUCCESS_CODE,
    InvoiceRequestMessages,
    InvoiceRPCMethodNames,
    InvoiceStubs,
    PaymentRequestMessages,
    PaymentRPCMethodNames,
    PaymentStubs,
)
from .schemas import Gateway


class ValidationError(Exception):
    pass


class ResponseCodeError(Exception):
    pass


M = TypeVar("M", bound=BaseModel)


class RPCClient(BaseModel):
    _instances: ClassVar[dict] = {}
    stub: 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,
        stub_cls,
        request_message: Optional[Message] = None,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
        response_model: Optional[Type[M]] = 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(stub_cls(config.CHANNEL), rpc_method_name, None)
        if not RPCMethod:
            raise ValidationError("Invalid RPC method name.")

        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)

        response_dict = MessageToDict(response, preserving_proto_field_name=True)
        data = response_dict.get("data", [])

        if not response_model:
            return response_dict

        return [response_model.model_validate(item) for item in data]


class Base(RPCClient):
    @validate_call
    async def inquiry(
        self,
        tracking_code: str | UUID4,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        # Assign the tracking code to the request message
        request = self.inquiry_request(tracking_code=tracking_code)

        # Perform the Inquiry operation
        return await self.perform_rpc(
            rpc_method_name=self.inquiry_method_name,
            request_message=request,
            stub_cls=self.inquiry_stub,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )


class Invoice(Base):
    inquiry_request: ClassVar = InvoiceRequestMessages.INQUIRY
    inquiry_method_name: ClassVar = InvoiceRPCMethodNames.INQUIRY
    inquiry_stub: ClassVar = InvoiceStubs.INQUIRY

    @validate_call
    async def create(
        self,
        owner_identifier: str | UUID4,
        total_amount: int,
        final_amount: int,
        tax_percentage: Optional[float] = None,
        discount_amount: int | None = None,
        discounted_amount: int | None = None,
        description: Optional[str] = None,
        extra_data: dict | None = None,
        trade_identifier: str | UUID4 = None,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        # Assign attributes to the CREATE request message
        request = InvoiceRequestMessages.CREATE(
            owner_identifier=str(owner_identifier),
            total_amount=total_amount,
            final_amount=final_amount,
            trade_identifier=str(trade_identifier),
        )
        if extra_data:
            request.extra_data.update(extra_data)
        if tax_percentage is not None:
            request.tax_percentage = tax_percentage
        if discount_amount:
            request.discount_amount = discount_amount
        if discounted_amount:
            request.discounted_amount = discounted_amount
        if description:
            request.description = description

        # Perform Create operation
        return await self.perform_rpc(
            rpc_method_name=InvoiceRPCMethodNames.CREATE,
            request_message=request,
            stub_cls=InvoiceStubs.CREATE,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )

    @validate_call
    async def fetch_user_invoices(
        self,
        owner_identifier: str | UUID4,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """List the invoices of a user."""
        # Assign the tracking code to the request message
        request = InvoiceRequestMessages.FETCH_USER_INVOICES(
            owner_identifier=owner_identifier
        )

        # Perform the Inquiry operation
        return await self.perform_rpc(
            rpc_method_name=InvoiceRPCMethodNames.FETCH_USER_INSTANCES,
            request_message=request,
            stub_cls=InvoiceStubs.FETCH_USER_INVOICES,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )

    # 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,
        callback_url: HttpUrl,
        description: Optional[str] = None,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        request = PaymentRequestMessages.REQUEST(
            invoice=invoice_identifier, gateway=gateway_title
        )
        if description:
            request.description = description
        if callback_url:
            request.callback_url = str(callback_url)

        return await self.perform_rpc(
            rpc_method_name=PaymentRPCMethodNames.REQUEST,
            request_message=request,
            stub_cls=PaymentStubs.INVOICE,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )

    @validate_call
    async def verify_payment(
        self,
        gateway_status: str,
        payment_identifier: str,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """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
        request = PaymentRequestMessages.VERIFY(
            gateway_status=gateway_status, payment_identifier=payment_identifier
        )

        # Perform the payment operation using the perform_rpc method
        return await self.perform_rpc(
            rpc_method_name=PaymentRPCMethodNames.VERIFY,
            request_message=request,
            stub_cls=PaymentStubs.INVOICE,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )


class Payment(Base):
    inquiry_request: ClassVar = PaymentRequestMessages.INQUIRY
    inquiry_method_name: ClassVar = PaymentRPCMethodNames.INQUIRY
    inquiry_stub: ClassVar = PaymentStubs.INQUIRY

    @validate_call
    async def list_gateways(
        self,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """List available payment gateways."""
        return await self.perform_rpc(
            rpc_method_name=PaymentRPCMethodNames.LIST_GATEWAYS,
            stub_cls=PaymentStubs.INVOICE,
            response_model=Gateway,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )

    @validate_call
    async def request(
        self,
        invoice_identifier: str | UUID4,
        gateway_title: str,
        callback_url: HttpUrl,
        description: Optional[str] = None,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = 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
        request = PaymentRequestMessages.REQUEST(
            invoice=invoice_identifier, gateway=gateway_title
        )
        if description:
            request.description = description
        if callback_url:
            request.callback_url = str(callback_url)

        # Perform the payment operation using the perform_rpc method
        return await self.perform_rpc(
            rpc_method_name=PaymentRPCMethodNames.REQUEST,
            request_message=request,
            stub_cls=PaymentStubs.INVOICE,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )

    @validate_call
    async def verify(
        self,
        gateway_status: str,
        payment_identifier: str,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """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
        request = PaymentRequestMessages.VERIFY(
            gateway_status=gateway_status, payment_identifier=payment_identifier
        )
        # Perform the payment operation using the perform_rpc method
        return await self.perform_rpc(
            rpc_method_name=PaymentRPCMethodNames.VERIFY,
            request_message=request,
            stub_cls=PaymentStubs.INVOICE,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )
