import logging
from abc import ABC, abstractmethod
from itertools import count
from typing import Final, Iterable, Optional, Tuple, Union

import attr

from .api_types import (APIResponse, BaseTelegram, BotCommand, Chat,
                        ChatMember, ChatPermissions, File, GameHighScore,
                        InlineKeyboardMarkup, InlineQueryResult, InputFile,
                        InputMedia, InputMediaAudio, InputMediaDocument,
                        InputMediaPhoto, InputMediaVideo, LabeledPrice,
                        MaskPosition, Message, MessageEntity, MessageId,
                        PassportElementError, Poll, ReplyMarkup,
                        ShippingOption, StickerSet, Update, User,
                        UserProfilePhotos, WebhookInfo)
from .constants import (ChatAction, DiceEmoji, ParseMode, PollType,
                        RequestMethod, UpdateType)
from .helpers import json_dumps

api_logger: Final[logging.Logger] = logging.getLogger('aiotgbot.api')


def _to_json(
    value: Union[BaseTelegram, Iterable[BaseTelegram], None]
) -> Optional[str]:
    if value is None:
        return None
    elif isinstance(value, Iterable):
        return json_dumps(tuple(item.to_dict() for item in value))
    else:
        return json_dumps(value.to_dict())


def _strs_to_json(value: Optional[Iterable[str]]) -> Optional[str]:
    return json_dumps(tuple(value)) if value is not None else None


def _parse_mode_to_str(parse_mode: Optional[ParseMode]) -> Optional[str]:
    return parse_mode.value if parse_mode is not None else None


def _dice_emoji_to_str(dice_emoji: Optional[DiceEmoji]) -> Optional[str]:
    return dice_emoji.value if dice_emoji is not None else None


ParamType = Union[int, float, str, InputFile, None]


