from datetime import date
from typing import ClassVar, Optional

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

from . import config
from .api_constants import SUCCESS_CODE, RequestMessages, RPCMethodNames, Stubs


class ValidationError(Exception):
    pass


class ResponseCodeError(Exception):
    pass


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,
        request_message: Optional[Message] = None,
        jwt: Optional[str] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = 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:
            config.create_channel()

        RPCMethod = getattr(self.stub(config.CHANNEL), rpc_method_name, None)

        if RPCMethod:
            metadata = [("api-key", config.grpc_settings.API_KEY)]
            if jwt:
                metadata.append(
                    ("authorization", jwt),
                )
            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)

            return MessageToDict(response, preserving_proto_field_name=True)
        else:
            raise ValidationError("Invalid RPC method name.")


class User(RPCClient):
    stub = Stubs.USER

    @validate_call
    async def fetch_one_by_id(
        self,
        identifier: str | UUID4,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """Fetch a user's info using user identifier."""
        request = RequestMessages.FETCH_USER(uuid_id=str(identifier))
        return await self.perform_rpc(
            rpc_method_name=RPCMethodNames.FETCH_ONE,
            request_message=request,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )

    @validate_call
    async def fetch_one_by_jwt(
        self,
        jwt: str,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """Fetch a user's info using user jwt."""
        return await self.perform_rpc(
            rpc_method_name=RPCMethodNames.FETCH_ONE,
            jwt=jwt,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )

    @validate_call
    async def update_one_by_id(
        self,
        identifier: str | UUID4,
        email: Optional[EmailStr] = None,
        first_name: Optional[str] = None,
        last_name: Optional[str] = None,
        date_of_birth: Optional[date] = None,
        address: Optional[str] = None,
        national_code: Optional[str] = None,
        about: Optional[str] = None,
        job: Optional[str] = None,
        last_education_degree: Optional[str] = None,
        major: Optional[str] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """Update a user's info using user identifier."""
        request = RequestMessages.UPDATE_USER(
            uuid_id=str(identifier),
            email=email,
            first_name=first_name,
            last_name=last_name,
            date_of_birth=str(date_of_birth) if date_of_birth else None,
            address=address,
            national_code=national_code,
            about=about,
            profile=RequestMessages.PROFILE(
                job=job, last_education_degree=last_education_degree, major=major
            ),
        )
        return await self.perform_rpc(
            rpc_method_name=RPCMethodNames.UPDATE_ONE,
            request_message=request,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )

    @validate_call
    async def update_one_by_jwt(
        self,
        jwt: str,
        email: Optional[EmailStr] = None,
        first_name: Optional[str] = None,
        last_name: Optional[str] = None,
        date_of_birth: Optional[date] = None,
        address: Optional[str] = None,
        national_code: Optional[str] = None,
        about: Optional[str] = None,
        job: Optional[str] = None,
        last_education_degree: Optional[str] = None,
        major: Optional[str] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """Update a user's info using user jwt."""
        request = RequestMessages.UPDATE_USER(
            email=email,
            first_name=first_name,
            last_name=last_name,
            date_of_birth=str(date_of_birth) if date_of_birth else None,
            address=address,
            national_code=national_code,
            about=about,
            profile=RequestMessages.PROFILE(
                job=job, last_education_degree=last_education_degree, major=major
            ),
        )
        return await self.perform_rpc(
            rpc_method_name=RPCMethodNames.UPDATE_ONE,
            request_message=request,
            jwt=jwt,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )
