"""Dexster Bot API data types — mirrors the Telegram Bot API structure."""

from __future__ import annotations

from typing import TYPE_CHECKING, Any, Optional, Union

from pydantic import BaseModel, ConfigDict, Field, PrivateAttr

if TYPE_CHECKING:
    pass  # Forward reference for bot type (used as Any at runtime)


class User(BaseModel):
    """Represents a Dexster user or bot."""

    id: int
    is_bot: bool = False
    first_name: Optional[str] = None  # May be absent in Dexster (uses displayName/username instead)
    last_name: Optional[str] = None
    username: Optional[str] = None


class Chat(BaseModel):
    """Represents a Dexster chat (DM, group, or channel)."""

    id: int
    type: Optional[str] = None  # "private", "group", "channel" — may be absent in message responses
    title: Optional[str] = None
    username: Optional[str] = None
    first_name: Optional[str] = None


class BotInfo(BaseModel):
    """Bot identity returned by getMe."""

    id: int
    bot_id: Optional[int] = None
    username: Optional[str] = None
    name: Optional[str] = None
    description: Optional[str] = None
    short_description: Optional[str] = None
    about_text: Optional[str] = None
    is_verified: bool = False
    is_native: bool = False


class PollOption(BaseModel):
    """A single option in a poll."""

    text: str
    voter_count: int = 0


class Poll(BaseModel):
    """Represents a poll."""

    id: Union[str, int]
    question: str
    options: list[PollOption]
    total_voter_count: int = 0
    is_closed: bool = False
    is_anonymous: bool = True
    type: str = "regular"
    allows_multiple_answers: bool = False
    correct_option_id: Optional[int] = None
    explanation: Optional[str] = None


class PollAnswer(BaseModel):
    """Incoming poll vote."""

    poll_id: str
    user: User
    option_ids: list[int]


class Dice(BaseModel):
    """Dice roll result."""

    emoji: str = "\U0001f3b2"
    value: int


class Contact(BaseModel):
    """Contact information."""

    phone_number: str
    first_name: str
    last_name: Optional[str] = None
    vcard: Optional[str] = None


class Location(BaseModel):
    """Geographic location."""

    latitude: float
    longitude: float
    live_period: Optional[int] = None          # Duration in seconds (60-86400)
    heading: Optional[int] = None              # Direction 1-360
    horizontal_accuracy: Optional[float] = None  # Meters 0-1500
    proximity_alert_radius: Optional[int] = None


class Venue(BaseModel):
    """Venue information."""

    location: Location
    title: str
    address: str
    foursquare_id: Optional[str] = None
    foursquare_type: Optional[str] = None


class ChatPermissions(BaseModel):
    """Default chat permissions."""

    can_send_messages: Optional[bool] = None
    can_send_media: Optional[bool] = None
    can_send_stickers: Optional[bool] = None
    can_send_polls: Optional[bool] = None
    can_send_links: Optional[bool] = None
    can_send_files: Optional[bool] = None
    can_add_members: Optional[bool] = None
    can_pin_messages: Optional[bool] = None
    can_change_info: Optional[bool] = None


class ChatMember(BaseModel):
    """Chat member with status and permissions."""

    user: User
    status: str  # "creator", "administrator", "member", "restricted", "left", "kicked"
    custom_title: Optional[str] = None
    is_anonymous: Optional[bool] = None
    can_change_info: Optional[bool] = None
    can_delete_messages: Optional[bool] = None
    can_ban_users: Optional[bool] = None
    can_invite_users: Optional[bool] = None
    can_pin_messages: Optional[bool] = None
    can_promote_members: Optional[bool] = None


class ChatInfo(BaseModel):
    """Full chat info returned by getChat."""

    id: int
    type: str
    title: Optional[str] = None
    username: Optional[str] = None
    description: Optional[str] = None
    member_count: Optional[int] = None
    permissions: Optional[ChatPermissions] = None


class WebAppInfo(BaseModel):
    """Web app URL container."""

    url: str


class MenuButton(BaseModel):
    """Menu button configuration."""

    type: str  # "commands", "web_app", "default"
    text: Optional[str] = None
    web_app: Optional[WebAppInfo] = None


class InlineQuery(BaseModel):
    """Incoming inline query."""

    id: str
    from_user: User = Field(..., alias="from")
    query: str = ""
    offset: str = ""
    chat_type: Optional[str] = None

    model_config = {"populate_by_name": True}