class ApiMethods(ABC):

    @abstractmethod
    async def _request(self, http_method: RequestMethod, api_method: str,
                       **params: ParamType) -> APIResponse: ...

    @abstractmethod
    async def _safe_request(
            self, http_method: RequestMethod, api_method: str,
            chat_id: Union[int, str], **params: ParamType
    ) -> APIResponse: ...

    async def get_updates(
        self, offset: Optional[int] = None,
        limit: Optional[int] = None,
        timeout: Optional[int] = None,
        allowed_updates: Optional[Iterable[UpdateType]] = None
    ) -> Tuple[Update, ...]:
        api_logger.debug(f'Get updates offset: {offset}, limit: {limit}, '
                         f'timeout: {timeout}, '
                         f'allowed_updates: {allowed_updates}')
        response = await self._request(
            RequestMethod.GET, 'getUpdates', offset=offset, limit=limit,
            timeout=timeout, allowed_updates=_strs_to_json(allowed_updates))

        return tuple(Update.from_dict(item) for item in response.result)

    async def set_webhook(
        self, url: Optional[str] = None,
        certificate: Optional[InputFile] = None,
        ip_address: Optional[str] = None,
        max_connections: Optional[int] = None,
        allowed_updates: Optional[Iterable[UpdateType]] = None,
        drop_pending_updates: Optional[bool] = None
    ) -> bool:
        api_logger.debug('Set webhook')
        response = await self._request(
            RequestMethod.POST, 'setWebhook', url=url, certificate=certificate,
            ip_address=ip_address, max_connections=max_connections,
            allowed_updates=_strs_to_json(allowed_updates),
            drop_pending_updates=drop_pending_updates)

        return response.result

    async def delete_webhook(
        self, drop_pending_updates: Optional[bool]
    ) -> bool:
        api_logger.debug('Delete webhook')
        response = await self._request(
            RequestMethod.POST, 'deleteWebhook',
            drop_pending_updates=drop_pending_updates)
        return response.result

    async def get_webhook_info(self) -> WebhookInfo:
        api_logger.debug('Get webhook info')
        response = await self._request(RequestMethod.GET, 'getWebhookInfo')

        return WebhookInfo.from_dict(response.result)

    async def get_me(self) -> User:
        api_logger.debug('Get me')
        response = await self._request(RequestMethod.GET, 'getMe')
        return User.from_dict(response.result)

    async def log_out(self) -> bool:
        api_logger.debug('Log out')
        response = await self._request(RequestMethod.POST, 'logOut')
        return response.result

    async def close(self) -> bool:
        api_logger.debug('Close')
        response = await self._request(RequestMethod.POST, 'close')
        return response.result

    async def send_message(
        self, chat_id: Union[int, str], text: str,
        parse_mode: Optional[ParseMode] = None,
        entities: Optional[Iterable[MessageEntity]] = None,
        disable_web_page_preview: Optional[bool] = None,
        disable_notification: Optional[bool] = None,
        reply_to_message_id: Optional[int] = None,
        allow_sending_without_reply: Optional[bool] = None,
        reply_markup: Optional[ReplyMarkup] = None
    ) -> Message:
        api_logger.debug('Send message "%s" to chat "%s"', text, chat_id)
        response = await self._safe_request(
            RequestMethod.POST, 'sendMessage', chat_id, text=text,
            parse_mode=_parse_mode_to_str(parse_mode),
            entities=_to_json(entities),
            disable_web_page_preview=disable_web_page_preview,
            disable_notification=disable_notification,
            reply_to_message_id=reply_to_message_id,
            allow_sending_without_reply=allow_sending_without_reply,
            reply_markup=_to_json(reply_markup))

        return Message.from_dict(response.result)

    async def forward_message(
        self, chat_id: Union[int, str], from_chat_id: Union[int, str],
        message_id: int, disable_notification: Optional[bool] = None
    ) -> Message:
        api_logger.debug('Forward message %s to "%s" from "%s"', message_id,
                         chat_id, from_chat_id)
        response = await self._safe_request(
            RequestMethod.POST, 'forwardMessage', chat_id,
            from_chat_id=from_chat_id, message_id=message_id,
            disable_notification=disable_notification)

        return Message.from_dict(response.result)

    async def copy_message(
        self, chat_id: Union[int, str], from_chat_id: Union[int, str],
        message_id: int, caption: Optional[str] = None,
        parse_mode: Optional[ParseMode] = None,
        caption_entities: Optional[Iterable[MessageEntity]] = None,
        disable_notification: Optional[bool] = None,
        reply_to_message_id: Optional[int] = None,
        allow_sending_without_reply: Optional[bool] = None,
        reply_markup: Optional[ReplyMarkup] = None
    ) -> MessageId:
        api_logger.debug('Copy message %s to "%s" from "%s"', message_id,
                         chat_id, from_chat_id)
        response = await self._safe_request(
            RequestMethod.POST, 'copyMessage', chat_id,
            from_chat_id=from_chat_id, message_id=message_id,
            parse_mode=parse_mode, caption_entities=_to_json(caption_entities),
            caption=caption, disable_notification=disable_notification,
            reply_to_message_id=reply_to_message_id,
            allow_sending_without_reply=allow_sending_without_reply,
            reply_markup=_to_json(reply_markup))

        return MessageId.from_dict(response.result)

    async def send_photo(
        self, chat_id: Union[int, str],
        photo: Union[InputFile, str],
        caption: Optional[str] = None,
        parse_mode: Optional[ParseMode] = None,
        caption_entities: Optional[Iterable[MessageEntity]] = None,
        disable_notification: Optional[bool] = None,
        reply_to_message_id: Optional[int] = None,
        allow_sending_without_reply: Optional[bool] = None,
        reply_markup: Optional[ReplyMarkup] = None
    ) -> Message:
        api_logger.debug('Send photo to "%s"', chat_id)
        response = await self._safe_request(
            RequestMethod.POST, 'sendPhoto', chat_id, photo=photo,
            caption=caption, parse_mode=_parse_mode_to_str(parse_mode),
            caption_entities=_to_json(caption_entities),
            disable_notification=disable_notification,
            reply_to_message_id=reply_to_message_id,
            allow_sending_without_reply=allow_sending_without_reply,
            reply_markup=_to_json(reply_markup))

        return Message.from_dict(response.result)

    async def send_audio(
        self, chat_id: Union[int, str],
        audio: Union[InputFile, str],
        caption: Optional[str] = None,
        parse_mode: Optional[ParseMode] = None,
        caption_entities: Optional[Iterable[MessageEntity]] = None,
        disable_notification: Optional[bool] = None,
        duration: Optional[int] = None,
        performer: Optional[str] = None,
        title: Optional[str] = None,
        thumb: Optional[Union[InputFile, str]] = None,
        reply_to_message_id: Optional[int] = None,
        allow_sending_without_reply: Optional[bool] = None,
        reply_markup: Optional[ReplyMarkup] = None
    ) -> Message:
        api_logger.debug('Send audio to "%s"', chat_id)
        response = await self._safe_request(
            RequestMethod.POST, 'sendAudio', chat_id, audio=audio,
            caption=caption, parse_mode=_parse_mode_to_str(parse_mode),
            caption_entities=_to_json(caption_entities),
            disable_notification=disable_notification,
            duration=duration, performer=performer,
            title=title, thumb=thumb,
            reply_to_message_id=reply_to_message_id,
            allow_sending_without_reply=allow_sending_without_reply,
            reply_markup=_to_json(reply_markup))

        return Message.from_dict(response.result)

    async def send_document(
        self, chat_id: Union[int, str],
        document: Union[InputFile, str],
        thumb: Optional[Union[InputFile, str]],
        caption: Optional[str] = None,
        parse_mode: Optional[ParseMode] = None,
        caption_entities: Optional[Iterable[MessageEntity]] = None,
        disable_content_type_detection: Optional[bool] = None,
        disable_notification: Optional[bool] = None,
        duration: Optional[int] = None,
        performer: Optional[str] = None,
        title: Optional[str] = None,
        reply_to_message_id: Optional[int] = None,
        allow_sending_without_reply: Optional[bool] = None,
        reply_markup: Optional[ReplyMarkup] = None
    ) -> Message:
        api_logger.debug('Send document to "%s"', chat_id)
        response = await self._safe_request(
            RequestMethod.POST, 'sendDocument', chat_id,
            document=document, thumb=thumb, caption=caption,
            parse_mode=_parse_mode_to_str(parse_mode),
            caption_entities=_to_json(caption_entities),
            disable_content_type_detection=disable_content_type_detection,
            disable_notification=disable_notification,
            duration=duration, performer=performer, title=title,
            reply_to_message_id=reply_to_message_id,
            allow_sending_without_reply=allow_sending_without_reply,
            reply_markup=_to_json(reply_markup))

        return Message.from_dict(response.result)

    async def send_video(
        self, chat_id: Union[int, str],
        video: Union[InputFile, str],
        duration: Optional[int] = None,
        width: Optional[int] = None,
        height: Optional[int] = None,
        thumb: Optional[Union[InputFile, str]] = None,
        caption: Optional[str] = None,
        parse_mode: Optional[ParseMode] = None,
        caption_entities: Optional[Iterable[MessageEntity]] = None,
        supports_streaming: Optional[bool] = None,
        disable_notification: Optional[bool] = None,
        reply_to_message_id: Optional[int] = None,
        allow_sending_without_reply: Optional[bool] = None,
        reply_markup: Optional[ReplyMarkup] = None
    ) -> Message:
        api_logger.debug('Send video to "%s"', chat_id)
        response = await self._safe_request(
            RequestMethod.POST, 'sendVideo', chat_id,
            video=video, duration=duration, width=width,
            height=height, thumb=thumb, caption=caption,
            parse_mode=_parse_mode_to_str(parse_mode),
            caption_entities=_to_json(caption_entities),
            supports_streaming=supports_streaming,
            disable_notification=disable_notification,
            reply_to_message_id=reply_to_message_id,
            allow_sending_without_reply=allow_sending_without_reply,
            reply_markup=_to_json(reply_markup))

        return Message.from_dict(response.result)

    async def send_animation(
        self, chat_id: Union[int, str],
        animation: Union[InputFile, str],
        duration: Optional[int] = None,
        width: Optional[int] = None,
        height: Optional[int] = None,
        thumb: Optional[Union[InputFile, str]] = None,
        caption: Optional[str] = None,
        parse_mode: Optional[ParseMode] = None,
        caption_entities: Optional[Iterable[MessageEntity]] = None,
        disable_notification: Optional[bool] = None,
        reply_to_message_id: Optional[int] = None,
        allow_sending_without_reply: Optional[bool] = None,
        reply_markup: Optional[ReplyMarkup] = None
    ) -> Message:
        api_logger.debug('Send animation to "%s"', chat_id)
        response = await self._safe_request(
            RequestMethod.POST, 'sendAnimation', chat_id,
            animation=animation, duration=duration, width=width,
            height=height, thumb=thumb, caption=caption,
            parse_mode=_parse_mode_to_str(parse_mode),
            caption_entities=_to_json(caption_entities),
            disable_notification=disable_notification,
            reply_to_message_id=reply_to_message_id,
            allow_sending_without_reply=allow_sending_without_reply,
            reply_markup=_to_json(reply_markup))

        return Message.from_dict(response.result)

    async def send_voice(
        self, chat_id: Union[int, str],
        voice: Union[InputFile, str],
        caption: Optional[str] = None,
        parse_mode: Optional[ParseMode] = None,
        caption_entities: Optional[Iterable[MessageEntity]] = None,
        duration: Optional[int] = None,
        disable_notification: Optional[bool] = None,
        reply_to_message_id: Optional[int] = None,
        allow_sending_without_reply: Optional[bool] = None,
        reply_markup: Optional[ReplyMarkup] = None
    ) -> Message:
        api_logger.debug('Send voice to "%s"', chat_id)
        response = await self._safe_request(
            RequestMethod.POST, 'sendVoice', chat_id,
            voice=voice, caption=caption,
            parse_mode=_parse_mode_to_str(parse_mode),
            caption_entities=_to_json(caption_entities),
            duration=duration,
            disable_notification=disable_notification,
            reply_to_message_id=reply_to_message_id,
            allow_sending_without_reply=allow_sending_without_reply,
            reply_markup=_to_json(reply_markup))

        return Message.from_dict(response.result)

    async def send_video_note(
        self, chat_id: Union[int, str],
        video_note: Union[InputFile, str],
        duration: Optional[int] = None,
        length: Optional[int] = None,
        thumb: Optional[Union[InputFile, str]] = None,
        disable_notification: Optional[bool] = None,
        reply_to_message_id: Optional[int] = None,
        allow_sending_without_reply: Optional[bool] = None,
        reply_markup: Optional[ReplyMarkup] = None
    ) -> Message:
        api_logger.debug('Send video not to "%s"', chat_id)
        response = await self._safe_request(
            RequestMethod.POST, 'sendVideoNote', chat_id,
            video_note=video_note, duration=duration,
            length=length, thumb=thumb,
            disable_notification=disable_notification,
            reply_to_message_id=reply_to_message_id,
            allow_sending_without_reply=allow_sending_without_reply,
            reply_markup=_to_json(reply_markup))

        return Message.from_dict(response.result)

    async def send_media_group(
        self, chat_id: Union[int, str],
        media: Iterable[Union[InputMediaAudio, InputMediaDocument,
                              InputMediaPhoto, InputMediaVideo]],
        disable_notification: Optional[bool] = None,
        reply_to_message_id: Optional[int] = None,
        allow_sending_without_reply: Optional[bool] = None
    ) -> Tuple[Message, ...]:
        api_logger.debug('Send media group to "%s"', chat_id)
        attached_media = []
        attachments = {}
        counter = count()
        for item in media:
            if isinstance(item.media, str):
                attached_media.append(item)
            else:
                attachment_name = f'attachment{next(counter)}'
                attachments[attachment_name] = item.media
                attached_media.append(attr.evolve(
                    item, media=f'attach://{attachment_name}'))
        response = await self._safe_request(
            RequestMethod.POST, 'sendMediaGroup', chat_id,
            media=json_dumps(tuple(item.to_dict() for item in attached_media)),
            disable_notification=disable_notification,
            reply_to_message_id=reply_to_message_id,
            allow_sending_without_reply=allow_sending_without_reply,
            **attachments
        )

        return tuple(Message.from_dict(item) for item in response.result)

    async def send_location(
        self, chat_id: Union[int, str],
        latitude: float,
        longitude: float,
        horizontal_accuracy: Optional[float] = None,
        live_period: Optional[int] = None,
        heading: Optional[int] = None,
        proximity_alert_radius: Optional[int] = None,
        length: Optional[int] = None,
        disable_notification: Optional[bool] = None,
        reply_to_message_id: Optional[int] = None,
        allow_sending_without_reply: Optional[bool] = None,
        reply_markup: Optional[ReplyMarkup] = None
    ) -> Message:
        api_logger.debug('Send location to "%s"', chat_id)
        response = await self._safe_request(
            RequestMethod.POST, 'sendLocation', chat_id,
            latitude=latitude, longitude=longitude,
            horizontal_accuracy=horizontal_accuracy,
            live_period=live_period, heading=heading,
            proximity_alert_radius=proximity_alert_radius, length=length,
            disable_notification=disable_notification,
            reply_to_message_id=reply_to_message_id,
            allow_sending_without_reply=allow_sending_without_reply,
            reply_markup=_to_json(reply_markup))

        return Message.from_dict(response.result)

    async def edit_message_live_location(
        self,
        latitude: float,
        longitude: float,
        horizontal_accuracy: Optional[float] = None,
        heading: Optional[int] = None,
        proximity_alert_radius: Optional[int] = None,
        chat_id: Optional[Union[int, str]] = None,
        message_id: Optional[int] = None,
        inline_message_id: Optional[str] = None,
        reply_markup: Optional[ReplyMarkup] = None
    ) -> Union[Message, bool]:
        api_logger.debug('Edit live location %s in "%s"', message_id, chat_id)
        response = await self._request(
            RequestMethod.POST, 'editMessageLiveLocation',
            chat_id=chat_id, message_id=message_id,
            inline_message_id=inline_message_id,
            latitude=latitude, longitude=longitude,
            horizontal_accuracy=horizontal_accuracy, heading=heading,
            proximity_alert_radius=proximity_alert_radius,
            reply_markup=_to_json(reply_markup))

        if isinstance(response.result, bool):
            return response.result
        else:
            return Message.from_dict(response.result)

    async def stop_message_live_location(
        self, chat_id: Optional[Union[int, str]] = None,
        message_id: Optional[int] = None,
        inline_message_id: Optional[str] = None,
        reply_markup: Optional[ReplyMarkup] = None
    ) -> Union[Message, bool]:
        api_logger.debug('Stop live location %s in "%s"', message_id, chat_id)
        response = await self._request(
            RequestMethod.POST, 'stopMessageLiveLocation',
            chat_id=chat_id, message_id=message_id,
            inline_message_id=inline_message_id,
            reply_markup=_to_json(reply_markup))

        if isinstance(response.result, bool):
            return response.result
        else:
            return Message.from_dict(response.result)

    async def send_venue(
        self, chat_id: Union[int, str],
        latitude: float,
        longitude: float,
        title: str,
        address: str,
        foursquare_id: Optional[str] = None,
        foursquare_type: Optional[str] = None,
        disable_notification: Optional[bool] = None,
        reply_to_message_id: Optional[int] = None,
        allow_sending_without_reply: Optional[bool] = None,
        reply_markup: Optional[ReplyMarkup] = None
    ) -> Message:
        api_logger.debug('Send venue to "%s"', chat_id)
        response = await self._safe_request(
            RequestMethod.POST, 'sendVenue', chat_id,
            latitude=latitude, longitude=longitude, title=title,
            address=address, foursquare_id=foursquare_id,
            foursquare_type=foursquare_type,
            disable_notification=disable_notification,
            reply_to_message_id=reply_to_message_id,
            allow_sending_without_reply=allow_sending_without_reply,
            reply_markup=_to_json(reply_markup))

        return Message.from_dict(response.result)

    async def send_contact(
        self, chat_id: Union[int, str],
        phone_number: str,
        first_name: str,
        last_name: Optional[str] = None,
        vcard: Optional[str] = None,
        disable_notification: Optional[bool] = None,
        reply_to_message_id: Optional[int] = None,
        allow_sending_without_reply: Optional[bool] = None,
        reply_markup: Optional[ReplyMarkup] = None
    ) -> Message:
        api_logger.debug('Send contact to "%s"', chat_id)
        response = await self._safe_request(
            RequestMethod.POST, 'sendContact', chat_id,
            phone_number=phone_number, first_name=first_name,
            last_name=last_name, vcard=vcard,
            disable_notification=disable_notification,
            reply_to_message_id=reply_to_message_id,
            allow_sending_without_reply=allow_sending_without_reply,
            reply_markup=_to_json(reply_markup))

        return Message.from_dict(response.result)

    async def send_poll(
        self, chat_id: Union[int, str], question: str,
        options: Iterable[str], is_anonymous: Optional[bool],
        type_: Optional[PollType] = None,
        allows_multiple_answers: Optional[bool] = None,
        correct_option_id: Optional[bool] = None,
        is_closed: Optional[bool] = None,
        explanation: Optional[str] = None,
        explanation_parse_mode: Optional[ParseMode] = None,
        explanation_entities: Optional[Iterable[MessageEntity]] = None,
        open_period: Optional[int] = None,
        close_date: Optional[int] = None,
        disable_notification: Optional[bool] = None,
        reply_to_message_id: Optional[int] = None,
        allow_sending_without_reply: Optional[bool] = None,
        reply_markup: Optional[ReplyMarkup] = None
    ) -> Message:
        api_logger.debug('Send poll to chat "%s"', chat_id)
        response = await self._safe_request(
            RequestMethod.POST, 'sendPoll', chat_id,
            question=question, options=json_dumps(tuple(options)),
            is_anonymous=is_anonymous, type=type_,
            allows_multiple_answers=allows_multiple_answers,
            correct_option_id=correct_option_id,
            is_closed=is_closed, explanation=explanation,
            explanation_parse_mode=explanation_parse_mode,
            explanation_entities=_to_json(explanation_entities),
            open_period=open_period, close_date=close_date,
            disable_notification=disable_notification,
            reply_to_message_id=reply_to_message_id,
            allow_sending_without_reply=allow_sending_without_reply,
            reply_markup=_to_json(reply_markup))

        return response.result

    async def send_dice(self, chat_id: Union[int, str],
                        emoji: Optional[DiceEmoji] = None,
                        disable_notification: Optional[bool] = None,
                        reply_to_message_id: Optional[int] = None,
                        allow_sending_without_reply: Optional[bool] = None,
                        reply_markup: Optional[ReplyMarkup] = None) -> Message:
        api_logger.debug('Send dice "%s" to chat "%s"', emoji, chat_id)
        response = await self._safe_request(
            RequestMethod.POST, 'sendDice', chat_id,
            emoji=_dice_emoji_to_str(emoji),
            disable_notification=disable_notification,
            reply_to_message_id=reply_to_message_id,
            allow_sending_without_reply=allow_sending_without_reply,
            reply_markup=_to_json(reply_markup))

        return response.result

    async def send_chat_action(self, chat_id: Union[int, str],
                               action: ChatAction) -> bool:
        api_logger.debug('Send action "%s" to chat "%s"', action, chat_id)
        response = await self._safe_request(
            RequestMethod.POST, 'sendChatAction', chat_id, action=action.value)

        return response.result

    async def get_user_profile_photos(
        self, user_id: int,
        offset: Optional[int] = None,
        limit: Optional[int] = None
    ) -> UserProfilePhotos:
        api_logger.debug('Get user profile photos %s offset %s limit %s',
                         user_id, offset, limit)
        response = await self._request(
            RequestMethod.GET, 'getUserProfilePhotos',
            user_id=user_id, offset=offset, limit=limit)

        return UserProfilePhotos.from_dict(response.result)

    async def get_file(self, file_id: str) -> File:
        api_logger.debug('Get file "%s"', file_id)
        response = await self._request(RequestMethod.GET, 'getFile',
                                       file_id=file_id)
        return File.from_dict(response.result)

    async def kick_chat_member(self, chat_id: Union[int, str], user_id: int,
                               until_date: Optional[int] = None) -> bool:
        response = await self._request(
            RequestMethod.POST, 'kickChatMember',
            chat_id=chat_id, user_id=user_id, until_date=until_date)

        return response.result

    async def unban_chat_member(self, chat_id: Union[int, str], user_id: int,
                                only_if_banned: Optional[bool] = None) -> bool:
        api_logger.debug('Unban member %s in "%s"', user_id, chat_id)
        response = await self._request(RequestMethod.POST, 'unbanChatMember',
                                       chat_id=chat_id, user_id=user_id,
                                       only_if_banned=only_if_banned)

        return response.result

    async def restrict_chat_member(
        self, chat_id: Union[int, str],
        user_id: int,
        permissions: ChatPermissions,
        until_date: Optional[int] = None,
    ) -> bool:
        api_logger.debug('Restrict member %s in "%s"', user_id, chat_id)
        response = await self._request(
            RequestMethod.POST, 'restrictChatMember', chat_id=chat_id,
            user_id=user_id, permissions=json_dumps(permissions.to_dict()),
            until_date=until_date)

        return response.result

    async def promote_chat_member(
        self, chat_id: Union[int, str],
        user_id: int,
        is_anonymous: Optional[int] = None,
        can_change_info: Optional[int] = None,
        can_post_messages: Optional[bool] = None,
        can_edit_messages: Optional[bool] = None,
        can_delete_messages: Optional[bool] = None,
        can_invite_users: Optional[bool] = None,
        can_restrict_members: Optional[bool] = None,
        can_pin_messages: Optional[bool] = None,
        can_promote_members: Optional[bool] = None
    ) -> bool:
        api_logger.debug('Promote member %s in "%s"', user_id, chat_id)
        response = await self._request(
            RequestMethod.POST, 'promoteChatMember',
            chat_id=chat_id, user_id=user_id, is_anonymous=is_anonymous,
            can_change_info=can_change_info,
            can_post_messages=can_post_messages,
            can_edit_messages=can_edit_messages,
            can_delete_messages=can_delete_messages,
            can_invite_users=can_invite_users,
            can_restrict_members=can_restrict_members,
            can_pin_messages=can_pin_messages,
            can_promote_members=can_promote_members)

        return response.result

    async def set_chat_administrator_custom_title(
        self, chat_id: Union[int, str],
        user_id: int,
        custom_title: str
    ) -> bool:
        api_logger.debug('Set title "%s" for admin %s in "%s"',
                         custom_title, user_id, chat_id)
        response = await self._request(
            RequestMethod.POST, 'setChatAdministratorCustomTitle',
            chat_id=chat_id, user_id=user_id, custom_title=custom_title)

        return response.result

    async def export_chat_invite_link(self, chat_id: Union[int, str]) -> str:
        api_logger.debug('Export chat "%s" invite link', chat_id)
        response = await self._request(
            RequestMethod.POST, 'exportChatInviteLink', chat_id=chat_id)

        return response.result

    async def set_chat_permissions(
        self, chat_id: Union[int, str],
        permissions: ChatPermissions
    ) -> bool:
        api_logger.debug('Set chat "%s" permissions', chat_id)
        response = await self._safe_request(
            RequestMethod.POST, 'setChatPermissions', chat_id,
            permissions=json_dumps(permissions.to_dict()))

        return response.result

    async def set_chat_photo(
        self, chat_id: Union[int, str],
        photo: InputFile
    ) -> bool:
        api_logger.debug('Set chat "%s" photo', chat_id)
        response = await self._safe_request(RequestMethod.POST, 'setChatPhoto',
                                            chat_id, photo=photo)

        return response.result

    async def delete_chat_photo(self, chat_id: Union[int, str]) -> bool:
        api_logger.debug('Delete chat "%s" photo', chat_id)
        response = await self._request(RequestMethod.POST, 'deleteChatPhoto',
                                       chat_id=chat_id)

        return response.result

    async def set_chat_title(self, chat_id: Union[int, str],
                             title: str) -> bool:
        api_logger.debug('Set title "%s" for chat "%s"', title, chat_id)
        response = await self._request(RequestMethod.POST, 'setChatTitle',
                                       chat_id=chat_id, title=title)

        return response.result

    async def set_chat_description(self, chat_id: Union[int, str],
                                   description: str) -> bool:
        api_logger.debug('Set chat "%s" description', chat_id)
        response = await self._request(
            RequestMethod.POST, 'setChatDescription', chat_id=chat_id,
            description=description)

        return response.result

    async def pin_chat_message(
        self, chat_id: Union[int, str], message_id: int,
        disable_notification: Optional[bool] = None
    ) -> bool:
        api_logger.debug('Pin message %s in chat "%s"', message_id, chat_id)
        response = await self._request(
            RequestMethod.POST, 'pinChatMessage',
            chat_id=chat_id, message_id=message_id,
            disable_notification=disable_notification)

        return response.result

    async def unpin_chat_message(self, chat_id: Union[int, str],
                                 message_id: Optional[int]) -> bool:
        api_logger.debug('Unpin message "%s" in chat "%s"',
                         message_id, chat_id)
        response = await self._request(RequestMethod.POST, 'unpinChatMessage',
                                       chat_id=chat_id, message_id=message_id)

        return response.result

    async def unpin_all_chat_messages(self, chat_id: Union[int, str]) -> bool:
        api_logger.debug('Unpin all messages in chat "%s"', chat_id)
        response = await self._request(RequestMethod.POST,
                                       'unpinAllChatMessages', chat_id=chat_id)

        return response.result

    async def leave_chat(self, chat_id: Union[int, str]) -> bool:
        api_logger.debug('Leave chat "%s"', chat_id)
        response = await self._request(RequestMethod.POST, 'leaveChat',
                                       chat_id=chat_id)

        return response.result

    async def get_chat(self, chat_id: Union[int, str]) -> Chat:
        api_logger.debug('Get chat "%s"', chat_id)
        response = await self._request(RequestMethod.GET, 'getChat',
                                       chat_id=chat_id)
        return Chat.from_dict(response.result)

    async def get_chat_administrators(
        self, chat_id: Union[int, str]
    ) -> Tuple[ChatMember, ...]:
        api_logger.debug('Get chat administrators "%s"', chat_id)
        response = await self._request(
            RequestMethod.GET, 'getChatAdministrators', chat_id=chat_id)

        return tuple(ChatMember.from_dict(item) for item in response.result)

    async def get_chat_members_count(self, chat_id: Union[int, str]) -> int:
        api_logger.debug('Get chat members count "%s"', chat_id)
        response = await self._request(
            RequestMethod.GET, 'getChatMembersCount', chat_id=chat_id)

        return response.result

    async def get_chat_member(self, chat_id: Union[int, str],
                              user_id: int) -> ChatMember:
        api_logger.debug('Get chat "%s" member %s', chat_id, user_id)
        response = await self._request(RequestMethod.GET, 'getChatMember',
                                       chat_id=chat_id, user_id=user_id)

        return ChatMember.from_dict(response.result)

    async def set_chat_sticker_set(self, chat_id: Union[int, str],
                                   sticker_set_name: str) -> bool:
        api_logger.debug('Set chat "%s" sticker set "%s"', chat_id,
                         sticker_set_name)
        response = await self._request(
            RequestMethod.POST, 'setChatStickerSet', chat_id=chat_id,
            sticker_set_name=sticker_set_name)

        return response.result

    async def delete_chat_sticker_set(self, chat_id: Union[int, str]) -> bool:
        api_logger.debug('Delete chat "%s" sticker set', chat_id)
        response = await self._request(RequestMethod.POST,
                                       'deleteChatStickerSet', chat_id=chat_id)

        return response.result

    async def answer_callback_query(self, callback_query_id: str,
                                    text: Optional[str] = None,
                                    show_alert: Optional[bool] = None,
                                    url: Optional[str] = None,
                                    cache_time: Optional[int] = None) -> bool:
        api_logger.debug('Answer callback query "%s"', callback_query_id)
        response = await self._request(
            RequestMethod.POST, 'answerCallbackQuery',
            callback_query_id=callback_query_id, text=text,
            show_alert=show_alert, url=url, cache_time=cache_time)

        return response.result

    async def set_my_commands(self, commands: Iterable[BotCommand]) -> bool:
        api_logger.debug('Set my commands "%s"', commands)
        response = await self._request(
            RequestMethod.POST, 'setMyCommands',
            commands=json_dumps(tuple(command.to_dict()
                                      for command in commands)))

        return response.result

    async def get_my_commands(self) -> Tuple[BotCommand, ...]:
        api_logger.debug('Get my commands')
        response = await self._request(RequestMethod.GET, 'getMyCommands')

        return tuple(BotCommand.from_dict(item) for item in response.result)

    async def edit_message_text(
        self,
        text: str,
        chat_id: Optional[Union[int, str]] = None,
        message_id: Optional[int] = None,
        inline_message_id: Optional[str] = None,
        parse_mode: Optional[ParseMode] = None,
        entities: Optional[Iterable[MessageEntity]] = None,
        disable_web_page_preview: Optional[bool] = None,
        reply_markup: Optional[InlineKeyboardMarkup] = None
    ) -> Union[Message, bool]:
        if inline_message_id is None:
            api_logger.debug('Edit message %s in "%s" text',
                             message_id, chat_id)
        else:
            api_logger.debug('Edit inline message "%s" text',
                             inline_message_id)
        response = await self._request(
            RequestMethod.POST, 'editMessageText',
            chat_id=chat_id, message_id=message_id,
            inline_message_id=inline_message_id, text=text,
            parse_mode=_parse_mode_to_str(parse_mode),
            entities=_to_json(entities),
            disable_web_page_preview=disable_web_page_preview,
            reply_markup=_to_json(reply_markup))

        if isinstance(response.result, bool):
            return response.result
        else:
            return Message.from_dict(response.result)

    async def edit_message_caption(
        self, chat_id: Optional[Union[int, str]] = None,
        message_id: Optional[int] = None,
        inline_message_id: Optional[str] = None,
        caption: Optional[str] = None,
        parse_mode: Optional[ParseMode] = None,
        caption_entities: Optional[Iterable[MessageEntity]] = None,
        reply_markup: Optional[InlineKeyboardMarkup] = None
    ) -> Union[Message, bool]:
        if inline_message_id is None:
            api_logger.debug('Edit message %s in "%s" caption',
                             message_id, chat_id)
        else:
            api_logger.debug('Edit inline message "%s" caption',
                             inline_message_id)
        response = await self._request(
            RequestMethod.POST, 'editMessageCaption',
            chat_id=chat_id, message_id=message_id,
            inline_message_id=inline_message_id,
            caption=caption, parse_mode=_parse_mode_to_str(parse_mode),
            caption_entities=_to_json(caption_entities),
            reply_markup=_to_json(reply_markup))

        if isinstance(response.result, bool):
            return response.result
        else:
            return Message.from_dict(response.result)

    async def edit_message_media(
        self, chat_id: Optional[Union[int, str]] = None,
        message_id: Optional[int] = None,
        inline_message_id: Optional[str] = None,
        media: InputMedia = None,
        reply_markup: Optional[InlineKeyboardMarkup] = None
    ) -> Union[Message, bool]:
        if inline_message_id is None:
            api_logger.debug('Edit message %s in "%s" media',
                             message_id, chat_id)
        else:
            api_logger.debug('Edit inline message "%s" media',
                             inline_message_id)
        attachments = {}
        if media is not None and not isinstance(media.media, str):
            attachment_name = 'attachment0'
            attachments[attachment_name] = media.media
            media = attr.evolve(media, media=f'attach://{attachment_name}')
        response = await self._request(
            RequestMethod.POST, 'editMessageMedia', chat_id=chat_id,
            message_id=message_id, inline_message_id=inline_message_id,
            media=_to_json(media), reply_markup=_to_json(reply_markup),
            **attachments)
        if isinstance(response.result, bool):
            return response.result
        else:
            return Message.from_dict(response.result)

    async def edit_message_reply_markup(
        self, chat_id: Optional[Union[int, str]] = None,
        message_id: Optional[int] = None,
        inline_message_id: Optional[str] = None,
        reply_markup: Optional[InlineKeyboardMarkup] = None
    ) -> Union[Message, bool]:
        if inline_message_id is None:
            api_logger.debug('Edit message %s in "%s" reply markup',
                             message_id, chat_id)
        else:
            api_logger.debug('Edit inline message "%s" reply markup',
                             inline_message_id)
        response = await self._request(
            RequestMethod.POST, 'editMessageReplyMarkup',
            chat_id=chat_id, message_id=message_id,
            inline_message_id=inline_message_id,
            reply_markup=_to_json(reply_markup))

        if isinstance(response.result, bool):
            return response.result
        else:
            return Message.from_dict(response.result)

    async def stop_poll(
        self, chat_id: Union[int, str],
        message_id: int = None,
        reply_markup: Optional[InlineKeyboardMarkup] = None
    ) -> Poll:
        api_logger.debug('Stop poll %s in "%s"', message_id, chat_id)
        response = await self._request(
            RequestMethod.POST, 'stopPoll',
            chat_id=chat_id, message_id=message_id,
            reply_markup=_to_json(reply_markup))

        return response.result

    async def delete_message(self, chat_id: Optional[Union[int, str]] = None,
                             message_id: Optional[int] = None) -> bool:
        api_logger.debug('Delete message %s in "%s"', message_id, chat_id)
        response = await self._request(RequestMethod.POST, 'deleteMessage',
                                       chat_id=chat_id, message_id=message_id)

        return response.result

    async def send_sticker(
        self, chat_id: Union[int, str],
        sticker: Union[InputFile, str],
        disable_notification: Optional[bool] = None,
        reply_to_message_id: Optional[int] = None,
        allow_sending_without_reply: Optional[bool] = None,
        reply_markup: Optional[ReplyMarkup] = None
    ) -> Message:
        api_logger.debug('Send sticker to "%s"', chat_id)
        response = await self._request(
            RequestMethod.POST, 'sendSticker',
            chat_id=chat_id, sticker=sticker,
            disable_notification=disable_notification,
            reply_to_message_id=reply_to_message_id,
            allow_sending_without_reply=allow_sending_without_reply,
            reply_markup=_to_json(reply_markup))

        return Message.from_dict(response.result)

    async def get_sticker_set(self, name: str) -> StickerSet:
        api_logger.debug('Get sticker set "%s"', name)
        response = await self._request(RequestMethod.GET, 'getStickerSet',
                                       name=name)

        return StickerSet.from_dict(response.result)

    async def upload_sticker_file(self, user_id: int,
                                  png_sticker: InputFile) -> File:
        api_logger.debug('Upload sticker file for %s', user_id)
        response = await self._request(
            RequestMethod.POST, 'uploadStickerFile',
            user_id=user_id, png_sticker=png_sticker)

        return File.from_dict(response.result)

    async def create_new_sticker_set(
        self, user_id: int, name: str, title: str, emojis: str,
        png_sticker: Union[InputFile, str, None] = None,
        tgs_sticker: Optional[InputFile] = None,
        contains_masks: Optional[bool] = None,
        mask_position: Optional[MaskPosition] = None
    ) -> bool:
        api_logger.debug('Create new sticker set "%s" for %s', name, user_id)
        response = await self._request(
            RequestMethod.POST, 'createNewStickerSet',
            user_id=user_id, name=name, title=title, emojis=emojis,
            png_sticker=png_sticker, tgs_sticker=tgs_sticker,
            contains_masks=contains_masks,
            mask_position=_to_json(mask_position))

        return response.result

    async def add_sticker_to_set(
        self, user_id: int, name: str, title: str,
        png_sticker: Union[InputFile, str], emojis: str,
        mask_position: Optional[MaskPosition] = None
    ) -> File:
        api_logger.debug('Add sticker to set "%s" for %s', name, user_id)
        response = await self._request(
            RequestMethod.POST, 'addStickerToSet',
            user_id=user_id, name=name, title=title, png_sticker=png_sticker,
            emojis=emojis, mask_position=_to_json(mask_position))

        return File.from_dict(response.result)

    async def set_sticker_position_in_set(self, sticker: str,
                                          position: int) -> bool:
        api_logger.debug('Set sticker "%s" position to %s', sticker, position)
        response = await self._request(
            RequestMethod.POST, 'setStickerPositionInSet',
            sticker=sticker, position=position)

        return response.result

    async def delete_sticker_from_set(self, sticker: str) -> bool:
        api_logger.debug('Delete sticker "%s" from set', sticker)
        response = await self._request(
            RequestMethod.POST, 'deleteStickerFromSet', sticker=sticker)

        return response.result

    async def set_sticker_set_thumb(
            self, name: str, user_id: int,
            thumb: Union[InputFile, str, None] = None
    ) -> bool:
        api_logger.debug('Set sticker set "%s" owned by "%s" thumb',
                         name, user_id)
        response = await self._request(
            RequestMethod.POST, 'setStickerSetThumb',
            name=name, user_id=user_id, thumb=thumb)

        return response.result

    async def answer_inline_query(
        self, inline_query_id: str, results: Iterable[InlineQueryResult],
        cache_time: Optional[int] = None, is_personal: Optional[bool] = None,
        next_offset: Optional[str] = None,
        switch_pm_text: Optional[str] = None,
        switch_pm_parameter: Optional[str] = None
    ) -> bool:
        api_logger.debug('Answer inline query "%s"', inline_query_id)
        response = await self._request(
            RequestMethod.POST, 'answerInlineQuery',
            inline_query_id=inline_query_id,
            results=json_dumps(tuple(result.to_dict() for result in results)),
            cache_time=cache_time, is_personal=is_personal,
            next_offset=next_offset, switch_pm_text=switch_pm_text,
            switch_pm_parameter=switch_pm_parameter)

        return response.result

    async def send_invoice(
        self, chat_id: int, title: str, description: str, payload: str,
        provider_token: str, start_parameter: str, currency: str,
        prices: Iterable[LabeledPrice], provider_data: Optional[str] = None,
        photo_url: Optional[str] = None, photo_size: Optional[int] = None,
        photo_width: Optional[int] = None, photo_height: Optional[int] = None,
        need_name: Optional[bool] = None,
        need_phone_number: Optional[bool] = None,
        need_email: Optional[bool] = None,
        need_shipping_address: Optional[bool] = None,
        send_phone_number_to_provider: Optional[bool] = None,
        send_email_to_provider: Optional[bool] = None,
        is_flexible: Optional[bool] = None,
        disable_notification: Optional[bool] = None,
        reply_to_message_id: Optional[int] = None,
        allow_sending_without_reply: Optional[bool] = None,
        reply_markup: Optional[InlineKeyboardMarkup] = None
    ) -> Message:
        api_logger.debug('Send invoice to %s', chat_id)
        response = await self._safe_request(
            RequestMethod.POST, 'sendInvoice', chat_id,
            title=title, description=description, payload=payload,
            provider_token=provider_token, start_parameter=start_parameter,
            currency=currency,
            prices=json_dumps(tuple(price.to_dict() for price in prices)),
            provider_data=provider_data, photo_url=photo_url,
            photo_size=photo_size, photo_width=photo_width,
            photo_height=photo_height, need_name=need_name,
            need_phone_number=need_phone_number, need_email=need_email,
            need_shipping_address=need_shipping_address,
            send_phone_number_to_provider=send_phone_number_to_provider,
            send_email_to_provider=send_email_to_provider,
            is_flexible=is_flexible,
            disable_notification=disable_notification,
            reply_to_message_id=reply_to_message_id,
            allow_sending_without_reply=allow_sending_without_reply,
            reply_markup=_to_json(reply_markup))

        return Message.from_dict(response.result)

    async def answer_shipping_query(
        self, inline_query_id: str, ok: bool,
        shipping_options: Optional[Iterable[ShippingOption]] = None,
        error_message: Optional[str] = None
    ) -> bool:
        api_logger.debug('Answer shipping query "%s"', inline_query_id)
        if shipping_options is not None:
            shipping_options_json: Optional[str] = json_dumps(tuple(
                item.to_dict() for item in shipping_options))
        else:
            shipping_options_json = None
        response = await self._request(
            RequestMethod.POST, 'answerShippingQuery',
            inline_query_id=inline_query_id, ok=ok,
            shipping_options=shipping_options_json,
            error_message=error_message)

        return response.result

    async def answer_pre_checkout_query(
        self, pre_checkout_query_id: str, ok: bool,
        error_message: Optional[str] = None
    ) -> bool:
        api_logger.debug('Answer pre checkout query "%s"',
                         pre_checkout_query_id)
        response = await self._request(
            RequestMethod.POST, 'answerPreCheckoutQuery',
            pre_checkout_query_id=pre_checkout_query_id, ok=ok,
            error_message=error_message)

        return response.result

    async def set_passport_data_errors(
        self, user_id: int,
        errors: Iterable[PassportElementError]
    ) -> bool:
        api_logger.debug('Set passport data errors %s', user_id)
        response = await self._request(
            RequestMethod.POST, 'setPassportDataErrors',
            user_id=user_id,
            errors=json_dumps(tuple(error.to_dict() for error in errors)))

        return response.result

    async def send_game(
        self, chat_id: int, game_short_name: str,
        disable_notification: Optional[bool] = None,
        reply_to_message_id: Optional[int] = None,
        allow_sending_without_reply: Optional[bool] = None,
        reply_markup: Optional[InlineKeyboardMarkup] = None
    ) -> Message:
        api_logger.debug('Send game "%s" to %s', chat_id, game_short_name)
        response = await self._safe_request(
            RequestMethod.POST, 'sendGame', chat_id,
            game_short_name=game_short_name,
            disable_notification=disable_notification,
            reply_to_message_id=reply_to_message_id,
            allow_sending_without_reply=allow_sending_without_reply,
            reply_markup=_to_json(reply_markup))

        return Message.from_dict(response.result)

    async def set_game_score(
        self, user_id: int, score: int,
        force: Optional[bool] = None,
        disable_edit_message: Optional[bool] = None,
        chat_id: Optional[int] = None, message_id: Optional[int] = None,
        inline_message_id: Optional[str] = None
    ) -> Union[Message, bool]:
        api_logger.debug('Set game score %s for %s', score, user_id)
        response = await self._request(
            RequestMethod.POST, 'setGameScore',
            user_id=user_id, score=score, force=force,
            disable_edit_message=disable_edit_message,
            chat_id=chat_id, message_id=message_id,
            inline_message_id=inline_message_id)

        if isinstance(response.result, bool):
            return response.result
        else:
            return Message.from_dict(response.result)

    async def get_game_high_scores(
        self, user_id: int, chat_id: Optional[int] = None,
        message_id: Optional[int] = None,
        inline_message_id: Optional[str] = None
    ) -> Tuple[GameHighScore, ...]:
        api_logger.debug('Get game high scores for %s', user_id)
        response = await self._request(
            RequestMethod.POST, 'getGameHighScores',
            user_id=user_id, chat_id=chat_id, message_id=message_id,
            inline_message_id=inline_message_id)

        return tuple(GameHighScore.from_dict(item) for item in response.result)
