"""Typed payload dataclasses for realtime events.

Each ``Realtime*Event`` wire message maps to one flat dataclass here. The
payloads are thin references (IDs, scalars, enums), not embedded resources;
use the ``fetch_*`` hydrate helpers to load the full resource through a
:class:`~chattolib.client.ChattoClient`.

``parse_payload(kind, pb_message)`` converts a protobuf event sub-message into
the matching dataclass via the shared ``pb_to_dict`` bridge, so parsing follows
the same ``.parse(dict)`` idiom as :mod:`chattolib.types`.
"""

from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime
from typing import TYPE_CHECKING, Any, Literal, Protocol, TypeAlias, overload

from chattolib.types import (
    CallEventSource,
    NotificationLevel,
    PresenceStatus,
    TimeFormat,
    _parse_enum,
    parse_datetime,
)

if TYPE_CHECKING:
    from chattolib._sync.client import SyncChattoClient
    from chattolib.client import ChattoClient
    from chattolib.types import (
        ActiveCall,
        Asset,
        DirectoryMember,
        Message,
        Notification,
        RoomWithViewerState,
    )


# --- Hydrate mixins ------------------------------------------------------


class _RoomScoped:
    room_id: str

    async def fetch_room(self, client: ChattoClient) -> RoomWithViewerState | None:
        return await client.get_room(self.room_id)

    def fetch_room_sync(self, client: SyncChattoClient) -> RoomWithViewerState | None:
        return client.get_room(self.room_id)


class _MessageScoped:
    room_id: str
    message_event_id: str

    async def fetch_message(self, client: ChattoClient) -> Message | None:
        return await client.get_message(self.room_id, self.message_event_id)

    def fetch_message_sync(self, client: SyncChattoClient) -> Message | None:
        return client.get_message(self.room_id, self.message_event_id)


class _UserScoped:
    user_id: str

    async def fetch_user(self, client: ChattoClient) -> DirectoryMember | None:
        return await client.get_user(user_id=self.user_id)

    def fetch_user_sync(self, client: SyncChattoClient) -> DirectoryMember | None:
        return client.get_user(user_id=self.user_id)


class _NotificationScoped:
    notification_id: str

    async def fetch_notification(self, client: ChattoClient) -> Notification | None:
        return await client.get_notification(self.notification_id)

    def fetch_notification_sync(self, client: SyncChattoClient) -> Notification | None:
        return client.get_notification(self.notification_id)


class _CallScoped:
    room_id: str

    async def fetch_call(self, client: ChattoClient) -> ActiveCall | None:
        return await client.get_active_call(self.room_id)

    def fetch_call_sync(self, client: SyncChattoClient) -> ActiveCall | None:
        return client.get_active_call(self.room_id)


class _AssetScoped:
    room_id: str | None
    asset_id: str

    async def fetch_asset(self, client: ChattoClient) -> Asset | None:
        if not self.room_id:
            return None
        return await client.get_asset(self.room_id, self.asset_id)

    def fetch_asset_sync(self, client: SyncChattoClient) -> Asset | None:
        if not self.room_id:
            return None
        return client.get_asset(self.room_id, self.asset_id)


# --- Payloads ------------------------------------------------------------


@dataclass
class MessagePostedPayload(_RoomScoped, _MessageScoped):
    room_id: str
    message_event_id: str
    thread_root_event_id: str | None = None

    @classmethod
    def parse(cls, data: dict[str, Any]) -> MessagePostedPayload:
        return cls(
            room_id=data.get("roomId", ""),
            message_event_id=data.get("messageEventId", ""),
            thread_root_event_id=data.get("threadRootEventId"),
        )


@dataclass
class MessageEditedPayload(_RoomScoped, _MessageScoped):
    room_id: str
    message_event_id: str

    @classmethod
    def parse(cls, data: dict[str, Any]) -> MessageEditedPayload:
        return cls(
            room_id=data.get("roomId", ""),
            message_event_id=data.get("messageEventId", ""),
        )


@dataclass
class MessageRetractedPayload(_RoomScoped, _MessageScoped):
    room_id: str
    message_event_id: str
    reason: str | None = None

    @classmethod
    def parse(cls, data: dict[str, Any]) -> MessageRetractedPayload:
        return cls(
            room_id=data.get("roomId", ""),
            message_event_id=data.get("messageEventId", ""),
            reason=data.get("reason"),
        )