class InlineQueryResultArticle(BaseModel):
    """Article inline query result."""

    type: str = "article"
    id: str
    title: str
    input_message_content: dict[str, Any]
    reply_markup: Optional[dict[str, Any]] = None
    description: Optional[str] = None
    thumbnail_url: Optional[str] = None


class InlineQueryResultPhoto(BaseModel):
    """Photo inline query result."""

    type: str = "photo"
    id: str
    photo_url: str
    thumbnail_url: str
    title: Optional[str] = None
    description: Optional[str] = None
    caption: Optional[str] = None
    reply_markup: Optional[dict[str, Any]] = None


# ── InputMessageContent models ────────────────────────────────────────


class InputTextMessageContent(BaseModel):
    """Text message content for inline results."""

    message_text: str
    parse_mode: Optional[str] = None


class InputLocationMessageContent(BaseModel):
    """Location message content for inline results."""

    latitude: float
    longitude: float


class InputVenueMessageContent(BaseModel):
    """Venue message content for inline results."""

    latitude: float
    longitude: float
    title: str
    address: str
    foursquare_id: Optional[str] = None
    foursquare_type: Optional[str] = None


class InputContactMessageContent(BaseModel):
    """Contact message content for inline results."""

    phone_number: str
    first_name: str
    last_name: Optional[str] = None
    vcard: Optional[str] = None


# ── URL-based InlineQueryResult models ────────────────────────────────


class InlineQueryResultGif(BaseModel):
    """GIF inline query result."""

    type: str = "gif"
    id: str
    gif_url: str
    thumbnail_url: str
    gif_width: Optional[int] = None
    gif_height: Optional[int] = None
    gif_duration: Optional[int] = None
    title: Optional[str] = None
    caption: Optional[str] = None
    parse_mode: Optional[str] = None
    reply_markup: Optional[dict[str, Any]] = None
    input_message_content: Optional[dict[str, Any]] = None


class InlineQueryResultMpeg4Gif(BaseModel):
    """MPEG4 GIF inline query result."""

    type: str = "mpeg4_gif"
    id: str
    mpeg4_url: str
    thumbnail_url: str
    mpeg4_width: Optional[int] = None
    mpeg4_height: Optional[int] = None
    mpeg4_duration: Optional[int] = None
    title: Optional[str] = None
    caption: Optional[str] = None
    parse_mode: Optional[str] = None
    reply_markup: Optional[dict[str, Any]] = None
    input_message_content: Optional[dict[str, Any]] = None


class InlineQueryResultVideo(BaseModel):
    """Video inline query result."""

    type: str = "video"
    id: str
    video_url: str
    mime_type: str
    thumbnail_url: str
    title: str
    caption: Optional[str] = None
    description: Optional[str] = None
    video_width: Optional[int] = None
    video_height: Optional[int] = None
    video_duration: Optional[int] = None
    parse_mode: Optional[str] = None
    reply_markup: Optional[dict[str, Any]] = None
    input_message_content: Optional[dict[str, Any]] = None


class InlineQueryResultAudio(BaseModel):
    """Audio inline query result."""

    type: str = "audio"
    id: str
    audio_url: str
    title: str
    caption: Optional[str] = None
    performer: Optional[str] = None
    audio_duration: Optional[int] = None
    parse_mode: Optional[str] = None
    reply_markup: Optional[dict[str, Any]] = None
    input_message_content: Optional[dict[str, Any]] = None


class InlineQueryResultVoice(BaseModel):
    """Voice inline query result."""

    type: str = "voice"
    id: str
    voice_url: str
    title: str
    caption: Optional[str] = None
    voice_duration: Optional[int] = None
    parse_mode: Optional[str] = None
    reply_markup: Optional[dict[str, Any]] = None
    input_message_content: Optional[dict[str, Any]] = None


class InlineQueryResultDocument(BaseModel):
    """Document inline query result."""

    type: str = "document"
    id: str
    title: str
    document_url: str
    mime_type: str
    caption: Optional[str] = None
    description: Optional[str] = None
    parse_mode: Optional[str] = None
    reply_markup: Optional[dict[str, Any]] = None
    input_message_content: Optional[dict[str, Any]] = None


