import logging
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 google.protobuf.struct_pb2 import Struct
from pydantic import (
    BaseModel,
    EmailStr,
    HttpUrl,
    StringConstraints,
    validate_call,
)
from typing_extensions import Annotated

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

logger = logging.getLogger(__name__)

IranianPhoneNumber = Annotated[
    str,
    StringConstraints(
        strip_whitespace=True, pattern=r"^(?:\+98|098|98|0098|)0?9\d{9}$"
    ),
]


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


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,
        response_model: Optional[Type[M]] = None,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ) -> list[M] | dict:
        """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 not RPCMethod:
            raise ValidationError("Invalid RPC method name.")

        metadata = None
        if request_id:
            metadata = [("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 data

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


class Notify(RPCClient):
    stub_cls = Stubs.MESSAGE
    sender_unit: str

    @staticmethod
    def determine_send_type(recipient):
        """Determine the send_type based on recipient."""
        # Check if recipient is an email or phone number
        if "@" in recipient:
            send_type = NotificationTypes.EMAIL_NOTIFICATION
        else:
            send_type = NotificationTypes.SMS_NOTIFICATION

        return send_type

    @validate_call
    async def send_message(
        self,
        *,
        data: dict,
        recipient: IranianPhoneNumber | EmailStr,
        extra_data: str,
        pro_type: str,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """Send a particular message containing `data` to the recipient.

        If the `recipient` is a valid email/mobile number, an email/sms will be sent."""

        data_struct = Struct()
        data_struct.update(data)

        # Determine the send type based on the recipient
        request = RequestMessages.SEND_MESSAGE(
            sender_unit=self.sender_unit,
            recipient=recipient,
            extra_data=extra_data,
            pro_type=pro_type,
            send_type=self.determine_send_type(recipient),
            data=data_struct,
        )

        # Perform send
        return await self.perform_rpc(
            rpc_method_name=RPCMethodNames.SEND_MESSAGE,
            request_message=request,
            request_id=request_id,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )

    @validate_call
    async def send_login(self, *, otp: str, recipient: IranianPhoneNumber | EmailStr):
        logger.debug(f"sending login otp to {recipient}")
        data = {"otp": otp}
        return await self.send_message(
            data=data,
            recipient=recipient,
            extra_data="Sending Login OTP To User",
            pro_type=EventProTypes.LOGIN,
        )

    @validate_call
    async def send_register(
        self, *, otp: str, recipient: IranianPhoneNumber | EmailStr
    ):
        logger.debug(f"sending register otp to {recipient}")
        data = {"otp": otp}
        return await self.send_message(
            data=data,
            recipient=recipient,
            extra_data="Sending Register OTP To User",
            pro_type=EventProTypes.REGISTER,
        )

    @validate_call
    async def send_welcome(
        self,
        *,
        recipient: IranianPhoneNumber | EmailStr,
        user_name: str,
        discount_code: str,
        discount_percentage: int,
    ):
        logger.debug(f"sending welcome notify to {recipient}")
        data = {
            "user": user_name,
            "discount_code": discount_code,
            "discount_percent": discount_percentage,
        }
        return await self.send_message(
            data=data,
            recipient=recipient,
            extra_data="Sending Welcome Notify to User",
            pro_type=EventProTypes.WELCOME,
        )

    @validate_call
    async def send_discount_1mo_after_reg(
        self,
        *,
        recipient: IranianPhoneNumber | EmailStr,
        user_name: str,
        discount_code: str,
        validity_duration: str,
    ):
        logger.debug(f"sending discount 1 month after registration to {recipient}")
        data = {
            "user": user_name,
            "discount_code": discount_code,
            "validity_duration": validity_duration,
        }
        return await self.send_message(
            data=data,
            recipient=recipient,
            extra_data="Sending Discount 1 month after registration to User",
            pro_type=EventProTypes.DISCOUNT_1MO_AFTER_REG,
        )

    @validate_call
    async def send_hbd(
        self,
        *,
        recipient: IranianPhoneNumber | EmailStr,
        user_name: str,
        discount_code: str,
        discount_percentage: int,
    ):
        logger.debug(f"sending happy birthday to {recipient}")
        data = {
            "user": user_name,
            "discount_code": discount_code,
            "discount_percent": discount_percentage,
        }
        return await self.send_message(
            data=data,
            recipient=recipient,
            extra_data="Sending Happy birthday to User",
            pro_type=EventProTypes.HAPPY_BIRTHDAY,
        )

    @validate_call
    async def send_cert_submit(
        self, *, recipient: IranianPhoneNumber | EmailStr, user_name: str, date: str
    ):
        logger.debug(f"sending certificate submit to {recipient}")
        data = {"user": user_name, "date": date}
        return await self.send_message(
            data=data,
            recipient=recipient,
            extra_data="Sending certificate submit to User",
            pro_type=EventProTypes.CERTIFICATE_SUBMIT,
        )

    @validate_call
    async def send_cert_edit(
        self, *, recipient: IranianPhoneNumber | EmailStr, user_name: str
    ):
        logger.debug(f"sending certificate edit to {recipient}")
        data = {"user": user_name}
        return await self.send_message(
            data=data,
            recipient=recipient,
            extra_data="Sending certificate edit to User",
            pro_type=EventProTypes.CERTIFICATE_EDIT,
        )

    @validate_call
    async def send_edu_sup_inform(
        self, *, recipient: IranianPhoneNumber | EmailStr, user_name: str, course: str
    ):
        logger.debug(f"sending master course to {recipient}")
        data = {"user": user_name, "course": course}
        return await self.send_message(
            data=data,
            recipient=recipient,
            extra_data="Sending Master Course to User",
            pro_type=EventProTypes.MASTER_COURSE_INFORM,
        )

    @validate_call
    async def send_init_course_reg(
        self,
        *,
        recipient: IranianPhoneNumber | EmailStr,
        user_name: str,
        support_phone_number: str | int,
    ):
        logger.debug(f"sending init course register to {recipient}")
        data = {"user": user_name, "support_phone_number": support_phone_number}
        return await self.send_message(
            data=data,
            recipient=recipient,
            extra_data="Sending Init Course Register to User",
            pro_type=EventProTypes.INITIAL_REGISTER,
        )

    @validate_call
    async def send_roadmap(
        self, *, recipient: IranianPhoneNumber | EmailStr, link: HttpUrl, course: str
    ):
        logger.debug(f"sending roadmap to {recipient}")
        data = {"link": link, "course": course}
        return await self.send_message(
            data=data,
            recipient=recipient,
            extra_data="Sending RoadMap to User",
            pro_type=EventProTypes.ROAD_MAP,
        )

    @validate_call
    async def send_success_consult_req(
        self, *, recipient: IranianPhoneNumber | EmailStr, user_name: str, course: str
    ):
        logger.debug(f"sending successful consult request notify to {recipient}")
        data = {"user": user_name, "course": course}
        return await self.send_message(
            data=data,
            recipient=recipient,
            extra_data="Sending Successful Consult Request Notify to User",
            pro_type=EventProTypes.CONSULT_REQUEST,
        )

    @validate_call
    async def send_discount(
        self,
        *,
        recipient: IranianPhoneNumber | EmailStr,
        user_name: str,
        course: str,
        support_phone_number: str,
        discount_code: str,
        discount_percentage: int,
    ):
        logger.debug(f"sending dicount notify to {recipient}")
        data = {
            "user": user_name,
            "course": course,
            "support_phone_number": support_phone_number,
            "discount_code": discount_code,
            "discount_percent": discount_percentage,
        }
        return await self.send_message(
            data=data,
            recipient=recipient,
            extra_data="Sending Discount Notify to User",
            pro_type=EventProTypes.DISCOUNT,
        )

    @validate_call
    async def send_master_course_reg_form(
        self, *, recipient: IranianPhoneNumber | EmailStr, link: HttpUrl
    ):
        logger.debug(f"sending master course register form to {recipient}")
        data = {"link": str(link)}
        return await self.send_message(
            data=data,
            recipient=recipient,
            extra_data="Sending master course register form to User",
            pro_type=EventProTypes.MASTER_COURSE_REGISTER_FORM,
        )

    @validate_call
    async def send_basket_marketing(
        self,
        *,
        recipient: IranianPhoneNumber | EmailStr,
        user_mobile: str,
        user_name: str,
    ):
        logger.debug(f"sending basket marketing sms to {recipient}")
        data = {"user_mobile": user_mobile, "user": user_name}
        return await self.send_message(
            data=data,
            recipient=recipient,
            extra_data="Sending basket marketing sms to User",
            pro_type=EventProTypes.BASKET_MERKETING,
        )