@dataclass
class ReactionPayload(_RoomScoped, _MessageScoped):
    room_id: str
    message_event_id: str
    emoji: str

    @classmethod
    def parse(cls, data: dict[str, Any]) -> ReactionPayload:
        return cls(
            room_id=data.get("roomId", ""),
            message_event_id=data.get("messageEventId", ""),
            emoji=data.get("emoji", ""),
        )


@dataclass
class TypingPayload(_RoomScoped):
    room_id: str
    thread_root_event_id: str | None = None

    @classmethod
    def parse(cls, data: dict[str, Any]) -> TypingPayload:
        return cls(
            room_id=data.get("roomId", ""),
            thread_root_event_id=data.get("threadRootEventId"),
        )


@dataclass
class PresenceChangedPayload(_UserScoped):
    user_id: str
    status: PresenceStatus

    @classmethod
    def parse(cls, data: dict[str, Any]) -> PresenceChangedPayload:
        return cls(
            user_id=data.get("userId", ""),
            status=_parse_enum(PresenceStatus, data.get("status"), PresenceStatus.UNSPECIFIED),  # type: ignore[arg-type]
        )


@dataclass
class RoomEventPayload(_RoomScoped):
    room_id: str

    @classmethod
    def parse(cls, data: dict[str, Any]) -> RoomEventPayload:
        return cls(room_id=data.get("roomId", ""))


@dataclass
class RoomUniversalChangedPayload(_RoomScoped):
    room_id: str
    universal: bool = False

    @classmethod
    def parse(cls, data: dict[str, Any]) -> RoomUniversalChangedPayload:
        return cls(room_id=data.get("roomId", ""), universal=data.get("universal", False))


@dataclass
class NotificationCreatedPayload(_NotificationScoped):
    notification_id: str
    room_id: str | None = None
    event_id: str | None = None
    in_reply_to_id: str | None = None
    silent: bool = False

    @classmethod
    def parse(cls, data: dict[str, Any]) -> NotificationCreatedPayload:
        return cls(
            notification_id=data.get("notificationId", ""),
            room_id=data.get("roomId"),
            event_id=data.get("eventId"),
            in_reply_to_id=data.get("inReplyToId"),
            silent=data.get("silent", False),
        )


@dataclass
class NotificationDismissedPayload(_NotificationScoped):
    notification_id: str

    @classmethod
    def parse(cls, data: dict[str, Any]) -> NotificationDismissedPayload:
        return cls(notification_id=data.get("notificationId", ""))


@dataclass
class NotificationLevelChangedPayload(_RoomScoped):
    room_id: str
    level: NotificationLevel
    effective_level: NotificationLevel

    @classmethod
    def parse(cls, data: dict[str, Any]) -> NotificationLevelChangedPayload:
        return cls(
            room_id=data.get("roomId", ""),
            level=_parse_enum(NotificationLevel, data.get("level"), NotificationLevel.UNSPECIFIED),  # type: ignore[arg-type]
            effective_level=_parse_enum(
                NotificationLevel, data.get("effectiveLevel"), NotificationLevel.UNSPECIFIED
            ),  # type: ignore[arg-type]
        )


@dataclass
class ThreadFollowChangedPayload(_RoomScoped):
    room_id: str
    thread_root_event_id: str
    following: bool = False

    @classmethod
    def parse(cls, data: dict[str, Any]) -> ThreadFollowChangedPayload:
        return cls(
            room_id=data.get("roomId", ""),
            thread_root_event_id=data.get("threadRootEventId", ""),
            following=data.get("following", False),
        )


@dataclass
class ThreadCreatedPayload(_RoomScoped):
    room_id: str
    thread_root_event_id: str

    @classmethod
    def parse(cls, data: dict[str, Any]) -> ThreadCreatedPayload:
        return cls(
            room_id=data.get("roomId", ""),
            thread_root_event_id=data.get("threadRootEventId", ""),
        )


@dataclass
class RoomMarkedAsReadPayload(_RoomScoped):
    room_id: str

    @classmethod
    def parse(cls, data: dict[str, Any]) -> RoomMarkedAsReadPayload:
        return cls(room_id=data.get("roomId", ""))


@dataclass
class ServerUpdatedPayload:
    name: str
    description: str
    logo_url: str | None = None
    banner_url: str | None = None

    @classmethod
    def parse(cls, data: dict[str, Any]) -> ServerUpdatedPayload:
        return cls(
            name=data.get("name", ""),
            description=data.get("description", ""),
            logo_url=data.get("logoUrl"),
            banner_url=data.get("bannerUrl"),
        )