class InlineQueryResultLocation(BaseModel):
    """Location inline query result."""

    type: str = "location"
    id: str
    latitude: float
    longitude: float
    title: str
    thumbnail_url: Optional[str] = None
    thumbnail_width: Optional[int] = None
    thumbnail_height: Optional[int] = None
    reply_markup: Optional[dict[str, Any]] = None
    input_message_content: Optional[dict[str, Any]] = None


class InlineQueryResultVenue(BaseModel):
    """Venue inline query result."""

    type: str = "venue"
    id: str
    latitude: float
    longitude: float
    title: str
    address: str
    foursquare_id: Optional[str] = None
    foursquare_type: Optional[str] = None
    thumbnail_url: Optional[str] = None
    thumbnail_width: Optional[int] = None
    thumbnail_height: Optional[int] = None
    reply_markup: Optional[dict[str, Any]] = None
    input_message_content: Optional[dict[str, Any]] = None


class InlineQueryResultContact(BaseModel):
    """Contact inline query result."""

    type: str = "contact"
    id: str
    phone_number: str
    first_name: str
    last_name: Optional[str] = None
    thumbnail_url: Optional[str] = None
    thumbnail_width: Optional[int] = None
    thumbnail_height: Optional[int] = None
    reply_markup: Optional[dict[str, Any]] = None
    input_message_content: Optional[dict[str, Any]] = None


# ── Cached InlineQueryResult models ───────────────────────────────────


class InlineQueryResultCachedPhoto(BaseModel):
    """Cached photo inline query result (uses file_id)."""

    type: str = "cached_photo"
    id: str
    photo_file_id: str
    title: Optional[str] = None
    description: Optional[str] = None
    caption: Optional[str] = None
    parse_mode: Optional[str] = None
    reply_markup: Optional[dict[str, Any]] = None
    input_message_content: Optional[dict[str, Any]] = None


class InlineQueryResultCachedGif(BaseModel):
    """Cached GIF inline query result (uses file_id)."""

    type: str = "cached_gif"
    id: str
    gif_file_id: str
    title: Optional[str] = None
    caption: Optional[str] = None
    parse_mode: Optional[str] = None
    reply_markup: Optional[dict[str, Any]] = None
    input_message_content: Optional[dict[str, Any]] = None


class InlineQueryResultCachedMpeg4Gif(BaseModel):
    """Cached MPEG4 GIF inline query result (uses file_id)."""

    type: str = "cached_mpeg4_gif"
    id: str
    mpeg4_file_id: str
    title: Optional[str] = None
    caption: Optional[str] = None
    parse_mode: Optional[str] = None
    reply_markup: Optional[dict[str, Any]] = None
    input_message_content: Optional[dict[str, Any]] = None


class InlineQueryResultCachedSticker(BaseModel):
    """Cached sticker inline query result (uses file_id)."""

    type: str = "cached_sticker"
    id: str
    sticker_file_id: str
    reply_markup: Optional[dict[str, Any]] = None
    input_message_content: Optional[dict[str, Any]] = None


class InlineQueryResultCachedDocument(BaseModel):
    """Cached document inline query result (uses file_id)."""

    type: str = "cached_document"
    id: str
    title: str
    document_file_id: str
    description: Optional[str] = None
    caption: Optional[str] = None
    parse_mode: Optional[str] = None
    reply_markup: Optional[dict[str, Any]] = None
    input_message_content: Optional[dict[str, Any]] = None


class InlineQueryResultCachedVideo(BaseModel):
    """Cached video inline query result (uses file_id)."""

    type: str = "cached_video"
    id: str
    video_file_id: str
    title: str
    description: Optional[str] = None
    caption: Optional[str] = None
    parse_mode: Optional[str] = None
    reply_markup: Optional[dict[str, Any]] = None
    input_message_content: Optional[dict[str, Any]] = None


class InlineQueryResultCachedVoice(BaseModel):
    """Cached voice inline query result (uses file_id)."""

    type: str = "cached_voice"
    id: str
    voice_file_id: str
    title: str
    caption: Optional[str] = None
    parse_mode: Optional[str] = None
    reply_markup: Optional[dict[str, Any]] = None
    input_message_content: Optional[dict[str, Any]] = None


