import asyncio
from typing import *
import aiohttp

from . import utils
from .types import *
from .cache import *
from . import fsm
from .router import *
from . import exceptions

import logging
bot_logger = logging.getLogger("aiomax.bot")

class Bot(Router):
    def __init__(self,
        access_token: str,
        command_prefixes: "str | List[str]" = "/",
        mention_prefix: bool = True,
        case_sensitive: bool = True,
        default_format: "Literal['markdown', 'html'] | None" = None,
        max_messages_cached: int = 10000
    ):
        '''
        Bot init

        :param access_token: Bot access token from https://max.ru/masterbot
        :param command_prefixes: List of command prefixes or a command prefix
        :param mention_prefix: Whether to respond to commands starting with the ping of the bot
        :param case_sensitive: If False the bot will respond to commands regardless of case
        :param default_format: Default message formatting mode
        :param max_messages_cached: Maximum number of messages to cache. Set to 0 to disable caching
        '''
        super().__init__(case_sensitive)

        self.access_token: str = access_token
        self.session = None
        self.polling = False

        self.command_prefixes: "str | List[str]" = command_prefixes
        self.mention_prefix: bool = mention_prefix
        self.default_format: "str | None" = default_format
        self.cache: "MessageCache | None" = MessageCache(max_messages_cached)\
            if max_messages_cached > 0 else None
        
        self.id: "int | None" = None
        self.username: "str | None" = None
        self.name: "str | None" = None
        self.description: "str | None" = None
        self.bot_commands: List[BotCommand] = None

        self.marker: "int | None" = None

        self.storage = fsm.FSMStorage()

    async def get(self, *args, **kwargs):
        '''
        Sends a GET request to the API.
        '''
        if self.session == None:
            raise Exception("Session is not initialized")
        
        params = kwargs.get('params', {})
        params['access_token'] = self.access_token
        if 'params' in kwargs:
            del kwargs['params']
        
        response = await self.session.get(*args, params=params, **kwargs)

        if response.status == 200:
            return response
    
        raise await utils.get_exception(response)
    
    
    async def post(self, *args, **kwargs):
        '''
        Sends a POST request to the API.
        '''
        if self.session == None:
            raise Exception("Session is not initialized")
        
        params = kwargs.get('params', {})
        params['access_token'] = self.access_token
        if 'params' in kwargs:
            del kwargs['params']
       
        response = await self.session.post(*args, params=params, **kwargs)

        if response.status == 200:
            return response
    
        raise await utils.get_exception(response)
    
    
    async def patch(self, *args, **kwargs):
        '''
        Sends a PATCH request to the API.
        '''
        if self.session == None:
            raise Exception("Session is not initialized")
        
        params = kwargs.get('params', {})
        params['access_token'] = self.access_token
        if 'params' in kwargs:
            del kwargs['params']
        
        response = await self.session.patch(*args, params=params, **kwargs)

        if response.status == 200:
            return response
    
        raise await utils.get_exception(response)
    
    
    async def put(self, *args, **kwargs):
        '''
        Sends a PUT request to the API.
        '''
        if self.session == None:
            raise Exception("Session is not initialized")
        
        params = kwargs.get('params', {})
        params['access_token'] = self.access_token
        if 'params' in kwargs:
            del kwargs['params']
        
        response = await self.session.put(*args, params=params, **kwargs)

        if response.status == 200:
            return response
    
        raise await utils.get_exception(response)
    
    
    async def delete(self, *args, **kwargs):
        '''
        Sends a DELETE request to the API.
        '''
        if self.session == None:
            raise Exception("Session is not initialized")
        
        params = kwargs.get('params', {})
        params['access_token'] = self.access_token
        if 'params' in kwargs:
            del kwargs['params']
        
        response = await self.session.delete(*args, params=params, **kwargs)

        if response.status == 200:
            return response
    
        raise await utils.get_exception(response)
    

    # send requests

    async def get_me(self) -> User:
        '''
        Returns info about the bot.
        '''
        response = await self.get(f"https://botapi.max.ru/me")
        user = await response.json()
        user = User.from_json(user)

        # caching info
        self.id = user.user_id
        self.username = user.username
        self.name = user.name
        self.bot_commands = user.commands
        self.description = user.description
        return user


    async def patch_me(
        self,
        name: "str | None" = None,
        description: "str | None" = None,
        commands: "List[BotCommand] | None" = None,
        photo: "ImageRequestPayload | None" = None
    ) -> User:
        '''
        Allows you to change info about the bot. Fill in only the fields that
        need to be updated.
        
        :param name: Bot display name
        :param description: Bot description
        :param commands: Commands supported by the bot. To remove all commands,
        pass an empty list.
        :param photo: Bot profile pictur
        '''
        if commands:
            commands = [i.as_dict() for i in commands]   
        if photo:
            photo = photo.as_dict()
        
        payload = {
            "name": name,
            "description": description,
            "commands": commands,
            "photo": photo
        }
        payload = {k: v for k, v in payload.items() if v}

        response = await self.patch(f"https://botapi.max.ru/me", json=payload)
        data = await response.json()
    
        # caching info
        if name:
            self.name = name
        if commands:
            self.bot_commands = commands
        if description:
            self.description = description
        
        if response.status == 200:
            return User.from_json(data)
        else:
            bot_logger.error(f"Failed to update bot info: {data}. ")
    
    
    async def get_chats(self, count: "int | None" = None, marker: "int | None" = None) -> List[Chat]:
        '''
        Returns the chats the bot is in.
        The result includes a list of chats and a marker for moving to the next page.

        :param count:  Number of chats requested. 50 by default
        :param marker: Pointer to the next page of data. Defaults to first page
        '''
        params = {
            "count": count,
            "marker": marker,
        }
        params = {k: v for k, v in params.items() if v}

        response = await self.get("https://botapi.max.ru/chats", params=params)
        data = await response.json()
        chats = [Chat.from_json(i) for i in data['chats']]

        return chats
    

    async def chat_by_link(self, link: str) -> Chat:
        '''
        Returns chat by a link or username.
 
        :param link: Public chat link or username.
        '''
        response = await self.get(f"https://botapi.max.ru/chats/{link}")
        json = await response.json()

        return Chat.from_json(json)
    
    
    async def get_chat(self, chat_id: int) -> Chat:
        '''
        Returns information about a chat.

        :param chat_id: The ID of the chat.
        '''
        response = await self.get(f"https://botapi.max.ru/chats/{chat_id}")
        json = await response.json()

        return Chat.from_json(json)
    
    
    async def get_pin(self, chat_id: int) -> "Message | None":
        '''
        Returns pinned message in the chat as ``. None if there is no pinned message

        :param chat_id: The ID of the chat.
        '''
        response = await self.get(f"https://botapi.max.ru/chats/{chat_id}/pin")
        json = await response.json()

        if json['message'] == None:
            return None

        return Message.from_json(json)


    async def pin(self,
        chat_id: int,
        message_id: str,
        notify: "bool | None" = None
    ):
        '''
        Pin a message in a chat

        :param chat_id: The ID of the chat.
        :param message_id: The ID of the message to pin.
        :param notify: Whether to notify users about the pin. True by default.
        '''
        payload = {
            "message_id": message_id,
            "notify": notify
        }
        payload = {k: v for k, v in payload.items() if v}

        response = await self.put(
            f"https://botapi.max.ru/chats/{chat_id}/pin", json=payload
        )
        return await response.json()


    async def delete_pin(self, chat_id: int):
        '''
        Delete pinned message in the chat

        :param chat_id: The ID of the chat.
        '''
        response = await self.delete(f"https://botapi.max.ru/chats/{chat_id}/pin")

        return await response.json()
    
    
    async def my_membership(self, chat_id: int) -> User:
        '''
        Returns information about the bot's membership in the chat.

        :param chat_id: The ID of the chat.
        '''
        response = await self.get(f"https://botapi.max.ru/chats/{chat_id}/members/me")
        json = await response.json()

        return User.from_json(json)


    async def leave_chat(self, chat_id: int):
        '''
        Remove the bot from the chat.

        :param chat_id: The ID of the chat.
        '''
        response = await self.delete(f"https://botapi.max.ru/chats/{chat_id}/members/me")

        return await response.json()


    async def get_admins(self, chat_id: int) -> List[User]:
        '''
        Returns a list of administrators in the chat.

        :param chat_id: The ID of the chat.
        '''
        response = await self.get(f"https://botapi.max.ru/chats/{chat_id}/members/admins")

        users = [User.from_json(i) for i in (await response.json())['members']]

        return users
    

    async def get_memberships(self, chat_id: int, user_ids: "List[int] | int") -> "List[User] | User | None":
        '''
        Returns a list of memberships in the chat for the users with the specified ID.
        '''
        params = {
            'user_ids': user_ids if type(user_ids) == list else [user_ids]
        }
        response = await self.get(f"https://botapi.max.ru/chats/{chat_id}/members", params=params)

        users = [User.from_json(i) for i in (await response.json())['members']]

        if type(user_ids) == list:
            return users
        else:
            return users[0] if len(users) > 0 else None


    async def get_members(self, chat_id: int, count_per_iter: int = 100) -> AsyncIterator[User]:
        '''
        Returns an asynchronous interator of members in the chat.

        :param chat_id: The ID of the chat.
        :param count_per_iter: The number of users to fetch per request.
        '''
        marker = None
        
        while True:
            params = {
                "count": count_per_iter,
                "marker": marker,
            }
            params = {k: v for k, v in params.items() if v}
            response = await self.get(f"https://botapi.max.ru/chats/{chat_id}/members", params=params)
            data = await response.json()

            users = [User.from_json(i) for i in data['members']]

            for user in users:
                yield user

            marker = data.get('marker', None)
            if marker == None:
                break

    
    async def add_members(self,
        chat_id: int,
        users: List[int]
    ):
        '''
        Adds users to the chat.

        :param chat_id: The ID of the chat.
        :param users: List of user IDs to add.
        '''

        response = await self.post(f"https://botapi.max.ru/chats/{chat_id}/members", json={"user_ids": users})

        return await response.json()
  

    async def kick_member(
        self,
        chat_id: int,
        user_id: int,
        block: "bool | None" = None
    ):
        '''
        Removes a user from the chat.

        :param chat_id: The ID of the chat.
        :param user_id: The ID of the user to remove.
        :param block: Whether to block the user. Ignored by default.
        '''

        params = {
            "chat_id": chat_id,
            "user_id": user_id,
            "block": block
        }
        params = {k: v for k, v in params.items() if v}
        
        if block != None:
            params["block"] = str(block)

        response = await self.delete(f"https://botapi.max.ru/chats/{chat_id}/members/", params=params)

        return await response.json()
    

    async def patch_chat(self,
        chat_id: int,
        icon: "ImageRequestPayload | None" = None,
        title: "str | None" = None,
        pin: "str | None" = None,
        notify: "bool | None" = None
    ) -> Chat:
        '''
        Allows you to edit chat information, like the name,
        icon and pinned message.

        :param chat_id: ID of the chat to change
        :param icon: Chat picture
        :param title: Chat name. From 1 to 200 characters
        :param pin: ID of the message to pin
        :param notify: Whether to notify users about the edit. True by default.
        '''

        payload = {
            "icon": icon.as_dict() if icon else None,
            "title": title,
            "pin": pin,
            "notify": notify
        }
        payload = {k: v for k, v in payload.items() if v}

        response = await self.patch(
            f"https://botapi.max.ru/chats/{chat_id}", json=payload
        )
        json = await response.json()

        return Chat.from_json(json)
    

    async def post_action(self, chat_id: int, action: str):
        '''
        Allows you to show a badge about performing an action in a chat, like
        "typing". Also allows for marking messages as read.
        
        :param chat_id: ID of the chat to do the action in
        :param action: The action to perform
        '''

        response = await self.post(f"https://botapi.max.ru/chats/{chat_id}/actions", json={"action": action})

        return await response.json()
    

    async def upload(self, data: "IO | str", type: str) -> dict:
        '''
        Uploads a file to the server. Returns raw JSON with the token.

        You probably should use one of the upload_* methods instead.

        :param data: File-like object or path to the file
        :param type: File type
        '''
        if isinstance(data, str):
            data = open(data, 'rb')
        
        form = aiohttp.FormData()
        form.add_field('data', data)
        url_resp = await self.post('https://botapi.max.ru/uploads', params={"type": type})
        url_json = await url_resp.json()
        token_resp = await self.session.post(url_json['url'], data=form)
        if type in {'audio', 'video'}:
            return url_json
        token_json = await token_resp.json()

        return token_json
    

    async def upload_image(self, data: "BinaryIO | str") -> PhotoAttachment:
        '''
        Uploads an image to the server and returns a PhotoAttachment.

        :param data: File-like object or path to the file
        '''
        raw_photo = await self.upload(data, 'image')
        token = list(raw_photo['photos'].values())[0]['token']
        return PhotoAttachment(PhotoPayload(token=token))
    

    async def upload_video(self, data: "BinaryIO | str") -> VideoAttachment:
        '''
        Uploads a video to the server and returns a VideoAttachment.

        :param data: File-like object or path to the file
        '''
        raw_video = await self.upload(data, 'video')
        token = raw_video['token']
        return VideoAttachment(MediaPayload(token=token))
    

    async def upload_audio(self, data: "BinaryIO | str") -> AudioAttachment:
        '''
        Uploads an audio file to the server and returns an AudioAttachment.

        :param data: File-like object or path to the file
        '''
        raw_audio = await self.upload(data, 'audio')
        token = raw_audio['token']
        return AudioAttachment(MediaPayload(token=token))
    

    async def upload_file(self, data: "IO | str") -> FileAttachment: 
        '''
        Uploads a file to the server and returns a FileAttachment.

        :param data: File-like object or path to the file
        '''
        raw_file = await self.upload(data, 'file')
        token = raw_file['token']
        return FileAttachment(MediaPayload(token=token))


    async def send_message(self,
        text: str,
        chat_id: "int | None" = None,
        user_id: "int | None" = None,
        format: "Literal['markdown', 'html', 'default'] | None" = 'default',
        reply_to: "int | None" = None,
        notify: bool = True,
        disable_link_preview: bool = False,
        keyboard: "List[List[buttons.Button]] | buttons.KeyboardBuilder | None" = None,
        attachments: "List[Attachment] | Attachment | None" = None
    ) -> Message:
        '''
        Allows you to send a message to a user or in a chat.
        
        :param text: Message text. Up to 4000 characters
        :param chat_id: Chat ID to send the message in.
        :param user_id: User ID to send the message to.
        :param format: Message format. Bot.default_format by default
        :param reply_to: ID of the message to reply to. Optional
        :param notify: Whether to notify users about the message. True by default.
        :param disable_link_preview: Whether to disable link embedding in messages. True by default
        :param keyboard: An inline keyboard to attach to the message
        :param attachments: List of attachments
        '''
        # error checking
        text = str(text)
        assert chat_id or user_id, "Either chat_id or user_id must be provided"
        assert not (chat_id and user_id), "Both chat_id and user_id cannot be provided"

        # sending
        params = {
            "chat_id": chat_id,
            "user_id": user_id,
            "disable_link_preview": str(disable_link_preview).lower()
        }
        params = {k: v for k, v in params.items() if v}

        if format == 'default':
            format = self.default_format

        body = utils.get_message_body(text, format, reply_to, notify, keyboard, attachments)

        try:
            response = await self.post(
                f"https://botapi.max.ru/messages", params=params, json=body
            )
            json = await response.json()
            message = Message.from_json(json['message'])
            message.bot = self
            return message
        
        except exceptions.AttachmentNotReady:
            await asyncio.sleep(1)
            return await self.send_message(text=text, chat_id=chat_id, user_id=user_id, format=format, reply_to=reply_to, notify=notify, disable_link_preview=disable_link_preview, attachments=attachments)


    async def edit_message(self,
        message_id: str,
        text: "str | None" = None,
        format: "Literal['markdown', 'html', 'default'] | None" = 'default',
        reply_to: "int | None" = None,
        notify: bool = True,
        keyboard: "List[List[buttons.Button]] | buttons.KeyboardBuilder | None" = None,
        attachments: "List[Attachment] | Attachment | None" = None
    ) -> Message:
        '''
        Allows you to edit a message.
        
        :param message_id: ID of the message to edit
        :param text: Message text. Up to 4000 characters
        :param format: Message format. Bot.default_format by default
        :param reply_to: ID of the message to reply to. Optional
        :param notify: Whether to notify users about the message. True by default.
        :param keyboard: An inline keyboard to attach to the message
        :param attachments: List of attachments
        '''
        # editing
        params = {
            "message_id": message_id
        }
        if format == 'default':
            format = self.default_format
            
        body = utils.get_message_body(text, format, reply_to, notify, keyboard, attachments)

        response = await self.put(
            f"https://botapi.max.ru/messages", params=params, json=body
        )
        
        json = await response.json()
        if not json['success']:
            raise Exception(json['message'])
        message = Message.from_json(json)
        message.bot = self
        return message


    async def delete_message(self,
        message_id: str
    ):
        '''
        Allows you to delete a message in chat.
        
        :param message_id: ID of the message to delete
        '''
        # editing
        params = {
            "message_id": message_id
        }

        response = await self.delete(
            f"https://botapi.max.ru/messages", params=params
        )
        
        json = await response.json()
        if not json['success']:
            raise Exception(json['message'])


    # async def get_message(self,
    #     message_id: str
    # ) -> Message:
    #     '''
    #     Allows you to fetch message's info.
        
    #     :param message_id: ID of the message to get info of
    #     '''
    #     assert message_id.startswith('mid.'), "Message ID invalid"

    #     message_id = message_id[4:]

    #     # editing
    #     response = await self.get(
    #         f"https://botapi.max.ru/messages/{message_id}"
    #     )
    #     if response.status != 200:
    #         raise Exception(await response.text())
        
    #     return Message.from_json(await response.json())
    # todo fix - for some reason API replies with "invalid message_id"


    async def get_updates(self, limit: int = 100) -> tuple[int, dict]:
        '''
        Get bot updates / events.
        
        :param limit: Maximum amount of updates to return.
        '''
        payload = {
            "limit": limit,
            "marker": self.marker
        }
        payload = {k: v for k, v in payload.items() if v}

        response = await self.get(
            f"https://botapi.max.ru/updates", params=payload
        )
        json = await response.json()
        if 'marker' in json:
            self.marker = json['marker']

        return json
    

    async def handle_update(self, update: dict):
        '''
        Handles an update.
        '''
        update_type = update['update_type'] 

        if update_type == "message_created":
            message = Message.from_json(update["message"])
            message.bot = self
            message.user_locale = update.get('user_locale', None)
            cursor = fsm.FSMCursor(self.storage, message.sender.user_id)

            # caching
            if self.cache:
                self.cache.add_message(message)

            # handling commands
            prefixes = self.command_prefixes if type(self.command_prefixes) != str\
                else [self.command_prefixes]
            prefixes = list(prefixes)
            handled = False

            if self.mention_prefix:
                prefixes.extend([f'@{self.username} {i}' for i in prefixes])

            for prefix in prefixes:
                if len(message.body.text) <= len(prefix):
                    continue

                prefix = prefix if self.case_sensitive else prefix.lower()
                if not message.body.text.startswith(prefix):
                    continue

                command = message.body.text[len(prefix):]
                name = command.split()[0]
                check_name = name if self.case_sensitive else name.lower()
                args = ' '.join(command.split()[1:])
                
                if check_name not in self.commands:
                    bot_logger.debug(f"Command \"{name}\" not handled")
                    return

                if len(self.commands[check_name]) == 0:
                    bot_logger.debug(f"Command \"{name}\" not handled")
                    return

                block = False
                
                for i in self.commands[check_name]:
                    kwargs = utils.context_kwargs(i.call, cursor=cursor)
                    asyncio.create_task(i.call(CommandContext(
                        self, message, name, args
                    ), **kwargs))

                    if not i.as_message:
                        block = True

                bot_logger.debug(f"Command \"{name}\" handled")
                if block:
                    return

            # handling
            handled = False

            for handler in self.handlers['message_created']:
                filters = [filter(message) for filter in handler.filters]

                if all(filters):
                    kwargs = utils.context_kwargs(handler.call, cursor=cursor)
                    asyncio.create_task(handler.call(message, **kwargs))
                    handled = True
            
            # handle logs
            if handled:
                bot_logger.debug(f"Message \"{message.body.text}\" handled")
            else:
                bot_logger.debug(f"Message \"{message.body.text}\" not handled")


        if update_type == 'message_edited': 
            message = Message.from_json(update["message"])
            message.bot = self
            message.user_locale = update.get('user_locale', None)
            cursor = fsm.FSMCursor(self.storage, message.sender.user_id)

            # caching
            old_message = None
            if self.cache:
                old_message = self.cache.get_message(message.id)
                self.cache.add_message(message)

            # handling
            for handler in self.handlers[update_type]:
                filters = [filter(message) for filter in handler.filters]

                if all(filters):
                    kwargs = utils.context_kwargs(handler.call, before=old_message, after=message, cursor=cursor)
                    asyncio.create_task(handler.call(old_message, message, **kwargs))

            # handle logs
            bot_logger.debug(f"Message \"{message.body.text}\" edited")


        if update_type == 'message_removed': 
            payload = MessageDeletePayload.from_json(update, self)

            if payload.user_id:
                cursor = fsm.FSMCursor(self.storage, payload.user_id)
            else:
                cursor = None

            # handling
            for handler in self.handlers[update_type]:
                filters = [filter(payload) for filter in handler.filters]

                if all(filters):
                    kwargs = utils.context_kwargs(handler.call, cursor=cursor)
                    asyncio.create_task(handler.call(payload, **kwargs))

            # handle logs
            bot_logger.debug(f"Message \"{payload.content}\" deleted")

                
        if update_type == 'bot_started':
            payload = BotStartPayload.from_json(update, self)
            cursor = fsm.FSMCursor(self.storage, payload.user.user_id)

            bot_logger.debug(f"User \"{payload.user!r}\" started bot")

            for i in self.handlers[update_type]:
                kwargs = utils.context_kwargs(i, cursor=cursor)
                asyncio.create_task(i(payload, **kwargs))

                
        if update_type == 'chat_title_changed':
            payload = ChatTitleEditPayload.from_json(update)
            cursor = fsm.FSMCursor(self.storage, payload.user.user_id)

            bot_logger.debug(f"User \"{payload.user!r}\" changed title of chat {payload.chat_id}")

            for i in self.handlers[update_type]:
                kwargs = utils.context_kwargs(i, cursor=cursor)
                asyncio.create_task(i(payload, **kwargs))

                
        if update_type == 'bot_added' or update_type == 'bot_removed':
            payload = ChatMembershipPayload.from_json(update)
            cursor = fsm.FSMCursor(self.storage, payload.user.user_id)

            for i in self.handlers[update_type]:
                kwargs = utils.context_kwargs(i, cursor=cursor)
                asyncio.create_task(i(payload, **kwargs))
                
                
        if update_type == 'user_added' or update_type == 'user_removed':
            payload = UserMembershipPayload.from_json(update)
            cursor = fsm.FSMCursor(self.storage, payload.user.user_id)

            for i in self.handlers[update_type]:
                kwargs = utils.context_kwargs(i, cursor=cursor)
                asyncio.create_task(i(payload, **kwargs))

                
        if update_type == 'message_callback':
            handled = False

            callback = Callback.from_json(
                update['callback'], update.get('message', None), update.get('user_locale', None), self
            )

            cursor = fsm.FSMCursor(self.storage, callback.user.user_id)
  
            for handler in self.handlers[update_type]:
                filters = [filter(callback) for filter in handler.filters]

                if all(filters):
                    kwargs = utils.context_kwargs(handler.call, cursor=cursor)
                    asyncio.create_task(handler.call(callback, **kwargs))
                    handled = True
                
            if handled:
                bot_logger.debug(f"Callback \"{callback.payload}\" handled")
            else:
                bot_logger.debug(f"Callback \"{callback.payload}\" not handled")

                
        if update_type == 'message_chat_created':
            payload = ChatCreatePayload.from_json(update)
            bot_logger.debug(f"Created chat \"{payload.start_payload}\"")

            for i in self.handlers[update_type]:
                asyncio.create_task(i(payload))


    async def start_polling(self, session: "aiohttp.ClientSession | None" = None):
        '''
        Starts polling.

        :param session: Custom aiohttp client session
        '''
        self.polling = True

        if not session:
            session = aiohttp.ClientSession()

        async with session:
            self.session = session

            # self info (this will cache the info automatically)
            await self.get_me()

            bot_logger.info(f"Started polling with bot @{self.username} ({self.id}) - {self.name}")

            # ready event
            for i in self.handlers['on_ready']:
                await i()

            while self.polling:
                try:
                    updates = await self.get_updates()
                    
                    for update in updates["updates"]:
                        await self.handle_update(update)

                except Exception as e:
                    bot_logger.exception(e)
                    await asyncio.sleep(3)

        self.session = None
        self.polling = False


    def run(self, *args, **kwargs):
        '''
        Shortcut for `asyncio.run(Bot.start_polling())`
        '''
        asyncio.run(self.start_polling(*args, **kwargs))