@dataclass
class UserProfileUpdatedPayload(_UserScoped):
    user_id: str
    login: str
    display_name: str
    avatar_url: str | None = None

    @classmethod
    def parse(cls, data: dict[str, Any]) -> UserProfileUpdatedPayload:
        return cls(
            user_id=data.get("userId", ""),
            login=data.get("login", ""),
            display_name=data.get("displayName", ""),
            avatar_url=data.get("avatarUrl"),
        )


@dataclass
class UserCustomStatusSetPayload(_UserScoped):
    user_id: str
    emoji: str
    text: str
    expires_at: datetime | None = None

    @classmethod
    def parse(cls, data: dict[str, Any]) -> UserCustomStatusSetPayload:
        return cls(
            user_id=data.get("userId", ""),
            emoji=data.get("emoji", ""),
            text=data.get("text", ""),
            expires_at=parse_datetime(data.get("expiresAt")),
        )


@dataclass
class UserCustomStatusClearedPayload(_UserScoped):
    user_id: str

    @classmethod
    def parse(cls, data: dict[str, Any]) -> UserCustomStatusClearedPayload:
        return cls(user_id=data.get("userId", ""))


@dataclass
class ServerUserPreferencesUpdatedPayload:
    time_format: TimeFormat
    timezone: str | None = None

    @classmethod
    def parse(cls, data: dict[str, Any]) -> ServerUserPreferencesUpdatedPayload:
        return cls(
            time_format=_parse_enum(TimeFormat, data.get("timeFormat"), TimeFormat.UNSPECIFIED),  # type: ignore[arg-type]
            timezone=data.get("timezone"),
        )


@dataclass
class RoomGroupsUpdatedPayload:
    changed: bool = False

    @classmethod
    def parse(cls, data: dict[str, Any]) -> RoomGroupsUpdatedPayload:
        return cls(changed=data.get("changed", False))


@dataclass
class ServerMemberDeletedPayload(_UserScoped):
    user_id: str

    @classmethod
    def parse(cls, data: dict[str, Any]) -> ServerMemberDeletedPayload:
        return cls(user_id=data.get("userId", ""))


@dataclass
class AssetProcessingPayload(_AssetScoped):
    asset_id: str
    room_id: str | None = None
    message_event_id: str | None = None

    @classmethod
    def parse(cls, data: dict[str, Any]) -> AssetProcessingPayload:
        return cls(
            asset_id=data.get("assetId", ""),
            room_id=data.get("roomId"),
            message_event_id=data.get("messageEventId"),
        )


@dataclass
class AssetDeletedPayload(_AssetScoped):
    asset_id: str
    room_id: str | None = None

    @classmethod
    def parse(cls, data: dict[str, Any]) -> AssetDeletedPayload:
        return cls(asset_id=data.get("assetId", ""), room_id=data.get("roomId"))


@dataclass
class CallEventPayload(_RoomScoped, _CallScoped):
    room_id: str
    call_id: str
    source: CallEventSource

    @classmethod
    def parse(cls, data: dict[str, Any]) -> CallEventPayload:
        return cls(
            room_id=data.get("roomId", ""),
            call_id=data.get("callId", ""),
            source=_parse_enum(CallEventSource, data.get("source"), CallEventSource.UNSPECIFIED),  # type: ignore[arg-type]
        )


@dataclass
class MentionNotificationPayload(_RoomScoped):
    room_id: str
    actor_user_id: str
    room_name: str | None = None
    actor_display_name: str | None = None

    @classmethod
    def parse(cls, data: dict[str, Any]) -> MentionNotificationPayload:
        return cls(
            room_id=data.get("roomId", ""),
            actor_user_id=data.get("actorUserId", ""),
            room_name=data.get("roomName"),
            actor_display_name=data.get("actorDisplayName"),
        )


@dataclass
class NewDirectMessageNotificationPayload(_RoomScoped):
    room_id: str
    sender_id: str
    sender_display_name: str | None = None
    sender_avatar_url: str | None = None
    conversation_name: str | None = None

    @classmethod
    def parse(cls, data: dict[str, Any]) -> NewDirectMessageNotificationPayload:
        return cls(
            room_id=data.get("roomId", ""),
            sender_id=data.get("senderId", ""),
            sender_display_name=data.get("senderDisplayName"),
            sender_avatar_url=data.get("senderAvatarUrl"),
            conversation_name=data.get("conversationName"),
        )


