from datetime import datetime
from typing import Any, ClassVar, Dict, List, 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, validate_call

from admac.grpc.accounts_pb2 import WalletPackageRetrieveRequest
from admac.schemas import (
    EducationalExperience,
    Goal,
    Profile,
    Skill,
    SocialMedia,
    Suggestion,
    WalletPackage,
    WalletPackages,
    WorkExperience,
)
from admac.schemas import User as UserSchema
from admac.schemas import Wallet as WalletSchema

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

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,
        jwt: Optional[str] = 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 = [("api-key", config.grpc_settings.API_KEY)]
        if request_id:
            metadata.append(("x-request-id", str(request_id)))
        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)

        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 User(RPCClient):
    stub_cls = Stubs.USER

    @validate_call
    async def fetch_bulk(
        self,
        identifiers: List[str | UUID4],
        request_id: Optional[str | int | UUID] = None,
        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,
            request_id=request_id,
            wait_for_ready=wait_for_ready,
        )

    @validate_call
    async def fetch_one_by_id(
        self,
        identifier: str | UUID4,
        request_id: Optional[str | int | UUID] = None,
        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,
            request_id=request_id,
            wait_for_ready=wait_for_ready,
        )

    @validate_call
    async def fetch_one_by_jwt(
        self,
        jwt: str,
        request_id: Optional[str | int | UUID] = None,
        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,
            request_id=request_id,
            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,
        request_id: Optional[str | int | UUID] = 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,
            request_id=request_id,
            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,
        request_id: Optional[str | int | UUID] = 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,
            request_id=request_id,
            wait_for_ready=wait_for_ready,
        )


class Asset(RPCClient):
    stub_cls = 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,
        registration_completed: Optional[bool] = None,
        request_id: Optional[str | int | UUID] = 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 is not None:
            request.is_active = is_active
        if registration_completed is not None:
            request.registration_completed = registration_completed
        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,
            request_id=request_id,
            wait_for_ready=wait_for_ready,
        )

    @validate_call
    async def fetch_all_by_id(
        self,
        owner_id: str | UUID,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """Fetch user assets by id."""

        # Fill the protobuf request
        request = RequestMessages.LIST_ASSET(owner_id=str(owner_id))

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

    @validate_call
    async def fetch_all_by_jwt(
        self,
        jwt: str,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """Fetch user assets by jwt."""

        # Perform grpc
        return await self.perform_rpc(
            rpc_method_name=RPCMethodNames.FETCH_ALL,
            jwt=jwt,
            timeout=timeout,
            request_id=request_id,
            wait_for_ready=wait_for_ready,
        )


class Wallet(RPCClient):
    stub_cls = Stubs.WALLET

    @validate_call
    async def fetch_one_by_owner_id(
        self,
        owner_id: str | UUID,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """Fetch user wallet by user id."""

        # Fill the protobuf request
        request = RequestMessages.FETCH_WALLET(owner_id=str(owner_id))

        # Perform grpc
        return await self.perform_rpc(
            rpc_method_name=RPCMethodNames.FETCH_ONE,
            request_message=request,
            response_model=WalletSchema,
            timeout=timeout,
            request_id=request_id,
            wait_for_ready=wait_for_ready,
        )

    @validate_call
    async def fetch_one_by_owner_jwt(
        self,
        jwt: str,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """Fetch user wallet by user jwt."""

        # Perform grpc
        return await self.perform_rpc(
            rpc_method_name=RPCMethodNames.FETCH_ONE,
            jwt=jwt,
            response_model=WalletSchema,
            timeout=timeout,
            request_id=request_id,
            wait_for_ready=wait_for_ready,
        )

    @validate_call
    async def fetch_one_package(
        self,
        package_name: str,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """Fetch one wallet package by its name."""

        return await self.perform_rpc(
            rpc_method_name=RPCMethodNames.FETCH_ONE_PACKAGE,
            request_message=WalletPackageRetrieveRequest(name=package_name),
            response_model=WalletPackage,
            timeout=timeout,
            request_id=request_id,
            wait_for_ready=wait_for_ready,
        )

    @validate_call
    async def fetch_all_packages(
        self,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """Fetch all wallet packages."""

        # Perform grpc
        return await self.perform_rpc(
            rpc_method_name=RPCMethodNames.FETCH_ALL_PACKAGES,
            response_model=WalletPackages,
            timeout=timeout,
            request_id=request_id,
            wait_for_ready=wait_for_ready,
        )

    @validate_call
    async def topup(
        self,
        wallet_id: str | UUID,
        amount: Optional[int] = None,
        package_name: Optional[str] = None,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """Add credit to a wallet."""
        request = RequestMessages.TOPUP_WALLET(uuid_id=str(wallet_id))
        if amount:
            request.amount = amount
        if package_name:
            request.package_name = package_name

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

    @validate_call
    async def spend(
        self,
        wallet_id: str | UUID,
        amount: int,
        request_id: Optional[str | int | UUID] = None,
        timeout: Optional[float] = None,
        wait_for_ready: Optional[bool] = None,
    ):
        """Spend amount from wallet a wallet."""
        request = RequestMessages.SPEND_WALLET(uuid_id=str(wallet_id), amount=amount)
        return await self.perform_rpc(
            rpc_method_name=RPCMethodNames.SPEND,
            request_message=request,
            timeout=timeout,
            request_id=request_id,
            wait_for_ready=wait_for_ready,
        )