class InlineQueryResultCachedAudio(BaseModel):
    """Cached audio inline query result (uses file_id)."""

    type: str = "cached_audio"
    id: str
    audio_file_id: str
    caption: Optional[str] = None
    parse_mode: Optional[str] = None
    reply_markup: Optional[dict[str, Any]] = None
    input_message_content: Optional[dict[str, Any]] = None


class WebhookInfo(BaseModel):
    """Webhook configuration info."""

    url: str = ""
    has_custom_certificate: bool = False
    pending_update_count: int = 0
    ip_address: Optional[str] = None
    last_error_date: Optional[int] = None
    last_error_message: Optional[str] = None
    max_connections: Optional[int] = None
    allowed_updates: Optional[list[str]] = None


class ChatMemberUpdated(BaseModel):
    """Member status change event."""

    chat: Chat
    from_user: User = Field(..., alias="from")
    date: int
    old_chat_member: ChatMember
    new_chat_member: ChatMember

    model_config = {"populate_by_name": True}


class ReactionType(BaseModel):
    """A single reaction (emoji-based)."""

    type: str = "emoji"
    emoji: str


class ChosenInlineResult(BaseModel):
    """Result of an inline query that was chosen by a user and sent to their chat partner."""

    result_id: str
    from_user: User = Field(..., alias="from")
    query: str = ""
    inline_message_id: Optional[str] = None

    model_config = {"populate_by_name": True}


class MessageReactionUpdated(BaseModel):
    """Reaction changes on a message."""

    chat: Chat
    message_id: int
    user: User
    date: int
    old_reaction: list[ReactionType] = []
    new_reaction: list[ReactionType] = []


class ChatJoinRequest(BaseModel):
    """Join request from a user to a chat."""

    chat: Chat
    from_user: User = Field(..., alias="from")
    date: int
    bio: Optional[str] = None
    invite_link: Optional[dict[str, Any]] = None

    model_config = {"populate_by_name": True}


class ComponentInteraction(BaseModel):
    """Represents a component interaction (button click, form submit, select change)."""

    component_id: str
    interaction_type: str
    values: Optional[dict[str, Any]] = None
    message_id: Optional[int] = None
    chat: Optional[Chat] = None
    from_user: Optional[User] = Field(None, alias="from")
    model_config = ConfigDict(populate_by_name=True)


# ── Media type models ──────────────────────────────────────────────────


class PhotoSize(BaseModel):
    """Represents one size of a photo or a file/sticker thumbnail."""

    file_id: str
    file_unique_id: Optional[str] = None
    width: int = 0
    height: int = 0
    file_size: Optional[int] = None


class Audio(BaseModel):
    """Represents an audio file."""

    file_id: str
    file_unique_id: Optional[str] = None
    duration: int = 0
    performer: Optional[str] = None
    title: Optional[str] = None
    file_name: Optional[str] = None
    mime_type: Optional[str] = None
    file_size: Optional[int] = None


class Video(BaseModel):
    """Represents a video file."""

    file_id: str
    file_unique_id: Optional[str] = None
    width: int = 0
    height: int = 0
    duration: int = 0
    file_name: Optional[str] = None
    mime_type: Optional[str] = None
    file_size: Optional[int] = None


class Document(BaseModel):
    """Represents a general file (not photo/audio/video/voice)."""

    file_id: str
    file_unique_id: Optional[str] = None
    file_name: Optional[str] = None
    mime_type: Optional[str] = None
    file_size: Optional[int] = None


class Animation(BaseModel):
    """Represents an animation file (GIF or H.264/MPEG-4 AVC without sound)."""

    file_id: str
    file_unique_id: Optional[str] = None
    width: int = 0
    height: int = 0
    duration: int = 0
    file_name: Optional[str] = None
    mime_type: Optional[str] = None
    file_size: Optional[int] = None


class Voice(BaseModel):
    """Represents a voice note."""

    file_id: str
    file_unique_id: Optional[str] = None
    duration: int = 0
    mime_type: Optional[str] = None
    file_size: Optional[int] = None


class Sticker(BaseModel):
    """Represents a sticker."""

    file_id: str
    file_unique_id: Optional[str] = None
    type: str = "regular"
    width: int = 0
    height: int = 0
    emoji: Optional[str] = None
    set_name: Optional[str] = None
    file_size: Optional[int] = None


class VideoNote(BaseModel):
    """Represents a video message (round video)."""

    file_id: str
    file_unique_id: Optional[str] = None
    length: int = 0
    duration: int = 0
    file_size: Optional[int] = None


