"""Dataclasses and enums mirroring Chatto's Connect API messages.

Enum ``value`` strings match the proto JSON encoding used on the wire
(e.g. ``PRESENCE_STATUS_ONLINE``). Parsers below accept both the fully-qualified
form and the short/legacy tail (``ONLINE``) for robustness against older
snapshots or clients that construct short enum strings by hand.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from datetime import UTC, datetime
from enum import Enum, StrEnum
from typing import Any

# --- Enums ---------------------------------------------------------------


def _parse_enum(cls: type[Enum], raw: Any, default: Enum) -> Enum:
    """Accept either the full ``FOO_BAR_BAZ`` form or its tail (``BAZ``)."""
    if raw is None or raw == "":
        return default
    if isinstance(raw, cls):
        return raw
    try:
        return cls(raw)
    except ValueError:
        pass
    # Tolerate integer values from proto JSON alternate encoding.
    if isinstance(raw, int):
        for member in cls:
            if getattr(member, "_proto_index", None) == raw:
                return member
    text = str(raw)
    for member in cls:
        value = member.value if isinstance(member.value, str) else ""
        if value.endswith("_" + text) or value == text:
            return member
    return default


class PresenceStatus(StrEnum):
    UNSPECIFIED = "PRESENCE_STATUS_UNSPECIFIED"
    ONLINE = "PRESENCE_STATUS_ONLINE"
    AWAY = "PRESENCE_STATUS_AWAY"
    DO_NOT_DISTURB = "PRESENCE_STATUS_DO_NOT_DISTURB"
    OFFLINE = "PRESENCE_STATUS_OFFLINE"


class TimeFormat(StrEnum):
    UNSPECIFIED = "TIME_FORMAT_UNSPECIFIED"
    AUTO = "TIME_FORMAT_AUTO"
    HOUR_12 = "TIME_FORMAT_12_HOUR"
    HOUR_24 = "TIME_FORMAT_24_HOUR"


class RoomKind(StrEnum):
    UNSPECIFIED = "ROOM_KIND_UNSPECIFIED"
    CHANNEL = "ROOM_KIND_CHANNEL"
    DM = "ROOM_KIND_DM"


class RoomDirectoryScope(StrEnum):
    UNSPECIFIED = "ROOM_DIRECTORY_SCOPE_UNSPECIFIED"
    ALL = "ROOM_DIRECTORY_SCOPE_ALL"
    CHANNELS = "ROOM_DIRECTORY_SCOPE_CHANNELS"
    DMS = "ROOM_DIRECTORY_SCOPE_DMS"


class NotificationLevel(StrEnum):
    UNSPECIFIED = "NOTIFICATION_LEVEL_UNSPECIFIED"
    DEFAULT = "NOTIFICATION_LEVEL_DEFAULT"
    MUTED = "NOTIFICATION_LEVEL_MUTED"
    NORMAL = "NOTIFICATION_LEVEL_NORMAL"
    ALL_MESSAGES = "NOTIFICATION_LEVEL_ALL_MESSAGES"


class CallEventSource(StrEnum):
    UNSPECIFIED = "REALTIME_CALL_EVENT_SOURCE_UNSPECIFIED"
    USER = "REALTIME_CALL_EVENT_SOURCE_USER"
    LIVEKIT = "REALTIME_CALL_EVENT_SOURCE_LIVEKIT"
    RECONCILIATION = "REALTIME_CALL_EVENT_SOURCE_RECONCILIATION"


class ImageFitMode(StrEnum):
    UNSPECIFIED = "IMAGE_FIT_MODE_UNSPECIFIED"
    CONTAIN = "IMAGE_FIT_MODE_CONTAIN"
    COVER = "IMAGE_FIT_MODE_COVER"


class VideoProcessingStatus(StrEnum):
    UNSPECIFIED = "MESSAGE_VIDEO_PROCESSING_STATUS_UNSPECIFIED"
    PROCESSING = "MESSAGE_VIDEO_PROCESSING_STATUS_PROCESSING"
    COMPLETED = "MESSAGE_VIDEO_PROCESSING_STATUS_COMPLETED"
    FAILED = "MESSAGE_VIDEO_PROCESSING_STATUS_FAILED"


class AssetUploadStatus(StrEnum):
    UNSPECIFIED = "ASSET_UPLOAD_STATUS_UNSPECIFIED"
    OPEN = "ASSET_UPLOAD_STATUS_OPEN"
    COMPLETED = "ASSET_UPLOAD_STATUS_COMPLETED"
    CANCELLED = "ASSET_UPLOAD_STATUS_CANCELLED"


class AdminRoomLayoutItemKind(StrEnum):
    UNSPECIFIED = "ADMIN_ROOM_LAYOUT_ITEM_KIND_UNSPECIFIED"
    ROOM = "ADMIN_ROOM_LAYOUT_ITEM_KIND_ROOM"
    SIDEBAR_LINK = "ADMIN_ROOM_LAYOUT_ITEM_KIND_SIDEBAR_LINK"


class ExternalIdentityFlowKind(StrEnum):
    UNSPECIFIED = "EXTERNAL_IDENTITY_FLOW_KIND_UNSPECIFIED"
    CREATE_ACCOUNT = "EXTERNAL_IDENTITY_FLOW_KIND_CREATE_ACCOUNT"
    LINK_ACCOUNT = "EXTERNAL_IDENTITY_FLOW_KIND_LINK_ACCOUNT"


class PermissionDecision(StrEnum):
    UNSPECIFIED = "PERMISSION_DECISION_UNSPECIFIED"
    ALLOW = "PERMISSION_DECISION_ALLOW"
    DENY = "PERMISSION_DECISION_DENY"
    NONE = "PERMISSION_DECISION_NONE"


class PermissionScopeKind(StrEnum):
    UNSPECIFIED = "PERMISSION_SCOPE_KIND_UNSPECIFIED"
    SERVER = "PERMISSION_SCOPE_KIND_SERVER"
    GROUP = "PERMISSION_SCOPE_KIND_GROUP"
    ROOM = "PERMISSION_SCOPE_KIND_ROOM"


# --- Time helpers --------------------------------------------------------


def parse_datetime(value: Any) -> datetime | None:
    """Parse an RFC 3339 timestamp string (proto JSON encoding for Timestamp)."""
    if not value:
        return None
    if isinstance(value, datetime):
        return value
    text = str(value)
    if text.endswith("Z"):
        text = text[:-1] + "+00:00"
    try:
        return datetime.fromisoformat(text)
    except ValueError:
        return None


def format_datetime(value: datetime) -> str:
    """Format a ``datetime`` as an RFC 3339 UTC timestamp (``...Z``)."""
    if value.tzinfo is None:
        value = value.replace(tzinfo=UTC)
    return value.astimezone(UTC).isoformat().replace("+00:00", "Z")


# --- Core resource dataclasses ------------------------------------------


@dataclass
class CustomUserStatus:
    emoji: str
    text: str
    expires_at: datetime | None = None

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> CustomUserStatus | None:
        if not data:
            return None
        return cls(
            emoji=data.get("emoji", ""),
            text=data.get("text", ""),
            expires_at=parse_datetime(data.get("expiresAt")),
        )


@dataclass
class User:
    id: str
    login: str
    display_name: str
    presence_status: PresenceStatus = PresenceStatus.UNSPECIFIED
    avatar_url: str | None = None
    deleted: bool = False
    custom_status: CustomUserStatus | None = None

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> User | None:
        if not data:
            return None
        return cls(
            id=data.get("id", ""),
            login=data.get("login", ""),
            display_name=data.get("displayName", ""),
            presence_status=_parse_enum(
                PresenceStatus, data.get("presenceStatus"), PresenceStatus.UNSPECIFIED
            ),  # type: ignore[arg-type]
            avatar_url=data.get("avatarUrl"),
            deleted=bool(data.get("deleted", False)),
            custom_status=CustomUserStatus.parse(data.get("customStatus")),
        )


@dataclass
class UserSettings:
    timezone: str | None = None
    time_format: TimeFormat = TimeFormat.UNSPECIFIED

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


@dataclass
class ViewerUser:
    profile: User | None
    settings: UserSettings
    has_verified_email: bool = False
    viewer_can_delete_account: bool = False
    last_login_change: datetime | None = None
    has_password: bool = False

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> ViewerUser | None:
        if not data:
            return None
        return cls(
            profile=User.parse(data.get("profile")),
            settings=UserSettings.parse(data.get("settings")),
            has_verified_email=bool(data.get("hasVerifiedEmail", False)),
            viewer_can_delete_account=bool(data.get("viewerCanDeleteAccount", False)),
            last_login_change=parse_datetime(data.get("lastLoginChange")),
            has_password=bool(data.get("hasPassword", False)),
        )


@dataclass
class DirectoryMember:
    user: User | None
    roles: list[str] = field(default_factory=list)
    created_at: datetime | None = None

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> DirectoryMember | None:
        if not data:
            return None
        return cls(
            user=User.parse(data.get("user")),
            roles=list(data.get("roles") or []),
            created_at=parse_datetime(data.get("createdAt")),
        )


@dataclass
class Page:
    total_count: int = 0
    has_more: bool = False

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> Page:
        data = data or {}
        return cls(
            total_count=int(data.get("totalCount", 0)),
            has_more=bool(data.get("hasMore", False)),
        )


@dataclass
class ServerProfile:
    name: str
    version: str = ""
    logo_url: str | None = None
    banner_url: str | None = None
    welcome_message: str | None = None
    description: str | None = None

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> ServerProfile:
        data = data or {}
        return cls(
            name=data.get("name", ""),
            version=data.get("version", ""),
            logo_url=data.get("logoUrl"),
            banner_url=data.get("bannerUrl"),
            welcome_message=data.get("welcomeMessage"),
            description=data.get("description"),
        )


@dataclass
class ProviderMetadata:
    id: str
    type: str
    label: str
    login_url: str = ""

    @classmethod
    def parse(cls, data: dict[str, Any]) -> ProviderMetadata:
        return cls(
            id=data.get("id", ""),
            type=data.get("type", ""),
            label=data.get("label", ""),
            login_url=data.get("loginUrl", ""),
        )


@dataclass
class ServerLogin:
    direct_registration_enabled: bool = False
    providers: list[ProviderMetadata] = field(default_factory=list)
    authorize_url: str = ""

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> ServerLogin:
        data = data or {}
        return cls(
            direct_registration_enabled=bool(data.get("directRegistrationEnabled", False)),
            providers=[ProviderMetadata.parse(p) for p in data.get("providers") or []],
            authorize_url=data.get("authorizeUrl", ""),
        )


@dataclass
class ServerRuntimeConfig:
    push_notifications_enabled: bool = False
    vapid_public_key: str | None = None
    livekit_url: str | None = None
    video_processing_enabled: bool = False
    max_upload_size: int = 0
    max_video_upload_size: int = 0
    message_edit_window_seconds: int = 0

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> ServerRuntimeConfig:
        data = data or {}
        return cls(
            push_notifications_enabled=bool(data.get("pushNotificationsEnabled", False)),
            vapid_public_key=data.get("vapidPublicKey"),
            livekit_url=data.get("livekitUrl"),
            video_processing_enabled=bool(data.get("videoProcessingEnabled", False)),
            max_upload_size=int(data.get("maxUploadSize", 0)),
            max_video_upload_size=int(data.get("maxVideoUploadSize", 0)),
            message_edit_window_seconds=int(data.get("messageEditWindowSeconds", 0)),
        )


@dataclass
class Room:
    id: str
    kind: RoomKind = RoomKind.UNSPECIFIED
    name: str = ""
    description: str = ""
    archived: bool = False
    group_id: str = ""
    universal: bool = False

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> Room | None:
        if not data:
            return None
        return cls(
            id=data.get("id", ""),
            kind=_parse_enum(RoomKind, data.get("kind"), RoomKind.UNSPECIFIED),  # type: ignore[arg-type]
            name=data.get("name", ""),
            description=data.get("description", ""),
            archived=bool(data.get("archived", False)),
            group_id=data.get("groupId", ""),
            universal=bool(data.get("universal", False)),
        )


@dataclass
class RoomSummary:
    id: str
    kind: RoomKind = RoomKind.UNSPECIFIED
    name: str = ""

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> RoomSummary | None:
        if not data:
            return None
        return cls(
            id=data.get("id", ""),
            kind=_parse_enum(RoomKind, data.get("kind"), RoomKind.UNSPECIFIED),  # type: ignore[arg-type]
            name=data.get("name", ""),
        )


@dataclass
class RoomViewerState:
    is_member: bool = False
    has_unread: bool = False

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> RoomViewerState:
        data = data or {}
        return cls(
            is_member=bool(data.get("isMember", False)),
            has_unread=bool(data.get("hasUnread", False)),
        )


@dataclass
class RoomWithViewerState:
    room: Room | None
    viewer_state: RoomViewerState

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> RoomWithViewerState | None:
        if not data:
            return None
        return cls(
            room=Room.parse(data.get("room")),
            viewer_state=RoomViewerState.parse(data.get("viewerState")),
        )


@dataclass
class SidebarLink:
    id: str
    label: str
    url: str

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> SidebarLink | None:
        if not data:
            return None
        return cls(
            id=data.get("id", ""),
            label=data.get("label", ""),
            url=data.get("url", ""),
        )


@dataclass
class RoomGroup:
    id: str
    name: str
    description: str = ""
    rooms: list[RoomWithViewerState] = field(default_factory=list)
    sidebar_links: list[SidebarLink] = field(default_factory=list)

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> RoomGroup | None:
        if not data:
            return None
        rooms: list[RoomWithViewerState] = []
        links: list[SidebarLink] = []
        for item in data.get("items") or []:
            room = RoomWithViewerState.parse(item.get("room"))
            if room:
                rooms.append(room)
            sl = item.get("sidebarLink")
            if sl:
                links.append(
                    SidebarLink(
                        id=sl.get("id", ""),
                        label=sl.get("label", ""),
                        url=sl.get("url", ""),
                    )
                )
        return cls(
            id=data.get("id", ""),
            name=data.get("name", ""),
            description=data.get("description", ""),
            rooms=rooms,
            sidebar_links=links,
        )


@dataclass
class RoomBan:
    id: str
    room_id: str
    user_id: str
    moderator_id: str = ""
    reason: str = ""
    created_at: datetime | None = None
    expires_at: datetime | None = None
    room: Room | None = None
    user: DirectoryMember | None = None
    moderator: DirectoryMember | None = None

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> RoomBan | None:
        if not data:
            return None
        return cls(
            id=data.get("id", ""),
            room_id=data.get("roomId", ""),
            user_id=data.get("userId", ""),
            moderator_id=data.get("moderatorId", ""),
            reason=data.get("reason", ""),
            created_at=parse_datetime(data.get("createdAt")),
            expires_at=parse_datetime(data.get("expiresAt")),
            room=Room.parse(data.get("room")),
            user=DirectoryMember.parse(data.get("user")),
            moderator=DirectoryMember.parse(data.get("moderator")),
        )


@dataclass
class AssetUrl:
    url: str
    expires_at: datetime | None = None

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> AssetUrl | None:
        if not data:
            return None
        return cls(
            url=data.get("url", ""),
            expires_at=parse_datetime(data.get("expiresAt")),
        )


@dataclass
class VideoVariant:
    quality: str
    width: int
    height: int
    size: int
    asset_url: AssetUrl | None

    @classmethod
    def parse(cls, data: dict[str, Any]) -> VideoVariant:
        return cls(
            quality=data.get("quality", ""),
            width=int(data.get("width", 0)),
            height=int(data.get("height", 0)),
            size=int(data.get("size", 0)),
            asset_url=AssetUrl.parse(data.get("assetUrl")),
        )


@dataclass
class VideoProcessing:
    status: VideoProcessingStatus = VideoProcessingStatus.UNSPECIFIED
    duration_ms: int = 0
    width: int = 0
    height: int = 0
    source_available: bool = False
    reason_code: str = ""
    thumbnail_asset_url: AssetUrl | None = None
    variants: list[VideoVariant] = field(default_factory=list)

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> VideoProcessing | None:
        if not data:
            return None
        return cls(
            status=_parse_enum(
                VideoProcessingStatus,
                data.get("status"),
                VideoProcessingStatus.UNSPECIFIED,
            ),  # type: ignore[arg-type]
            duration_ms=int(data.get("durationMs", 0)),
            width=int(data.get("width", 0)),
            height=int(data.get("height", 0)),
            source_available=bool(data.get("sourceAvailable", False)),
            reason_code=data.get("reasonCode", ""),
            thumbnail_asset_url=AssetUrl.parse(data.get("thumbnailAssetUrl")),
            variants=[VideoVariant.parse(v) for v in data.get("variants") or []],
        )


@dataclass
class MessageAttachment:
    id: str
    filename: str
    content_type: str
    width: int = 0
    height: int = 0
    asset_url: AssetUrl | None = None
    thumbnail_asset_url: AssetUrl | None = None
    video_processing: VideoProcessing | None = None

    @classmethod
    def parse(cls, data: dict[str, Any]) -> MessageAttachment:
        return cls(
            id=data.get("id", ""),
            filename=data.get("filename", ""),
            content_type=data.get("contentType", ""),
            width=int(data.get("width", 0)),
            height=int(data.get("height", 0)),
            asset_url=AssetUrl.parse(data.get("assetUrl")),
            thumbnail_asset_url=AssetUrl.parse(data.get("thumbnailAssetUrl")),
            video_processing=VideoProcessing.parse(data.get("videoProcessing")),
        )


@dataclass
class LinkPreview:
    url: str
    title: str | None = None
    description: str | None = None
    image_url: str | None = None
    image_asset_id: str | None = None
    site_name: str | None = None
    embed_type: str | None = None
    embed_id: str | None = None

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> LinkPreview | None:
        if not data:
            return None
        return cls(
            url=data.get("url", ""),
            title=data.get("title"),
            description=data.get("description"),
            image_url=data.get("imageUrl"),
            image_asset_id=data.get("imageAssetId"),
            site_name=data.get("siteName"),
            embed_type=data.get("embedType"),
            embed_id=data.get("embedId"),
        )


@dataclass
class MessageReaction:
    emoji: str
    count: int = 0
    has_reacted: bool = False
    preview_user_ids: list[str] = field(default_factory=list)

    @classmethod
    def parse(cls, data: dict[str, Any]) -> MessageReaction:
        return cls(
            emoji=data.get("emoji", ""),
            count=int(data.get("count", 0)),
            has_reacted=bool(data.get("hasReacted", False)),
            preview_user_ids=list(data.get("previewUserIds") or []),
        )


@dataclass
class ThreadSummary:
    thread_root_event_id: str
    reply_count: int = 0
    last_reply_at: datetime | None = None
    participant_preview_user_ids: list[str] = field(default_factory=list)
    participant_count: int = 0
    is_following: bool | None = None
    has_unread: bool | None = None

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> ThreadSummary | None:
        if not data:
            return None
        viewer = data.get("viewerState") or {}
        return cls(
            thread_root_event_id=data.get("threadRootEventId", ""),
            reply_count=int(data.get("replyCount", 0)),
            last_reply_at=parse_datetime(data.get("lastReplyAt")),
            participant_preview_user_ids=list(
                data.get("participantPreviewUserIds") or []
            ),
            participant_count=int(data.get("participantCount", 0)),
            is_following=viewer.get("isFollowing") if "isFollowing" in viewer else None,
            has_unread=viewer.get("hasUnread") if "hasUnread" in viewer else None,
        )


@dataclass
class Message:
    id: str
    room_id: str
    created_at: datetime | None
    actor_id: str
    body: str | None = None
    attachments: list[MessageAttachment] = field(default_factory=list)
    link_preview: LinkPreview | None = None
    updated_at: datetime | None = None
    in_reply_to: str = ""
    thread_root_event_id: str = ""
    echo_of_event_id: str = ""
    echo_from_thread_root_event_id: str = ""
    channel_echo_event_id: str = ""
    reactions: list[MessageReaction] = field(default_factory=list)
    thread: ThreadSummary | None = None
    deleted_at: datetime | None = None

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> Message | None:
        if not data:
            return None
        return cls(
            id=data.get("id", ""),
            room_id=data.get("roomId", ""),
            created_at=parse_datetime(data.get("createdAt")),
            actor_id=data.get("actorId", ""),
            body=data.get("body"),
            attachments=[MessageAttachment.parse(a) for a in data.get("attachments") or []],
            link_preview=LinkPreview.parse(data.get("linkPreview")),
            updated_at=parse_datetime(data.get("updatedAt")),
            in_reply_to=data.get("inReplyTo", ""),
            thread_root_event_id=data.get("threadRootEventId", ""),
            echo_of_event_id=data.get("echoOfEventId", ""),
            echo_from_thread_root_event_id=data.get("echoFromThreadRootEventId", ""),
            channel_echo_event_id=data.get("channelEchoEventId", ""),
            reactions=[MessageReaction.parse(r) for r in data.get("reactions") or []],
            thread=ThreadSummary.parse(data.get("thread")),
            deleted_at=parse_datetime(data.get("deletedAt")),
        )


@dataclass
class TimelineEvent:
    id: str
    created_at: datetime | None
    actor_id: str = ""
    kind: str = ""  # "message_posted", "room_created", "user_joined_room", ...
    message: Message | None = None
    room_id: str = ""

    @classmethod
    def parse(cls, data: dict[str, Any]) -> TimelineEvent:
        kind = ""
        message: Message | None = None
        room_id = ""
        for candidate in (
            "messagePosted",
            "roomCreated",
            "roomUpdated",
            "roomDeleted",
            "roomArchived",
            "roomUnarchived",
            "userJoinedRoom",
            "userLeftRoom",
        ):
            payload = data.get(candidate)
            if payload is None:
                continue
            kind = _snake_case(candidate)
            if candidate == "messagePosted":
                message = Message.parse(payload.get("message"))
            else:
                room_id = payload.get("roomId", "")
            break
        return cls(
            id=data.get("id", ""),
            created_at=parse_datetime(data.get("createdAt")),
            actor_id=data.get("actorId", ""),
            kind=kind,
            message=message,
            room_id=room_id,
        )


def _snake_case(name: str) -> str:
    out: list[str] = []
    for ch in name:
        if ch.isupper() and out:
            out.append("_")
        out.append(ch.lower())
    return "".join(out)


@dataclass
class TimelinePage:
    events: list[TimelineEvent] = field(default_factory=list)
    start_cursor: str = ""
    end_cursor: str = ""
    has_older: bool = False
    has_newer: bool = False
    users_by_id: dict[str, User] = field(default_factory=dict)

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> TimelinePage:
        data = data or {}
        includes = data.get("includes") or {}
        users_raw = includes.get("users") or {}
        users: dict[str, User] = {}
        for uid, user_data in users_raw.items():
            parsed_user = User.parse(user_data)
            if parsed_user is not None:
                users[uid] = parsed_user
        return cls(
            events=[TimelineEvent.parse(e) for e in data.get("events") or []],
            start_cursor=data.get("startCursor", ""),
            end_cursor=data.get("endCursor", ""),
            has_older=bool(data.get("hasOlder", False)),
            has_newer=bool(data.get("hasNewer", False)),
            users_by_id=users,
        )


@dataclass
class FollowedThread:
    room: RoomSummary | None
    root_message: Message | None
    thread: ThreadSummary | None

    @classmethod
    def parse(cls, data: dict[str, Any]) -> FollowedThread:
        return cls(
            room=RoomSummary.parse(data.get("room")),
            root_message=Message.parse(data.get("rootMessage")),
            thread=ThreadSummary.parse(data.get("thread")),
        )


@dataclass
class FollowedThreadsPage:
    threads: list[FollowedThread] = field(default_factory=list)
    page: Page = field(default_factory=Page)
    users_by_id: dict[str, User] = field(default_factory=dict)


@dataclass
class Notification:
    id: str
    created_at: datetime | None
    actor: User | None
    kind: str  # "direct_message" | "mention" | "reply" | "room_message"
    room: RoomSummary | None = None
    event_id: str = ""
    in_reply_to_id: str = ""
    thread_root_event_id: str | None = None

    @classmethod
    def parse(cls, data: dict[str, Any]) -> Notification:
        kind = ""
        room: RoomSummary | None = None
        event_id = ""
        in_reply_to_id = ""
        thread_root: str | None = None
        for candidate in ("directMessage", "mention", "reply", "roomMessage"):
            payload = data.get(candidate)
            if payload is None:
                continue
            kind = _snake_case(candidate)
            room = RoomSummary.parse(payload.get("room"))
            event_id = payload.get("eventId", "")
            in_reply_to_id = payload.get("inReplyToId", "") if candidate == "reply" else ""
            thread_root = payload.get("threadRootEventId")
            break
        return cls(
            id=data.get("id", ""),
            created_at=parse_datetime(data.get("createdAt")),
            actor=User.parse(data.get("actor")),
            kind=kind,
            room=room,
            event_id=event_id,
            in_reply_to_id=in_reply_to_id,
            thread_root_event_id=thread_root,
        )


@dataclass
class NotificationsPage:
    notifications: list[Notification] = field(default_factory=list)
    page: Page = field(default_factory=Page)


@dataclass
class NotificationPreference:
    level: NotificationLevel = NotificationLevel.UNSPECIFIED
    effective_level: NotificationLevel = NotificationLevel.UNSPECIFIED

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> NotificationPreference:
        data = data or {}
        return cls(
            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 Asset:
    id: str
    filename: str
    content_type: str
    size: int
    width: int = 0
    height: int = 0
    asset_url: AssetUrl | None = None
    thumbnail_asset_url: AssetUrl | None = None
    video_processing: VideoProcessing | None = None

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> Asset | None:
        if not data:
            return None
        return cls(
            id=data.get("id", ""),
            filename=data.get("filename", ""),
            content_type=data.get("contentType", ""),
            size=int(data.get("size", 0)),
            width=int(data.get("width", 0)),
            height=int(data.get("height", 0)),
            asset_url=AssetUrl.parse(data.get("assetUrl")),
            thumbnail_asset_url=AssetUrl.parse(data.get("thumbnailAssetUrl")),
            video_processing=VideoProcessing.parse(data.get("videoProcessing")),
        )


@dataclass
class ImageTransformOptions:
    width: int
    height: int
    fit: ImageFitMode = ImageFitMode.UNSPECIFIED

    def to_wire(self) -> dict[str, Any]:
        return {
            "width": self.width,
            "height": self.height,
            "fit": self.fit.value,
        }


@dataclass
class ActiveCallParticipant:
    user: User | None = None
    joined_at: datetime | None = None
    call_id: str = ""

    @classmethod
    def parse(cls, data: dict[str, Any]) -> ActiveCallParticipant:
        return cls(
            user=User.parse(data.get("user")),
            joined_at=parse_datetime(data.get("joinedAt")),
            call_id=data.get("callId", ""),
        )


@dataclass
class ActiveCall:
    call_id: str
    room: RoomSummary | None
    participants: list[ActiveCallParticipant] = field(default_factory=list)

    @classmethod
    def parse(cls, data: dict[str, Any]) -> ActiveCall:
        return cls(
            call_id=data.get("callId", ""),
            room=RoomSummary.parse(data.get("room")),
            participants=[
                ActiveCallParticipant.parse(p) for p in data.get("participants") or []
            ],
        )


# --- Roles ----------------------------------------------------------------


@dataclass
class Role:
    name: str
    display_name: str = ""
    description: str = ""
    is_system: bool = False
    position: int = 0
    pingable: bool = False

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> Role | None:
        if not data:
            return None
        return cls(
            name=data.get("name", ""),
            display_name=data.get("displayName", ""),
            description=data.get("description", ""),
            is_system=bool(data.get("isSystem", False)),
            position=int(data.get("position", 0)),
            pingable=bool(data.get("pingable", False)),
        )


@dataclass
class AdminRole:
    role: Role | None
    permissions: list[str] = field(default_factory=list)
    permission_denials: list[str] = field(default_factory=list)

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> AdminRole | None:
        if not data:
            return None
        return cls(
            role=Role.parse(data.get("role")),
            permissions=list(data.get("permissions") or []),
            permission_denials=list(data.get("permissionDenials") or []),
        )


# --- Asset uploads --------------------------------------------------------


@dataclass
class AssetUpload:
    upload_id: str
    room_id: str
    status: AssetUploadStatus = AssetUploadStatus.UNSPECIFIED
    committed_offset: int = 0
    size: int = 0
    max_chunk_size: int = 0
    sha256: str = ""
    expires_at: datetime | None = None
    asset_id: str = ""

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> AssetUpload | None:
        if not data:
            return None
        return cls(
            upload_id=data.get("uploadId", ""),
            room_id=data.get("roomId", ""),
            status=_parse_enum(
                AssetUploadStatus,
                data.get("status"),
                AssetUploadStatus.UNSPECIFIED,
            ),  # type: ignore[arg-type]
            committed_offset=int(data.get("committedOffset", 0)),
            size=int(data.get("size", 0)),
            max_chunk_size=int(data.get("maxChunkSize", 0)),
            sha256=data.get("sha256", ""),
            expires_at=parse_datetime(data.get("expiresAt")),
            asset_id=data.get("assetId", ""),
        )


# --- External identity ----------------------------------------------------


@dataclass
class ExternalIdentityProvider:
    provider: ProviderMetadata | None
    link_url: str = ""
    linked: bool = False
    linked_identity_subject_hash: str = ""

    @classmethod
    def parse(cls, data: dict[str, Any]) -> ExternalIdentityProvider:
        provider_data = data.get("provider")
        provider = ProviderMetadata.parse(provider_data) if provider_data else None
        return cls(
            provider=provider,
            link_url=data.get("linkUrl", ""),
            linked=bool(data.get("linked", False)),
            linked_identity_subject_hash=data.get("linkedIdentitySubjectHash", ""),
        )


@dataclass
class LinkedExternalIdentity:
    provider_id: str
    provider_type: str = ""
    provider_label: str = ""
    subject_hash: str = ""

    @classmethod
    def parse(cls, data: dict[str, Any]) -> LinkedExternalIdentity:
        return cls(
            provider_id=data.get("providerId", ""),
            provider_type=data.get("providerType", ""),
            provider_label=data.get("providerLabel", ""),
            subject_hash=data.get("subjectHash", ""),
        )


# --- External identity flow types ----------------------------------------


@dataclass
class PendingExternalIdentity:
    kind: ExternalIdentityFlowKind = ExternalIdentityFlowKind.UNSPECIFIED
    provider_id: str = ""
    provider_type: str = ""
    provider_label: str = ""
    verified_email: str = ""
    login_hint: str = ""
    display_name_hint: str = ""
    bound_user_id: str = ""
    redirect_path: str = ""

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> PendingExternalIdentity | None:
        if not data:
            return None
        return cls(
            kind=_parse_enum(  # type: ignore[arg-type]
                ExternalIdentityFlowKind,
                data.get("kind"),
                ExternalIdentityFlowKind.UNSPECIFIED,
            ),
            provider_id=data.get("providerId", ""),
            provider_type=data.get("providerType", ""),
            provider_label=data.get("providerLabel", ""),
            verified_email=data.get("verifiedEmail", ""),
            login_hint=data.get("loginHint", ""),
            display_name_hint=data.get("displayNameHint", ""),
            bound_user_id=data.get("boundUserId", ""),
            redirect_path=data.get("redirectPath", ""),
        )


@dataclass
class ExternalIdentityAccount:
    user_id: str = ""
    login: str = ""
    token: str = ""

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> ExternalIdentityAccount | None:
        if not data:
            return None
        return cls(
            user_id=data.get("userId", ""),
            login=data.get("login", ""),
            token=data.get("token", ""),
        )


# --- Admin: server / members ---------------------------------------------


@dataclass
class ServerConfig:
    server_name: str = ""
    description: str = ""
    motd: str = ""
    welcome_message: str = ""

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> ServerConfig:
        data = data or {}
        return cls(
            server_name=data.get("serverName", ""),
            description=data.get("description", ""),
            motd=data.get("motd", ""),
            welcome_message=data.get("welcomeMessage", ""),
        )


@dataclass
class AdminMember:
    user: User | None
    roles: list[str] = field(default_factory=list)
    created_at: datetime | None = None
    has_verified_email: bool = False
    verified_emails: list[str] = field(default_factory=list)
    viewer_can_delete_account: bool = False
    last_login_change: datetime | None = None

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> AdminMember | None:
        if not data:
            return None
        return cls(
            user=User.parse(data.get("user")),
            roles=list(data.get("roles") or []),
            created_at=parse_datetime(data.get("createdAt")),
            has_verified_email=bool(data.get("hasVerifiedEmail", False)),
            verified_emails=list(data.get("verifiedEmails") or []),
            viewer_can_delete_account=bool(data.get("viewerCanDeleteAccount", False)),
            last_login_change=parse_datetime(data.get("lastLoginChange")),
        )


# --- Admin: member/role detail envelopes ---------------------------------


@dataclass
class AdminMemberDetail:
    member: AdminMember | None
    roles: list[AdminRole] = field(default_factory=list)
    available_permissions: list[str] = field(default_factory=list)
    viewer_can_assign_roles: bool = False
    viewer_can_manage_roles: bool = False
    viewer_can_manage_user_permissions: bool = False

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> AdminMemberDetail:
        data = data or {}
        roles = [
            r
            for raw in data.get("roles") or []
            if (r := AdminRole.parse(raw)) is not None
        ]
        return cls(
            member=AdminMember.parse(data.get("member")),
            roles=roles,
            available_permissions=list(data.get("availablePermissions") or []),
            viewer_can_assign_roles=bool(data.get("viewerCanAssignRoles", False)),
            viewer_can_manage_roles=bool(data.get("viewerCanManageRoles", False)),
            viewer_can_manage_user_permissions=bool(
                data.get("viewerCanManageUserPermissions", False)
            ),
        )


@dataclass
class AdminRoleDetail:
    role: AdminRole | None
    users: list[User] = field(default_factory=list)
    viewer_can_manage_roles: bool = False
    viewer_can_assign_roles: bool = False

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> AdminRoleDetail:
        data = data or {}
        users = [
            u
            for raw in data.get("users") or []
            if (u := User.parse(raw)) is not None
        ]
        return cls(
            role=AdminRole.parse(data.get("role")),
            users=users,
            viewer_can_manage_roles=bool(data.get("viewerCanManageRoles", False)),
            viewer_can_assign_roles=bool(data.get("viewerCanAssignRoles", False)),
        )


# --- Admin: room layout --------------------------------------------------


@dataclass
class AdminRoomLayoutGroup:
    id: str
    name: str
    description: str = ""
    rooms: list[Room] = field(default_factory=list)
    sidebar_links: list[SidebarLink] = field(default_factory=list)
    can_create_room: bool = False

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> AdminRoomLayoutGroup | None:
        if not data:
            return None
        rooms: list[Room] = []
        links: list[SidebarLink] = []
        for item in data.get("items") or []:
            room = Room.parse(item.get("room"))
            if room is not None:
                rooms.append(room)
            sl = item.get("sidebarLink")
            if sl:
                links.append(
                    SidebarLink(
                        id=sl.get("id", ""),
                        label=sl.get("label", ""),
                        url=sl.get("url", ""),
                    )
                )
        return cls(
            id=data.get("id", ""),
            name=data.get("name", ""),
            description=data.get("description", ""),
            rooms=rooms,
            sidebar_links=links,
            can_create_room=bool(data.get("canCreateRoom", False)),
        )


# --- Viewer snapshot (GetViewerResponse) ----------------------------------


@dataclass
class CapabilityGrant:
    """Effective decision for one capability key (e.g. ``admin.view``)."""

    capability: str
    granted: bool = False

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> CapabilityGrant | None:
        if data is None:
            return None
        return cls(
            capability=data.get("capability", ""),
            granted=bool(data.get("granted", False)),
        )


@dataclass
class ViewerCapabilities:
    """Permission-derived capabilities for the authenticated user."""

    grants: list[CapabilityGrant] = field(default_factory=list)
    has_unread_followed_threads: bool = False
    has_pending_followed_thread_notifications: bool = False

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> ViewerCapabilities | None:
        if data is None:
            return None
        grants = [
            g
            for raw in data.get("grants") or []
            if (g := CapabilityGrant.parse(raw)) is not None
        ]
        return cls(
            grants=grants,
            has_unread_followed_threads=bool(
                data.get("hasUnreadFollowedThreads", False)
            ),
            has_pending_followed_thread_notifications=bool(
                data.get("hasPendingFollowedThreadNotifications", False)
            ),
        )


@dataclass
class PermissionGrant:
    """Effective decision for one permission key (e.g. ``server.manage``)."""

    permission: str
    granted: bool = False

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> PermissionGrant | None:
        if data is None:
            return None
        return cls(
            permission=data.get("permission", ""),
            granted=bool(data.get("granted", False)),
        )


@dataclass
class ViewerState:
    """Non-permission server state for the authenticated user."""

    has_unread_rooms: bool = False

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> ViewerState | None:
        if data is None:
            return None
        return cls(
            has_unread_rooms=bool(data.get("hasUnreadRooms", False)),
        )


@dataclass
class RoomNotificationPreference:
    """Notification preference for one joined room."""

    room_id: str
    preference: NotificationPreference | None = None

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> RoomNotificationPreference | None:
        if data is None:
            return None
        raw_pref = data.get("preference")
        return cls(
            room_id=data.get("roomId", ""),
            preference=NotificationPreference.parse(raw_pref) if raw_pref is not None else None,
        )


@dataclass
class AdminEventLogEntry:
    """One durable EVT entry decoded for operator inspection (``AdminEventLogEntry``)."""

    sequence: str = ""
    subject: str = ""
    aggregate_type: str = ""
    aggregate_id: str = ""
    event_type: str = ""
    event_id: str = ""
    actor_id: str = ""
    created_at: datetime | None = None
    payload_json: str = ""

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> AdminEventLogEntry | None:
        if data is None:
            return None
        return cls(
            sequence=data.get("sequence", ""),
            subject=data.get("subject", ""),
            aggregate_type=data.get("aggregateType", ""),
            aggregate_id=data.get("aggregateId", ""),
            event_type=data.get("eventType", ""),
            event_id=data.get("eventId", ""),
            actor_id=data.get("actorId", ""),
            created_at=parse_datetime(data.get("createdAt")),
            payload_json=data.get("payloadJson", ""),
        )


@dataclass
class AdminEventLogPage:
    """Newest-first page of durable EVT entries (``ListEventsResponse``)."""

    entries: list[AdminEventLogEntry] = field(default_factory=list)
    has_older: bool = False
    end_cursor: str | None = None
    total_count: int = 0
    scanned_count: int = 0
    scan_limit: int = 0
    scan_limited: bool = False

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> AdminEventLogPage:
        data = data or {}
        entries = [
            e
            for raw in data.get("entries") or []
            if (e := AdminEventLogEntry.parse(raw)) is not None
        ]
        return cls(
            entries=entries,
            has_older=bool(data.get("hasOlder", False)),
            end_cursor=data.get("endCursor"),
            total_count=int(data.get("totalCount", 0)),
            scanned_count=int(data.get("scannedCount", 0)),
            scan_limit=int(data.get("scanLimit", 0)),
            scan_limited=bool(data.get("scanLimited", False)),
        )


@dataclass
class ViewerSnapshot:
    """Full authenticated viewer snapshot (``GetViewerResponse``)."""

    user: ViewerUser | None = None
    capabilities: ViewerCapabilities | None = None
    server_notification_preference: NotificationPreference | None = None
    room_notification_preferences: list[RoomNotificationPreference] = field(
        default_factory=list
    )
    viewer_permissions: list[PermissionGrant] = field(default_factory=list)
    viewer_state: ViewerState | None = None

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> ViewerSnapshot:
        data = data or {}
        # ServerViewerPermissions wraps a "permissions" list; flatten it.
        vp_raw = data.get("viewerPermissions") or {}
        viewer_permissions = [
            p
            for raw in vp_raw.get("permissions") or []
            if (p := PermissionGrant.parse(raw)) is not None
        ]
        room_prefs = [
            rp
            for raw in data.get("roomNotificationPreferences") or []
            if (rp := RoomNotificationPreference.parse(raw)) is not None
        ]
        raw_snp = data.get("serverNotificationPreference")
        return cls(
            user=ViewerUser.parse(data.get("user")),
            capabilities=ViewerCapabilities.parse(data.get("capabilities")),
            server_notification_preference=(
                NotificationPreference.parse(raw_snp) if raw_snp is not None else None
            ),
            room_notification_preferences=room_prefs,
            viewer_permissions=viewer_permissions,
            viewer_state=ViewerState.parse(data.get("viewerState")),
        )


# --- Admin permissions ----------------------------------------------------


@dataclass
class PermissionScope:
    kind: PermissionScopeKind = PermissionScopeKind.UNSPECIFIED
    id: str = ""

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> PermissionScope | None:
        if not data:
            return None
        return cls(
            kind=_parse_enum(  # type: ignore[arg-type]
                PermissionScopeKind, data.get("kind"), PermissionScopeKind.UNSPECIFIED
            ),
            id=data.get("id", ""),
        )


@dataclass
class ScopedPermissionDecision:
    permission: str = ""
    scope: PermissionScope | None = None
    override: PermissionDecision = PermissionDecision.UNSPECIFIED
    effective: PermissionDecision = PermissionDecision.UNSPECIFIED

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> ScopedPermissionDecision | None:
        if not data:
            return None
        return cls(
            permission=data.get("permission", ""),
            scope=PermissionScope.parse(data.get("scope")),
            override=_parse_enum(  # type: ignore[arg-type]
                PermissionDecision, data.get("override"), PermissionDecision.UNSPECIFIED
            ),
            effective=_parse_enum(  # type: ignore[arg-type]
                PermissionDecision, data.get("effective"), PermissionDecision.UNSPECIFIED
            ),
        )


@dataclass
class PermissionMatrixScope:
    id: str = ""
    label: str = ""
    kind: PermissionScopeKind = PermissionScopeKind.UNSPECIFIED
    parent_group_id: str = ""

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> PermissionMatrixScope | None:
        if not data:
            return None
        return cls(
            id=data.get("id", ""),
            label=data.get("label", ""),
            kind=_parse_enum(  # type: ignore[arg-type]
                PermissionScopeKind, data.get("kind"), PermissionScopeKind.UNSPECIFIED
            ),
            parent_group_id=data.get("parentGroupId", ""),
        )


@dataclass
class PermissionMatrixCell:
    permission: str = ""
    scope_id: str = ""
    override: PermissionDecision = PermissionDecision.UNSPECIFIED
    effective: PermissionDecision = PermissionDecision.UNSPECIFIED

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> PermissionMatrixCell | None:
        if not data:
            return None
        return cls(
            permission=data.get("permission", ""),
            scope_id=data.get("scopeId", ""),
            override=_parse_enum(  # type: ignore[arg-type]
                PermissionDecision, data.get("override"), PermissionDecision.UNSPECIFIED
            ),
            effective=_parse_enum(  # type: ignore[arg-type]
                PermissionDecision, data.get("effective"), PermissionDecision.UNSPECIFIED
            ),
        )


@dataclass
class RolePermissionDecisions:
    """Resource-oriented permission decisions for one role.

    Maps to ``ListRolePermissionDecisionsResponse``.
    """

    role_name: str = ""
    decisions: list[ScopedPermissionDecision] = field(default_factory=list)

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> RolePermissionDecisions:
        data = data or {}
        decisions = [
            d
            for raw in data.get("decisions") or []
            if (d := ScopedPermissionDecision.parse(raw)) is not None
        ]
        return cls(
            role_name=data.get("roleName", ""),
            decisions=decisions,
        )


@dataclass
class UserPermissionDecisions:
    """Resource-oriented permission decisions for one user.

    Maps to ``ListUserPermissionDecisionsResponse``.
    """

    user_id: str = ""
    decisions: list[ScopedPermissionDecision] = field(default_factory=list)

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> UserPermissionDecisions:
        data = data or {}
        decisions = [
            d
            for raw in data.get("decisions") or []
            if (d := ScopedPermissionDecision.parse(raw)) is not None
        ]
        return cls(
            user_id=data.get("userId", ""),
            decisions=decisions,
        )


@dataclass
class RolePermissionMatrix:
    """Permission matrix for one role across all scopes (``RolePermissionMatrix``)."""

    role_name: str = ""
    applicable_permissions: list[str] = field(default_factory=list)
    scopes: list[PermissionMatrixScope] = field(default_factory=list)
    cells: list[PermissionMatrixCell] = field(default_factory=list)

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> RolePermissionMatrix | None:
        if not data:
            return None
        scopes = [
            s
            for raw in data.get("scopes") or []
            if (s := PermissionMatrixScope.parse(raw)) is not None
        ]
        cells = [
            c
            for raw in data.get("cells") or []
            if (c := PermissionMatrixCell.parse(raw)) is not None
        ]
        return cls(
            role_name=data.get("roleName", ""),
            applicable_permissions=list(data.get("applicablePermissions") or []),
            scopes=scopes,
            cells=cells,
        )


@dataclass
class UserPermissionMatrix:
    """Permission matrix for one user across all scopes (``UserPermissionMatrix``)."""

    user_id: str = ""
    applicable_permissions: list[str] = field(default_factory=list)
    scopes: list[PermissionMatrixScope] = field(default_factory=list)
    cells: list[PermissionMatrixCell] = field(default_factory=list)

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> UserPermissionMatrix | None:
        if not data:
            return None
        scopes = [
            s
            for raw in data.get("scopes") or []
            if (s := PermissionMatrixScope.parse(raw)) is not None
        ]
        cells = [
            c
            for raw in data.get("cells") or []
            if (c := PermissionMatrixCell.parse(raw)) is not None
        ]
        return cls(
            user_id=data.get("userId", ""),
            applicable_permissions=list(data.get("applicablePermissions") or []),
            scopes=scopes,
            cells=cells,
        )


# --- Group G: admin diagnostics tree (diagnostics.proto) -----------------


@dataclass
class AdminProjectionMetric:
    """Projection-specific diagnostic metric (``AdminProjectionMetric``)."""

    name: str = ""
    value: int = 0
    bytes: int = 0

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> AdminProjectionMetric | None:
        if data is None:
            return None
        return cls(
            name=data.get("name", ""),
            value=int(data.get("value", 0)),
            bytes=int(data.get("bytes", 0)),
        )


@dataclass
class AdminProjectionState:
    """Event-sourced projection health and memory estimates (``AdminProjectionState``)."""

    key: str = ""
    name: str = ""
    subjects: list[str] = field(default_factory=list)
    started: bool = False
    startup_duration_seconds: float | None = None
    last_applied_sequence: str = ""
    matching_stream_sequence: str = ""
    stream_last_sequence: str = ""
    lag: int = 0
    failed: bool = False
    failed_sequence: str = ""
    failure: str = ""
    entry_count: int = 0
    estimated_bytes: int = 0
    average_entry_bytes: int = 0
    metrics: list[AdminProjectionMetric] = field(default_factory=list)

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> AdminProjectionState | None:
        if data is None:
            return None
        raw_sds = data.get("startupDurationSeconds")
        metrics = [
            m
            for raw in data.get("metrics") or []
            if (m := AdminProjectionMetric.parse(raw)) is not None
        ]
        return cls(
            key=data.get("key", ""),
            name=data.get("name", ""),
            subjects=list(data.get("subjects") or []),
            started=bool(data.get("started", False)),
            startup_duration_seconds=(
                float(raw_sds) if raw_sds is not None else None
            ),
            last_applied_sequence=data.get("lastAppliedSequence", ""),
            matching_stream_sequence=data.get("matchingStreamSequence", ""),
            stream_last_sequence=data.get("streamLastSequence", ""),
            lag=int(data.get("lag", 0)),
            failed=bool(data.get("failed", False)),
            failed_sequence=data.get("failedSequence", ""),
            failure=data.get("failure", ""),
            entry_count=int(data.get("entryCount", 0)),
            estimated_bytes=int(data.get("estimatedBytes", 0)),
            average_entry_bytes=int(data.get("averageEntryBytes", 0)),
            metrics=metrics,
        )


@dataclass
class AdminConnectionInfo:
    """Current NATS server connection metadata (``AdminConnectionInfo``)."""

    connected: bool = False
    server_id: str = ""
    server_name: str = ""
    version: str = ""
    max_payload: int = 0
    rtt: str = ""

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> AdminConnectionInfo | None:
        if data is None:
            return None
        return cls(
            connected=bool(data.get("connected", False)),
            server_id=data.get("serverId", ""),
            server_name=data.get("serverName", ""),
            version=data.get("version", ""),
            max_payload=int(data.get("maxPayload", 0)),
            rtt=data.get("rtt", ""),
        )


@dataclass
class AdminAccountInfo:
    """JetStream account limits and usage (``AdminAccountInfo``)."""

    memory: int = 0
    memory_used: int = 0
    storage: int = 0
    storage_used: int = 0
    streams: int = 0
    streams_used: int = 0
    consumers: int = 0
    consumers_used: int = 0

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> AdminAccountInfo | None:
        if data is None:
            return None
        return cls(
            memory=int(data.get("memory", 0)),
            memory_used=int(data.get("memoryUsed", 0)),
            storage=int(data.get("storage", 0)),
            storage_used=int(data.get("storageUsed", 0)),
            streams=int(data.get("streams", 0)),
            streams_used=int(data.get("streamsUsed", 0)),
            consumers=int(data.get("consumers", 0)),
            consumers_used=int(data.get("consumersUsed", 0)),
        )


@dataclass
class AdminServerStats:
    """Aggregate Chatto object counts (``AdminServerStats``)."""

    user_count: int = 0
    channel_room_count: int = 0
    dm_room_count: int = 0

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> AdminServerStats | None:
        if data is None:
            return None
        return cls(
            user_count=int(data.get("userCount", 0)),
            channel_room_count=int(data.get("channelRoomCount", 0)),
            dm_room_count=int(data.get("dmRoomCount", 0)),
        )


@dataclass
class AdminNatsStreamInfo:
    """JetStream stream state (``AdminNatsStreamInfo``)."""

    name: str = ""
    description: str = ""
    subjects: list[str] = field(default_factory=list)
    storage: str = ""
    messages: int = 0
    bytes: int = 0
    first_sequence: str = ""
    last_sequence: str = ""
    consumer_count: int = 0
    replicas: int = 0
    cluster_leader: str = ""

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> AdminNatsStreamInfo | None:
        if data is None:
            return None
        return cls(
            name=data.get("name", ""),
            description=data.get("description", ""),
            subjects=list(data.get("subjects") or []),
            storage=data.get("storage", ""),
            messages=int(data.get("messages", 0)),
            bytes=int(data.get("bytes", 0)),
            first_sequence=data.get("firstSequence", ""),
            last_sequence=data.get("lastSequence", ""),
            consumer_count=int(data.get("consumerCount", 0)),
            replicas=int(data.get("replicas", 0)),
            cluster_leader=data.get("clusterLeader", ""),
        )


@dataclass
class AdminNatsConsumerInfo:
    """JetStream consumer state (``AdminNatsConsumerInfo``)."""

    stream: str = ""
    name: str = ""
    durable: str = ""
    filter_subject: str = ""
    filter_subjects: list[str] = field(default_factory=list)
    ack_policy: str = ""
    pull_based: bool = False
    push_bound: bool = False
    pending: int = 0
    ack_pending: int = 0
    redelivered: int = 0
    waiting: int = 0
    delivered_consumer_sequence: str = ""
    delivered_stream_sequence: str = ""
    ack_floor_consumer_sequence: str = ""
    ack_floor_stream_sequence: str = ""

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> AdminNatsConsumerInfo | None:
        if data is None:
            return None
        return cls(
            stream=data.get("stream", ""),
            name=data.get("name", ""),
            durable=data.get("durable", ""),
            filter_subject=data.get("filterSubject", ""),
            filter_subjects=list(data.get("filterSubjects") or []),
            ack_policy=data.get("ackPolicy", ""),
            pull_based=bool(data.get("pullBased", False)),
            push_bound=bool(data.get("pushBound", False)),
            pending=int(data.get("pending", 0)),
            ack_pending=int(data.get("ackPending", 0)),
            redelivered=int(data.get("redelivered", 0)),
            waiting=int(data.get("waiting", 0)),
            delivered_consumer_sequence=data.get("deliveredConsumerSequence", ""),
            delivered_stream_sequence=data.get("deliveredStreamSequence", ""),
            ack_floor_consumer_sequence=data.get("ackFloorConsumerSequence", ""),
            ack_floor_stream_sequence=data.get("ackFloorStreamSequence", ""),
        )


@dataclass
class AdminNatsStats:
    """JetStream stream and consumer diagnostics (``AdminNatsStats``)."""

    total_messages: int = 0
    total_bytes: int = 0
    total_consumer_pending: int = 0
    total_ack_pending: int = 0
    streams: list[AdminNatsStreamInfo] = field(default_factory=list)
    consumers: list[AdminNatsConsumerInfo] = field(default_factory=list)

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> AdminNatsStats | None:
        if data is None:
            return None
        streams = [
            s
            for raw in data.get("streams") or []
            if (s := AdminNatsStreamInfo.parse(raw)) is not None
        ]
        consumers = [
            c
            for raw in data.get("consumers") or []
            if (c := AdminNatsConsumerInfo.parse(raw)) is not None
        ]
        return cls(
            total_messages=int(data.get("totalMessages", 0)),
            total_bytes=int(data.get("totalBytes", 0)),
            total_consumer_pending=int(data.get("totalConsumerPending", 0)),
            total_ack_pending=int(data.get("totalAckPending", 0)),
            streams=streams,
            consumers=consumers,
        )


@dataclass
class AdminSystemInfo:
    """Broker and application diagnostics (``AdminSystemInfo``)."""

    connection: AdminConnectionInfo | None = None
    account: AdminAccountInfo | None = None
    nats: AdminNatsStats | None = None
    stats: AdminServerStats | None = None

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> AdminSystemInfo | None:
        if data is None:
            return None
        return cls(
            connection=AdminConnectionInfo.parse(data.get("connection")),
            account=AdminAccountInfo.parse(data.get("account")),
            nats=AdminNatsStats.parse(data.get("nats")),
            stats=AdminServerStats.parse(data.get("stats")),
        )


@dataclass
class AdminSystemInfoSnapshot:
    """Envelope for ``GetSystemInfoResponse``.

    Contains broker/JetStream diagnostics and projection health snapshots.
    """

    system_info: AdminSystemInfo | None = None
    projections: list[AdminProjectionState] = field(default_factory=list)

    @classmethod
    def parse(cls, data: dict[str, Any] | None) -> AdminSystemInfoSnapshot:
        data = data or {}
        projections = [
            p
            for raw in data.get("projections") or []
            if (p := AdminProjectionState.parse(raw)) is not None
        ]
        return cls(
            system_info=AdminSystemInfo.parse(data.get("systemInfo")),
            projections=projections,
        )