@dataclass
class SessionTerminatedPayload:
    reason: str

    @classmethod
    def parse(cls, data: dict[str, Any]) -> SessionTerminatedPayload:
        return cls(reason=data.get("reason", ""))


# --- Union + dispatch ----------------------------------------------------

RealtimePayload: TypeAlias = (
    MessagePostedPayload
    | MessageEditedPayload
    | MessageRetractedPayload
    | ReactionPayload
    | TypingPayload
    | PresenceChangedPayload
    | RoomEventPayload
    | RoomUniversalChangedPayload
    | NotificationCreatedPayload
    | NotificationDismissedPayload
    | NotificationLevelChangedPayload
    | ThreadFollowChangedPayload
    | ThreadCreatedPayload
    | RoomMarkedAsReadPayload
    | ServerUpdatedPayload
    | UserProfileUpdatedPayload
    | UserCustomStatusSetPayload
    | UserCustomStatusClearedPayload
    | ServerUserPreferencesUpdatedPayload
    | RoomGroupsUpdatedPayload
    | ServerMemberDeletedPayload
    | AssetProcessingPayload
    | AssetDeletedPayload
    | CallEventPayload
    | MentionNotificationPayload
    | NewDirectMessageNotificationPayload
    | SessionTerminatedPayload
)


class _ParsablePayload(Protocol):
    """Structural type for the payload classes: each exposes ``parse``."""

    @classmethod
    def parse(cls, data: dict[str, Any]) -> RealtimePayload: ...


# Maps each oneof event kind to the payload class that parses it. Several
# kinds share one wire message and therefore one payload class.
_PAYLOAD_DISPATCH: dict[str, type[_ParsablePayload]] = {
    "message_posted": MessagePostedPayload,
    "message_edited": MessageEditedPayload,
    "message_retracted": MessageRetractedPayload,
    "reaction_added": ReactionPayload,
    "reaction_removed": ReactionPayload,
    "user_typing": TypingPayload,
    "presence_changed": PresenceChangedPayload,
    "room_created": RoomEventPayload,
    "room_updated": RoomEventPayload,
    "room_deleted": RoomEventPayload,
    "room_archived": RoomEventPayload,
    "room_unarchived": RoomEventPayload,
    "user_joined_room": RoomEventPayload,
    "user_left_room": RoomEventPayload,
    "room_universal_changed": RoomUniversalChangedPayload,
    "notification_created": NotificationCreatedPayload,
    "notification_dismissed": NotificationDismissedPayload,
    "notification_level_changed": NotificationLevelChangedPayload,
    "thread_follow_changed": ThreadFollowChangedPayload,
    "room_marked_as_read": RoomMarkedAsReadPayload,
    "thread_created": ThreadCreatedPayload,
    "server_updated": ServerUpdatedPayload,
    "user_profile_updated": UserProfileUpdatedPayload,
    "user_custom_status_set": UserCustomStatusSetPayload,
    "user_custom_status_cleared": UserCustomStatusClearedPayload,
    "server_user_preferences_updated": ServerUserPreferencesUpdatedPayload,
    "room_groups_updated": RoomGroupsUpdatedPayload,
    "server_member_deleted": ServerMemberDeletedPayload,
    "asset_processing_started": AssetProcessingPayload,
    "asset_processing_succeeded": AssetProcessingPayload,
    "asset_processing_failed": AssetProcessingPayload,
    "asset_deleted": AssetDeletedPayload,
    "call_started": CallEventPayload,
    "call_participant_joined": CallEventPayload,
    "call_participant_left": CallEventPayload,
    "call_ended": CallEventPayload,
    "mention_notification": MentionNotificationPayload,
    "new_direct_message_notification": NewDirectMessageNotificationPayload,
    "session_terminated": SessionTerminatedPayload,
}