class Message(BaseModel):
    """Represents a message in a Dexster chat.

    When returned from bot API methods, the message is bound to a bot instance
    enabling convenience methods like .reply(), .edit(), .delete().
    """

    message_id: int
    date: int = 0  # May be absent in edit responses
    chat: Chat
    from_user: Optional[User] = Field(None, alias="from")
    text: Optional[str] = None
    entities: Optional[list[dict[str, Any]]] = None
    reply_to_message: Optional[Message] = None
    poll: Optional[Poll] = None
    dice: Optional[Dice] = None
    contact: Optional[Contact] = None
    venue: Optional[Venue] = None
    location: Optional[Location] = None
    media_group_id: Optional[str] = None
    caption: Optional[str] = None

    # Media fields
    photo: Optional[list[PhotoSize]] = None
    video: Optional[Video] = None
    audio: Optional[Audio] = None
    document: Optional[Document] = None
    animation: Optional[Animation] = None
    voice: Optional[Voice] = None
    sticker: Optional[Sticker] = None
    video_note: Optional[VideoNote] = None

    # Forward fields
    forward_from: Optional[User] = None
    forward_date: Optional[int] = None

    # Additional metadata
    reply_markup: Optional[dict[str, Any]] = None

    _bot: Any = PrivateAttr(default=None)

    model_config = {"populate_by_name": True, "arbitrary_types_allowed": True}

    def _bind(self, bot: Any) -> Message:
        """Bind a bot instance for convenience methods. Returns self for chaining."""
        object.__setattr__(self, "_bot", bot)
        return self

    async def reply(
        self,
        text: str,
        reply_markup: Optional[Any] = None,
        **kwargs: Any,
    ) -> Message:
        """Reply to this message.

        Args:
            text: Text of the reply message.
            reply_markup: Optional keyboard markup.
            **kwargs: Additional parameters passed to send_message.

        Returns:
            The sent Message, bound to the same bot.

        Raises:
            RuntimeError: If the message is not bound to a bot instance.
        """
        if self._bot is None:
            raise RuntimeError("Message not bound to a bot instance")
        return await self._bot.send_message(
            self.chat.id,
            text,
            reply_to_message_id=self.message_id,
            reply_markup=reply_markup,
            **kwargs,
        )

    async def edit(
        self,
        text: str,
        reply_markup: Optional[Any] = None,
        **kwargs: Any,
    ) -> Message:
        """Edit this message's text.

        Args:
            text: New text for the message.
            reply_markup: Optional new keyboard markup.
            **kwargs: Additional parameters passed to edit_message_text.

        Returns:
            The edited Message, bound to the same bot.

        Raises:
            RuntimeError: If the message is not bound to a bot instance.
        """
        if self._bot is None:
            raise RuntimeError("Message not bound to a bot instance")
        return await self._bot.edit_message_text(
            self.chat.id,
            self.message_id,
            text,
            reply_markup=reply_markup,
            **kwargs,
        )

    async def delete(self) -> bool:
        """Delete this message.

        Returns:
            True if the message was deleted successfully.

        Raises:
            RuntimeError: If the message is not bound to a bot instance.
        """
        if self._bot is None:
            raise RuntimeError("Message not bound to a bot instance")
        return await self._bot.delete_message(self.chat.id, self.message_id)

    async def reply_photo(
        self,
        photo_url: str,
        caption: Optional[str] = None,
        **kwargs: Any,
    ) -> Message:
        """Reply to this message with a photo.

        Args:
            photo_url: URL of the photo to send.
            caption: Optional caption for the photo.
            **kwargs: Additional parameters passed to send_photo.

        Returns:
            The sent Message, bound to the same bot.

        Raises:
            RuntimeError: If the message is not bound to a bot instance.
        """
        if self._bot is None:
            raise RuntimeError("Message not bound to a bot instance")
        return await self._bot.send_photo(
            self.chat.id,
            photo_url,
            caption=caption,
            reply_to_message_id=self.message_id,
            **kwargs,
        )


