import asyncio
import logging
import pathlib
import string
import time
import uuid
from pathlib import Path
from typing import List, TypeVar, Optional, Union, Dict

import aiofiles
import aiohttp
import qrcode
from aiohttp import ClientTimeout
from websockets.exceptions import ConnectionClosedOK, WebSocketException

from pyromax.api import MaxClient
from pyromax.exceptions import LoggingError, LoggingTimeoutError, SendMessageError, SendMessageFileError, \
    SendMessageNotFoundError
from pyromax.mixins import AsyncInitializerMixin
from pyromax.types import Chat, Opcode, Video, File, Photo, Audio
from pyromax.utils import get_dict_value_by_path
from pyromax.utils import get_random_string
from pyromax.utils.write_token import read_token, write_token

# pprint(dir(Connection))

T = TypeVar('T')
root_dir = Path.cwd()


class MaxApi(AsyncInitializerMixin):
    async def _async_init(self, *args, device_id: str = None, token: str = None,
                          global_context: dict[type[T], T] = None, ping_interval=30, **kwargs):
        if not global_context:
            default_args = {}

        token = await read_token()
        self.__init__(device_id=device_id, token=token, global_context=global_context, ping_interval=ping_interval,
                      *args, **kwargs)

        while True:
            try:
                await self.attach()
                await self.login()
                await self._authorize()
                break
            except WebSocketException:
                await self.detach()
                self.__logger.info('WebSocket cannot init, retrying...')
        self.__logger.info('MaxApi get ready for use')

    async def reload_if_connection_broke(self, dispatcher):
        while True:
            tasks = []
            try:
                tasks.append(asyncio.create_task(dispatcher.start_polling(self)))
                tasks.append(asyncio.create_task(self._keepalive()))

                done, pending = await asyncio.wait(
                    tasks,
                    return_when=asyncio.FIRST_EXCEPTION
                )

                for task in done:
                    if task.exception():
                        raise task.exception()
            except WebSocketException:
                await self.max_client.kill_pending()

                for task in tasks:
                    if not task.done():
                        task.cancel()
                if tasks:
                    await asyncio.gather(*tasks, return_exceptions=True)
                self.__logger.warning('WebSocket connection broke')
                await self.detach()
                await self.attach()
                await self.send_user_agent()
                await self._authorize()

    def __init__(self, device_id: str = None, token: str = None, ping_interval=30, global_context=None, *args,
                 **kwargs) -> None:
        if not global_context:
            global_context = {}
        self.__max_client: MaxClient | None = None
        self._ping_interval: int = ping_interval
        self._track_id: int | None = None
        self.first_name: str | None = None
        self.last_name: str | None = None
        self.name: str | None = None
        self.type: str | None = None
        self.phone: str | None = None
        self.update_time: int | None = None
        self.id: int | None = None
        self.account_status: str | None = None
        self.__logger: logging.Logger = logging.getLogger('MaxApi')
        self.chats: list[Chat] | None = None
        self.start_time = time.time()
        self.__token = token if token else None
        self.device_id = device_id if device_id else self.get_random_device_id()
        self.client_is_login: bool = True if self.__token else False
        self.base_data = {
            MaxApi: self
        }
        self.base_data.update(
            global_context
        )
        self._infinite_recv_task: asyncio.Task | None = None

    # def me(self):
    #     return

    @property
    def max_client(self) -> MaxClient:
        return self.__max_client

    @max_client.setter
    def max_client(self, value: MaxClient) -> None:
        self.__max_client = value

    async def attach(self) -> None:
        if not self.max_client:
            self.max_client = await MaxClient(max_api=self)
            self.start_time = time.time()

    async def detach(self):
        if self.max_client:
            await self.max_client.close_websocket()
            self.max_client = None
        return self.__token, self.device_id, self._ping_interval

    async def _keepalive(self):
        self.__logger.info('Starting keepalive')
        while True:
            self.__logger.debug('Sending keep-alive ping')
            ping = await self.max_client.send_and_receive(opcode=Opcode.PING.value, payload={
                'interactive': False,
            })
            self.__logger.debug('Received keep-alive pong')
            await asyncio.sleep(self._ping_interval)

    @staticmethod
    def get_random_device_id():
        random_string = list(get_random_string(32, string.ascii_lowercase + string.digits))

        for inx in [8, 13, 18, 23]:
            random_string.insert(inx, '-')

        return ''.join(random_string)

    async def send_user_agent(self) -> tuple[int, int, int, str] | None:
        await self.max_client.send_and_receive(opcode=Opcode.SEND_USER_AGENT.value, payload={
            "userAgent": {"deviceType": "WEB", "locale": "ru", "deviceLocale": "ru", "osVersion": "Alpha",
                          "deviceName": "MaxBot",
                          "headerUserAgent": 'Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0',
                          "appVersion": "25.12.14", "screen": "1440x2560 1.0x", "timezone": "Europe/Moscow"},
            "deviceId": self.device_id}, )

        if self.client_is_login:
            return

        response = await self.max_client.send_and_receive(opcode=Opcode.METADATA_FOR_LOGIN.value, payload='NotSend')

        payload = response[0][0]["payload"]

        polling_interval = payload['pollingInterval'] / 1000
        track_id = payload['trackId']
        expires_at = payload['expiresAt'] / 1000
        url = payload['qrLink']

        return polling_interval, track_id, expires_at, url

    async def login(self):
        async def url_callback_for_login_url(url: str):
            """
            Creating a QR code scanned by max. It is displayed immediately in the console

            Args:
                url - authorization url

            """

            qr = qrcode.QRCode()
            qr.add_data(url)

            qr.make(fit=True)
            qr.print_ascii(invert=True)

        try:
            self.__logger.info('Start Login')

            try:
                metadata = await self.send_user_agent()

                if metadata:
                    self._polling_interval, self._track_id, expires_at, url = metadata
                else:
                    return True
            except KeyError:
                self.__logger.error('Not found attributes in json')
                raise LoggingError('Not found attributes in json')
            self.max_client.websocket.ping_interval = self._polling_interval

            await asyncio.create_task(url_callback_for_login_url(url))

            try:
                async with asyncio.timeout(expires_at - time.time()):
                    while not self.client_is_login:

                        response = await self.max_client.send_and_receive(opcode=Opcode.TRACK_LOGIN.value,
                                                                          payload={"trackId": self._track_id})
                        if response[0][0]["payload"]["status"].get("loginAvailable") is True:
                            self.__logger.info('Login Successful')
                            self.client_is_login = True
                            break
                        await asyncio.sleep(self._polling_interval)
            except asyncio.TimeoutError:
                self.__logger.error('Login Timeout')
                raise LoggingTimeoutError('Login Timeout')
            await self._get_user_data()

            return True
        except ConnectionClosedOK:
            self.__logger.info('Login Failed')
            raise LoggingError('Connection closed')

    async def _parse_user_data(self, response: dict) -> None:
        contact = get_dict_value_by_path('payload profile contact', response)
        names = get_dict_value_by_path('names', contact)
        self.id = get_dict_value_by_path('id', contact)
        self.name = get_dict_value_by_path('name', names)
        self.first_name = get_dict_value_by_path('firstName', names)
        self.last_name = get_dict_value_by_path('lastName', names)
        self.type = get_dict_value_by_path('type', names)
        self.phone = get_dict_value_by_path('phone', contact)
        self.update_time = get_dict_value_by_path('updateTime', contact)

    async def _get_user_data(self) -> None:
        self.__logger.info('Getting User Data...')
        response = await self.max_client.send_and_receive(opcode=Opcode.GET_USER_DATA.value,
                                                          payload={'trackId': self._track_id})
        self.__token = get_dict_value_by_path('payload tokenAttrs LOGIN token', response[0][0])
        await self._parse_user_data(response)

    async def _authorize(self) -> None:
        self.__logger.info('Sending authorize request...')

        response, seq = await self.max_client.send_and_receive(opcode=Opcode.AUTHORIZE.value, payload={
            'interactive': False,
            'token': self.__token,
        })

        await self.max_client.wait_recv(seq - 1, cmd=0)
        token = get_dict_value_by_path('payload token', response[0])
        if token:
            self.__token = str(token)
            await write_token(self.__token)
        await self._parse_user_data(response[0])

        json_chats = response[0]['payload']['chats']

        chats = Chat.from_json(json_chats, self)

        self.chats = chats
        self.__logger.info('Authorized')

    async def get_chat_per_id(self, chat_id: str):
        self.__logger.info('Getting chat info...')
        response = await self.max_client.send_and_receive(opcode=Opcode.GET_CHAT.value, payload={
            'chatIds': [chat_id],
        })

        response = response[0]

        self.__logger.info('Got chat info')

        return Chat(**response['payload']['chats'][0], max_api=self)

    async def send_message(self, chat_id: int, text: str, attaches: List[Video | File | Photo | Audio] = [],
                           other_message_elements: dict = None):
        types_of_attachments = {
            Video: 'VIDEO',
            File: 'FILE',
            Photo: 'PHOTO',
            Audio: 'AUDIO',
        }

        required_params_for_type = {
            'VIDEO': (('videoId', 'video_id'), ('token',)),
            'FILE': (('fileId', 'file_id'),),
            'PHOTO': (('photoToken', 'photo_token'),),
            'AUDIO': (('token', 'audio_token'),),
        }

        loaded_attachments = []
        for attachment in attaches:
            type_of_attachment = types_of_attachments[type(attachment)]

            payload = {
                '_type': type_of_attachment,
            }

            for param in required_params_for_type[type_of_attachment]:
                if len(param) == 1:
                    payload[param[0]] = getattr(attachment, param[0])
                elif len(param) == 2:
                    payload[param[0]] = getattr(attachment, param[1])

            loaded_attachments.append(payload)

        payload = {
            'chatId': chat_id,
            'message': {
                'cid': -round(time.time() * 1000),
                'attaches': loaded_attachments,
            },
        }
        if other_message_elements:
            payload['message'].update(other_message_elements)

        if text:
            payload['message']['text'] = text

        response = await self.max_client.send_and_receive(opcode=Opcode.SEND_MESSAGE.value, payload=payload)
        while error_if_exist := get_dict_value_by_path('payload error', response):
            error_message = get_dict_value_by_path("payload message", response)
            title = get_dict_value_by_path("payload title", response)
            match error_if_exist:
                case 'attachment.not.ready':
                    response = await self.max_client.send_and_receive(opcode=Opcode.SEND_MESSAGE.value, payload=payload)
                    continue
                case 'proto.payload':
                    raise SendMessageFileError(
                        f'''
                        title: {title},
                        error: {error_if_exist},
                        message: {error_message}
                        '''
                    )
                case 'not.found':
                    raise SendMessageNotFoundError(
                        f'''
                        title: {title},
                        error: {error_if_exist},
                        message: {error_message}
                        '''
                    )
                case _:
                    raise SendMessageError(
                        f'''
                        title: {title},
                        error: {error_if_exist},
                        message: {error_message}
                        '''
                    )

    @classmethod
    async def __preparation_data(cls, file: Union[File, Video, Photo, Audio], chat_id: int, message_id: str) -> tuple[
                                                                                                             str, int, dict] | None:
        """
        Prepares data for sending message
        Args:
            file:
            chat_id:
            message_id:

        Returns:
            None if Photo or Audio
            Tuple if File or Video
        """

        if isinstance(file, Photo) or isinstance(file, Audio):
            # Всё есть уже в объекте
            return None


        elif isinstance(file, File):
            path_for_url = "payload url"
            opcode: int = Opcode.DOWNLOAD_FILE.value
            payload: Dict = {
                "fileId": file.file_id,
                "chatId": chat_id,
                "messageId": message_id
            }

        elif isinstance(file, Video):
            path_for_url = "payload MP4_720"

            opcode: int = Opcode.DOWNLOAD_VIDEO.value
            payload = {
                "videoId": file.video_id,
                "chatId": chat_id,
                "messageId": message_id
            }

        else:
            raise TypeError("Unknown media type")

        final_result = (path_for_url, opcode, payload)

        return final_result

    @classmethod
    async def __download_file(cls, url: str, path_for_upload: Optional[str | Path] = None) -> Union[
        bytes, str]:
        """

        Args:
            url: url for download

            path_for_upload: path for save file if None, then it is saved in a byte stream

        Returns:

        """

        chunks: list[bytes] = []
        headers = {
            "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0",
            "Accept": "*/*",
            "Referer": "https://ok.ru/",
        }

        cookies = {
            "tstc": "p"
        }
        # Для видео передаю заголовки и куки, т.к видео с серваков одноклассников
        async with aiohttp.ClientSession(headers=headers, cookies=cookies, timeout=ClientTimeout(total=60)) as session:
            async with session.get(url) as response:

                if response.status != 200:
                    raise RuntimeError("Download failed")

                if path_for_upload:
                    pth_upload = pathlib.Path(path_for_upload)
                    filename = response.headers.get("X-File-Name")

                    if filename is None or pathlib.Path.exists(pth_upload / filename):
                        filename_first = f"{uuid.uuid4()}"
                    else:
                        filename_first = filename
                    # Файл уже получает расширение без нативного расширения. Костыль, но можно изменить
                    async with aiofiles.open(pth_upload / filename_first, 'wb') as f:
                        async for chunk in response.content.iter_chunked(8192):
                            await f.write(chunk)
                    return filename_first
                else:
                    async for chunk in response.content.iter_chunked(8192):
                        chunks.append(chunk)
                    return b"".join(chunks)

    async def download_file(self, file: Union[Photo, Audio, File, Video], chat_id: int, message_id: str,
                            path_for_download: Optional[str | Path] = None) -> Union[bytes, str]:

        """

        Args:
            file: file object
            chat_id: chat id for download
            message_id: message id for download
            path_for_download: path for download file
        Returns:

        """
        data_for_get_url = await self.__preparation_data(file, chat_id, message_id)
        if data_for_get_url:
            path_for_url, opcode, payload = data_for_get_url[0], data_for_get_url[1], data_for_get_url[2]

            response = await self.max_client.send_and_receive(opcode=opcode, payload=payload)

            url_for_download = get_dict_value_by_path(
                path=path_for_url,
                data=response[0][0],
            )
        else:
            url_for_download = file.url
        result = await self.__download_file(url=url_for_download,
                                            path_for_upload=path_for_download)
        return result


async def main():
    logging.basicConfig(level=logging.INFO)


if __name__ == '__main__':
    asyncio.run(main())