# Per-kind overloads so callers passing a literal kind get the concrete
# payload type inferred (e.g. ``parse_payload("message_posted", pb)`` narrows
# to ``MessagePostedPayload | None``). Mirrors ``RealtimeEvent.get``.
@overload
def parse_payload(
    kind: Literal["message_posted"], pb_message: Any
) -> MessagePostedPayload | None: ...
@overload
def parse_payload(
    kind: Literal["message_edited"], pb_message: Any
) -> MessageEditedPayload | None: ...
@overload
def parse_payload(
    kind: Literal["message_retracted"], pb_message: Any
) -> MessageRetractedPayload | None: ...
@overload
def parse_payload(
    kind: Literal["reaction_added", "reaction_removed"], pb_message: Any
) -> ReactionPayload | None: ...
@overload
def parse_payload(kind: Literal["user_typing"], pb_message: Any) -> TypingPayload | None: ...
@overload
def parse_payload(
    kind: Literal["presence_changed"], pb_message: Any
) -> PresenceChangedPayload | None: ...
@overload
def parse_payload(
    kind: Literal[
        "room_created",
        "room_updated",
        "room_deleted",
        "room_archived",
        "room_unarchived",
        "user_joined_room",
        "user_left_room",
    ],
    pb_message: Any,
) -> RoomEventPayload | None: ...
@overload
def parse_payload(
    kind: Literal["room_universal_changed"], pb_message: Any
) -> RoomUniversalChangedPayload | None: ...
@overload
def parse_payload(
    kind: Literal["notification_created"], pb_message: Any
) -> NotificationCreatedPayload | None: ...
@overload
def parse_payload(
    kind: Literal["notification_dismissed"], pb_message: Any
) -> NotificationDismissedPayload | None: ...
@overload
def parse_payload(
    kind: Literal["notification_level_changed"], pb_message: Any
) -> NotificationLevelChangedPayload | None: ...
@overload
def parse_payload(
    kind: Literal["thread_follow_changed"], pb_message: Any
) -> ThreadFollowChangedPayload | None: ...
@overload
def parse_payload(
    kind: Literal["thread_created"], pb_message: Any
) -> ThreadCreatedPayload | None: ...
@overload
def parse_payload(
    kind: Literal["room_marked_as_read"], pb_message: Any
) -> RoomMarkedAsReadPayload | None: ...
@overload
def parse_payload(
    kind: Literal["server_updated"], pb_message: Any
) -> ServerUpdatedPayload | None: ...
@overload
def parse_payload(
    kind: Literal["user_profile_updated"], pb_message: Any
) -> UserProfileUpdatedPayload | None: ...
@overload
def parse_payload(
    kind: Literal["user_custom_status_set"], pb_message: Any
) -> UserCustomStatusSetPayload | None: ...
@overload
def parse_payload(
    kind: Literal["user_custom_status_cleared"], pb_message: Any
) -> UserCustomStatusClearedPayload | None: ...
@overload
def parse_payload(
    kind: Literal["server_user_preferences_updated"], pb_message: Any
) -> ServerUserPreferencesUpdatedPayload | None: ...
@overload
def parse_payload(
    kind: Literal["room_groups_updated"], pb_message: Any
) -> RoomGroupsUpdatedPayload | None: ...
@overload
def parse_payload(
    kind: Literal["server_member_deleted"], pb_message: Any
) -> ServerMemberDeletedPayload | None: ...
@overload
def parse_payload(
    kind: Literal[
        "asset_processing_started", "asset_processing_succeeded", "asset_processing_failed"
    ],
    pb_message: Any,
) -> AssetProcessingPayload | None: ...
@overload
def parse_payload(
    kind: Literal["asset_deleted"], pb_message: Any
) -> AssetDeletedPayload | None: ...
@overload
def parse_payload(
    kind: Literal["call_started", "call_participant_joined", "call_participant_left", "call_ended"],
    pb_message: Any,
) -> CallEventPayload | None: ...
@overload
def parse_payload(
    kind: Literal["mention_notification"], pb_message: Any
) -> MentionNotificationPayload | None: ...
@overload
def parse_payload(
    kind: Literal["new_direct_message_notification"], pb_message: Any
) -> NewDirectMessageNotificationPayload | None: ...
@overload
def parse_payload(
    kind: Literal["session_terminated"], pb_message: Any
) -> SessionTerminatedPayload | None: ...
@overload
def parse_payload(kind: str, pb_message: Any) -> RealtimePayload | None: ...


def parse_payload(kind: str, pb_message: Any) -> RealtimePayload | None:
    """Convert a realtime event sub-message into its typed payload dataclass.

    Returns ``None`` for unknown/future kinds so newer servers degrade
    gracefully rather than raising.
    """
    payload_cls = _PAYLOAD_DISPATCH.get(kind)
    if payload_cls is None:
        return None
    from chattolib._transport import pb_to_dict

    return payload_cls.parse(pb_to_dict(pb_message))