class CallbackQuery(BaseModel):
    """Represents an incoming callback query from an inline keyboard button press.

    When returned from bot API methods, the callback query is bound to a bot
    instance enabling the .answer() convenience method.
    """

    id: str
    from_user: User = Field(..., alias="from")
    message: Optional[Message] = None
    chat_instance: Optional[str] = None
    data: Optional[str] = None

    _bot: Any = PrivateAttr(default=None)

    model_config = {"populate_by_name": True, "arbitrary_types_allowed": True}

    def _bind(self, bot: Any) -> CallbackQuery:
        """Bind a bot instance for convenience methods. Returns self for chaining."""
        object.__setattr__(self, "_bot", bot)
        return self

    async def answer(
        self,
        text: Optional[str] = None,
        show_alert: bool = False,
    ) -> bool:
        """Answer this callback query.

        Args:
            text: Optional text to show as a notification or alert.
            show_alert: If True, show as alert popup instead of notification.

        Returns:
            True if the callback query was answered successfully.

        Raises:
            RuntimeError: If the callback query is not bound to a bot instance.
        """
        if self._bot is None:
            raise RuntimeError("CallbackQuery not bound to a bot instance")
        return await self._bot.answer_callback_query(
            self.id, text=text, show_alert=show_alert,
        )


class Update(BaseModel):
    """Represents an incoming update from the Dexster Bot API."""

    update_id: int
    message: Optional[Message] = None
    edited_message: Optional[Message] = None
    callback_query: Optional[CallbackQuery] = None
    inline_query: Optional[InlineQuery] = None
    my_chat_member: Optional[ChatMemberUpdated] = None
    poll_answer: Optional[PollAnswer] = None
    channel_post: Optional[Message] = None
    edited_channel_post: Optional[Message] = None
    chosen_inline_result: Optional[ChosenInlineResult] = None
    message_reaction: Optional[MessageReactionUpdated] = None
    chat_member: Optional[ChatMemberUpdated] = None
    chat_join_request: Optional[ChatJoinRequest] = None
    poll: Optional[Poll] = None
    component_interaction: Optional[ComponentInteraction] = None

    @property
    def effective_message(self) -> Optional[Message]:
        """Get the message from this update, regardless of type."""
        if self.message:
            return self.message
        if self.edited_message:
            return self.edited_message
        if self.channel_post:
            return self.channel_post
        if self.edited_channel_post:
            return self.edited_channel_post
        if self.callback_query and self.callback_query.message:
            return self.callback_query.message
        return None

    @property
    def effective_chat(self) -> Optional[Chat]:
        """Get the chat from this update."""
        msg = self.effective_message
        if msg:
            return msg.chat
        if self.my_chat_member:
            return self.my_chat_member.chat
        if self.chat_member:
            return self.chat_member.chat
        if self.message_reaction:
            return self.message_reaction.chat
        if self.chat_join_request:
            return self.chat_join_request.chat
        if self.component_interaction and self.component_interaction.chat:
            return self.component_interaction.chat
        return None

    @property
    def effective_user(self) -> Optional[User]:
        """Get the user who triggered this update."""
        if self.callback_query:
            return self.callback_query.from_user
        if self.inline_query:
            return self.inline_query.from_user
        if self.chosen_inline_result:
            return self.chosen_inline_result.from_user
        if self.my_chat_member:
            return self.my_chat_member.from_user
        if self.chat_member:
            return self.chat_member.from_user
        if self.message_reaction:
            return self.message_reaction.user
        if self.chat_join_request:
            return self.chat_join_request.from_user
        if self.component_interaction and self.component_interaction.from_user:
            return self.component_interaction.from_user
        if self.message and self.message.from_user:
            return self.message.from_user
        if self.edited_message and self.edited_message.from_user:
            return self.edited_message.from_user
        if self.channel_post and self.channel_post.from_user:
            return self.channel_post.from_user
        if self.edited_channel_post and self.edited_channel_post.from_user:
            return self.edited_channel_post.from_user
        return None


class BotFile(BaseModel):
    """Represents a file returned by getFile."""

    file_id: str
    file_url: str
    file_size: Optional[int] = None
    file_type: Optional[str] = None
    mime_type: Optional[str] = None


class UserProfilePhotos(BaseModel):
    """Represents a user's profile photos."""

    total_count: int = 0
    photos: list[list[dict[str, Any]]] = []


class ApiResponse(BaseModel):
    """Standard Dexster Bot API response wrapper."""

    ok: bool
    result: Any = None
    error_code: Optional[int] = None
    description: Optional[str] = None
