from datetime import datetime
from typing import Any, ClassVar, Dict, List, Optional

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

from admac.schemas import (
    EducationalExperience,
    Goal,
    Profile,
    Skill,
    SocialMedia,
    Suggestion,
    WorkExperience,
)
from admac.schemas import User as UserSchema

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_bulk(
        self,
        identifiers: List[str | UUID4],
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """Fetch users' info by identifier."""
        request = RequestMessages.FETCH_BULK_USER(uuid_id=map(str, identifiers))
        return await self.perform_rpc(
            rpc_method_name=RPCMethodNames.FETCH_BULK,
            request_message=request,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )

    @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,
        user: Optional[UserSchema] = None,
        profile: Optional[Profile] = None,
        work_experience: Optional[WorkExperience] = None,
        skill: Optional[Skill] = None,
        educational_experience: Optional[EducationalExperience] = None,
        goal: Optional[Goal] = None,
        suggestion: Optional[Suggestion] = None,
        social_media: Optional[SocialMedia] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """Update a user's info using user identifier."""

        # Prepare request data
        request: Dict[str, Any] = {"uuid_id": str(identifier)}

        if user:
            request.update(user.model_dump())  # main user fields

        nested_mapping = {
            "profile": (profile, RequestMessages.PROFILE),
            "work_experience": (work_experience, RequestMessages.WORK_EXPERIENCE),
            "skill": (skill, RequestMessages.SKILL),
            "educational_experience": (
                educational_experience,
                RequestMessages.EDUCATIONAL_EXPERIENCE,
            ),
            "goal": (goal, RequestMessages.GOAL),
            "suggestion": (suggestion, RequestMessages.SUGGESTION),
            "social_media": (social_media, RequestMessages.SOCIAL_MEDIA),
        }

        for field_name, (data, request_class) in nested_mapping.items():
            request[field_name] = request_class(**data.model_dump()) if data else None

        request = RequestMessages.UPDATE_USER(**request)

        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,
        user: Optional[UserSchema] = None,
        profile: Optional[Profile] = None,
        work_experience: Optional[WorkExperience] = None,
        skill: Optional[Skill] = None,
        educational_experience: Optional[EducationalExperience] = None,
        goal: Optional[Goal] = None,
        suggestion: Optional[Suggestion] = None,
        social_media: Optional[SocialMedia] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """Update a user's info using JWT with nested validated data."""

        # Prepare request data
        request = {}
        if user:
            request.update(user.model_dump())  # main user fields

        nested_mapping = {
            "profile": (profile, RequestMessages.PROFILE),
            "work_experience": (work_experience, RequestMessages.WORK_EXPERIENCE),
            "skill": (skill, RequestMessages.SKILL),
            "educational_experience": (
                educational_experience,
                RequestMessages.EDUCATIONAL_EXPERIENCE,
            ),
            "goal": (goal, RequestMessages.GOAL),
            "suggestion": (suggestion, RequestMessages.SUGGESTION),
            "social_media": (social_media, RequestMessages.SOCIAL_MEDIA),
        }

        for field_name, (data, request_class) in nested_mapping.items():
            request[field_name] = request_class(**data.model_dump()) if data else None

        # Create gRPC request
        request = RequestMessages.UPDATE_USER(**request)

        return await self.perform_rpc(
            rpc_method_name=RPCMethodNames.UPDATE_ONE,
            request_message=request,
            jwt=jwt,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )


class Asset(RPCClient):
    stub = Stubs.ASSET

    @validate_call
    async def create(
        self,
        owner_id: str,
        asset_type: str,
        asset_id: str,
        asset_name: str,
        expire_date: Optional[str | datetime] = None,
        is_active: Optional[bool] = None,
        installment_identifier: Optional[str] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """Create an Asset for a User."""

        # Fill the protobuf request
        request = RequestMessages.CREATE_ASSET(
            owner_id=owner_id, type=asset_type, asset_id=asset_id, asset_name=asset_name
        )
        if expire_date:
            request.expire_date = str(expire_date)
        if is_active:
            request.is_active = is_active
        if installment_identifier:
            request.installment_identifier = installment_identifier

        # Perform grpc
        return await self.perform_rpc(
            rpc_method_name=RPCMethodNames.CREATE,
            request_message=request,
            timeout=timeout,
            wait_for_ready=wait_for_ready,
        )
