# coding: utf-8
import logging
import tempfile
import threading
import time
import uuid
from typing import Any, Dict, List

import cqhttp
from PIL import Image
from cqhttp import CQHttp
from ehforwarderbot import EFBMsg, MsgType, EFBChat, coordinator, EFBStatus
from ehforwarderbot.exceptions import EFBMessageError, EFBOperationNotSupported
from ehforwarderbot.status import EFBMessageRemoval
from ehforwarderbot.utils import extra
from requests import RequestException

from .ChatMgr import ChatManager
from .Exceptions import CoolQDisconnectedException, CoolQAPIFailureException, CoolQOfflineException, \
    CoolQUnknownException
from .MsgDecorator import QQMsgProcessor
from .Utils import qq_emoji_list, async_send_messages_to_master, process_quote_text, coolq_text_encode, \
    upload_image_smms
from ..BaseClient import BaseClient
from ... import QQMessengerChannel


class CoolQ(BaseClient):
    client_name: str = "CoolQ Client"
    client_id: str = "CoolQ"
    client_config: Dict[str, Any]

    coolq_bot: CQHttp = None
    logger: logging.Logger = logging.getLogger(__name__)
    channel: QQMessengerChannel

    friend_list = []
    group_list = []
    discuss_list = []
    repeat_counter = 0
    update_repeat_counter = 0

    def __init__(self, client_id: str, config: Dict[str, Any], channel):
        super().__init__(client_id, config)
        self.client_config = config[self.client_id]
        self.coolq_bot = CQHttp(api_root=self.client_config['api_root'],
                                access_token=self.client_config['access_token']
                                )
        self.channel = channel
        self.chat_manager = ChatManager(channel)

        self.is_connected = False
        self.is_logged_in = False
        self.msg_decorator = QQMsgProcessor(instance=self)

        @self.coolq_bot.on_message()
        def handle_msg(context):
            self.logger.debug(repr(context))
            msg_element = context['message']
            main_text: str = ''
            messages = []
            qq_uid = context['user_id']
            at_list = {}
            for i in range(len(msg_element)):
                msg_type = msg_element[i]['type']
                msg_data = msg_element[i]['data']
                if msg_type == 'text':
                    main_text += msg_data['text']
                elif msg_type == 'face':
                    qq_face = int(msg_data['id'])
                    if qq_face in qq_emoji_list:
                        main_text += qq_emoji_list[qq_face]
                    else:
                        main_text += '\u2753'  # ❓
                elif msg_type == 'sface':
                    main_text += '\u2753'  # ❓
                elif msg_type == 'at':
                    # todo Recheck if bug exists
                    g_id = context['group_id']
                    my_uid = self.get_qq_uid()
                    self.logger.debug('My QQ uid: %s\n'
                                      'QQ who is mentioned: %s\n', my_uid, msg_data['qq'])
                    group_card = ''
                    if str(msg_data['qq']) == 'all':
                        group_card = 'all'
                    else:
                        member_info = self.coolq_api_query('get_group_member_info',
                                                           group_id=g_id,
                                                           user_id=msg_data['qq'])
                        group_card = member_info['card'] if member_info['card'] != '' else member_info['nickname']
                    self.logger.debug('Group card: {}'.format(group_card))
                    substitution_begin = len(main_text) + 1
                    substitution_end = len(main_text) + len(group_card) + 2
                    main_text += ' @{} '.format(group_card)
                    if str(my_uid) == str(msg_data['qq']) or str(msg_data['qq']) == 'all':
                        at_list[(substitution_begin, substitution_end)] = EFBChat(self.channel).self()
                else:
                    messages.append(self.call_msg_decorator(msg_type, msg_data))
            if main_text != "":
                messages.append(self.msg_decorator.qq_text_simple_wrapper(main_text, at_list))
            uid: str = str(uuid.uuid4())
            coolq_msg_id = context['message_id']
            for i in range(len(messages)):
                if not isinstance(messages[i], EFBMsg):
                    continue
                efb_msg = messages[i]
                efb_msg.uid = uid + '_' + str(coolq_msg_id) + '_' + str(i)
                # if qq_uid != '80000000':
                if 'anonymous' not in context or context['anonymous'] is None:
                    if context['message_type'] == 'group':
                        if context['sub_type'] == 'notice':
                            context['event_description'] = "System Notification"
                            efb_msg.author = self.chat_manager.build_efb_chat_as_system_user(context)
                        else:
                            g_id = context['group_id']
                            context['alias'] = self.coolq_api_query('get_group_member_info',
                                                                    group_id=g_id,
                                                                    user_id=qq_uid)['card']
                            efb_msg.author: EFBChat = self.chat_manager.build_efb_chat_as_user(context, False)
                    else:
                        if context['message_type'] == 'private':
                            context['alias'] = self.get_friend_remark(qq_uid)
                        efb_msg.author: EFBChat = self.chat_manager.build_efb_chat_as_user(context, False)
                else:  # anonymous user in group
                    efb_msg.author: EFBChat = self.chat_manager.build_efb_chat_as_anonymous_user(context)

                if context['message_type'] == 'private':
                    efb_msg.chat: EFBChat = self.chat_manager.build_efb_chat_as_user(context, True)
                else:
                    efb_msg.chat: EFBChat = self.chat_manager.build_efb_chat_as_group(context)

                if context['message_type'] == 'discuss':
                    self.discuss_list.append(efb_msg.chat)

                efb_msg.deliver_to = coordinator.master

                def send_message_wrapper(*args, **kwargs):
                    threading.Thread(target=async_send_messages_to_master, args=args, kwargs=kwargs).start()

                send_message_wrapper(efb_msg)

        @self.coolq_bot.on_notice('group_increase')
        def handle_group_increase_msg(context):
            context['event_description'] = '\u2139 Group Member Increase Event'
            text = '{nickname}({context[user_id]}) joined the group({group_name})'

            original_group = self.get_group_info(context['group_id'], False)
            group_name = context['group_id']
            if original_group is not None and 'group_name' in original_group:
                group_name = original_group['group_name']
            text = text.format(nickname=self.get_stranger_info(context['user_id'])['nickname'],
                               context=context,
                               group_name=group_name)
            if (context['sub_type']) == 'invite':
                text += ' via invitation'
            context['message'] = text
            self.send_efb_group_notice(context)

        @self.coolq_bot.on_notice('group_decrease')
        def handle_group_decrease_msg(context):
            context['event_description'] = "\u2139 Group Member Decrease Event"
            original_group = self.get_group_info(context['group_id'], False)
            group_name = context['group_id']
            if original_group is not None and 'group_name' in original_group:
                group_name = original_group['group_name']
            text = ''
            if context['sub_type'] == 'kick_me':
                text = "You've been kicked from the group" \
                       + '({})'.format(group_name)
            else:
                quit_msg = 'quited the group' if context['sub_type'] == 'leave' else 'was kicked from the group'
                text = '{nickname}({context[user_id]}) {quit_msg}({group_name})'
                text = text.format(nickname=self.get_stranger_info(context['user_id'])['nickname'],
                                   context=context,
                                   quit_msg=quit_msg,
                                   group_name=group_name)
            context['message'] = text
            self.send_efb_group_notice(context)

        @self.coolq_bot.on_notice('group_upload')
        def handle_group_file_upload_msg(context):
            context['event_description'] = "\u2139 Group File Upload Event"

            original_group = self.get_group_info(context['group_id'], False)
            group_name = context['group_id']
            if original_group is not None and 'group_name' in original_group:
                group_name = original_group['group_name']

            file_info_msg = 'File ID: {file[id]} Filename: {file[name]} File size: {file[size]}'.format(
                file=context['file'])
            member_info = self.coolq_api_query('get_group_member_info',
                                               group_id=context['group_id'],
                                               user_id=context['user_id'])
            group_card = member_info['card'] if member_info['card'] != '' else member_info['nickname']
            text = '{member_card}({context[user_id]}) uploaded a file to group({group_name})\n'
            text = text.format(member_card=group_card,
                               context=context,
                               group_name=group_name) + file_info_msg
            context['message'] = text
            self.send_efb_group_notice(context)

        @self.coolq_bot.on_notice('friend_add')
        def handle_friend_add_msg(context):
            context['event_description'] = '\u2139 New Friend Event'
            context['uid_prefix'] = 'friend_add'
            text = '{nickname}({context[user_id]}) has become your friend!'
            text = text.format(nickname=self.get_stranger_info(context['user_id'])['nickname'],
                               context=context)
            context['message'] = text
            self.send_msg_to_master(context)

        @self.coolq_bot.on_request('group', 'friend')
        def handle_request(context):
            self.logger.debug(repr(context))
            # todo

        self.run_instance(host=self.client_config['host'], port=self.client_config['port'], debug=False)

        # threading.Thread(target=self.check_running_status).start()
        self.check_status_periodically(threading.Event())

        threading.Timer(1800, self.update_contacts_periodically, [threading.Event()]).start()

    def run_instance(self, *args, **kwargs):
        threading.Thread(target=self.coolq_bot.run, args=args, kwargs=kwargs, daemon=True).start()

    def relogin(self):
        self.coolq_api_query('set_restart')
        # self.coolq_bot.set_restart()

    def logout(self):
        raise NotImplementedError

    @extra(name="Check CoolQ Status",
           desc="Force efb-qq-slave to refresh status from CoolQ Client.\n"
                "Usage: {function_name}")
    def login(self, param: str = ""):
        self.check_status_periodically(None)
        return 'Done'

    def get_stranger_info(self, user_id):
        return self.coolq_api_query('get_stranger_info', user_id=user_id, no_cache=False)
        # return self.coolq_bot.get_stranger_info(user_id=user_id, no_cache=False)

    def get_login_info(self) -> Dict[Any, Any]:
        res = self.coolq_bot.get_status()
        if 'good' in res or 'online' in res:
            data = self.coolq_bot.get_login_info()
            return {'status': 0, 'data': {'uid': data['user_id'], 'nickname': data['nickname']}}
        else:
            return {'status': 1}

    def get_groups(self) -> List:
        # todo Add support for discuss group iteration
        self.update_group_list()  # Force update group list
        res = self.group_list
        # res = self.coolq_bot.get_group_list()
        groups = []
        for i in range(len(res)):
            context = {'message_type': 'group',
                       'group_id': res[i]['group_id']}
            efb_chat = self.chat_manager.build_efb_chat_as_group(context)
            groups.append(efb_chat)
        return groups + self.discuss_list

    def get_friends(self) -> List:
        # Warning: Experimental API
        try:
            self.update_friend_list()  # Force update friend list
        except CoolQAPIFailureException:
            self.deliver_alert_to_master('Failed to retrieve the friend list.\n'
                                         'Only groups are shown.')
            return []
        res = self.friend_list
        # res = self.coolq_bot._get_friend_list()
        users = []
        for i in range(len(res)):  # friend group
            for j in range(len(res[i]['friends'])):
                current_user = res[i]['friends'][j]
                txt = '[{}] {}'
                txt = txt.format(res[i]['friend_group_name'], current_user['remark'])
                if current_user['nickname'] == current_user['remark']:  # no remark name
                    context = {'user_id': str(current_user['user_id']),
                               'nickname': txt,
                               'alias': None}
                else:
                    context = {'user_id': str(current_user['user_id']),
                               'nickname': current_user['nickname'],
                               'alias': txt}
                efb_chat = self.chat_manager.build_efb_chat_as_user(context, True)
                users.append(efb_chat)
        return users

    def receive_message(self):
        # Replaced by handle_msg()
        pass

    def send_message(self, msg: EFBMsg):
        # todo Add support for edited message
        """
        self.logger.info("[%s] Sending message to WeChat:\n"
                         "uid: %s\n"
                         "Type: %s\n"
                         "Text: %s\n"
                         "Target Chat: %s\n"
                         "Target uid: %s\n",
                         msg.uid,
                         msg.chat.chat_uid, msg.type, msg.text, repr(msg.target.chat), msg.target.uid)
        """
        m = QQMsgProcessor(instance=self)
        chat_type = msg.chat.chat_uid.split('_')

        self.logger.debug('[%s] Is edited: %s', msg.uid, msg.edit)
        if msg.edit:
            if self.client_config['is_pro']:
                try:
                    uid_type = msg.uid.split('_')
                    self.recall_message(uid_type[1])
                except CoolQAPIFailureException:
                    raise EFBOperationNotSupported("Failed to recall the message!\n"
                                                   "This message may have already expired.")

        if msg.type in [MsgType.Text, MsgType.Link]:
            if isinstance(msg.target, EFBMsg):
                max_length = -1  # todo
                tgt_text = coolq_text_encode(process_quote_text(msg.target.text, max_length))
                user_type = msg.target.author.chat_uid.split('_')
                tgt_alias = ""
                if chat_type[0] != 'private' and not msg.target.author.is_self:
                    tgt_alias += m.coolq_code_at_wrapper(user_type[1])
                else:
                    tgt_alias = ""
                msg.text = "%s%s\n\n%s" % (tgt_alias, tgt_text, coolq_text_encode(msg.text))
            msg.uid = self.coolq_send_message(chat_type[0], chat_type[1], msg.text)
            self.logger.debug('[%s] Sent as a text message. %s', msg.uid, msg.text)
        elif msg.type in (MsgType.Image, MsgType.Sticker):
            self.logger.info("[%s] Image/Sticker %s", msg.uid, msg.type)
            text = ''
            if not self.client_config['is_pro']:  # CoolQ Air
                if self.client_config['air_option']['upload_to_smms']:
                    text = '[Image] {}'
                    smms_data = None
                    try:
                        smms_data = upload_image_smms(msg.file, msg.path)
                    except CoolQUnknownException as e:
                        text = '[Image]'
                        self.deliver_alert_to_master('Failed to upload the image to sm.ms! Return Msg: '
                                                     + getattr(e, 'message', repr(e)))
                    else:
                        if smms_data is not None:
                            text = text.format(smms_data['url'])
                else:
                    text = '[Image]'
            else:
                if msg.type != MsgType.Sticker:
                    text += m.coolq_code_image_wrapper(msg.file, msg.path)
                else:
                    with tempfile.NamedTemporaryFile(suffix=".gif") as f:
                        img = Image.open(msg.file)
                        try:
                            alpha = img.split()[3]
                            mask = Image.eval(alpha, lambda a: 255 if a <= 128 else 0)
                        except IndexError:
                            mask = Image.eval(img.split()[0], lambda a: 0)
                        img = img.convert('RGB').convert('P', palette=Image.ADAPTIVE, colors=255)
                        img.paste(255, mask)
                        img.save(f, transparency=255)
                        msg.file.close()
                        f.seek(0)
                        text += m.coolq_code_image_wrapper(f, f.name)
            msg.uid = self.coolq_send_message(chat_type[0], chat_type[1], text)
            if msg.text:
                self.coolq_send_message(chat_type[0], chat_type[1], msg.text)
        # todo More MsgType Support
        return msg

    def call_msg_decorator(self, msg_type: str, *args):
        func = getattr(self.msg_decorator, 'qq_{}_wrapper'.format(msg_type))
        return func(*args)

    def get_qq_uid(self):
        res = self.get_login_info()
        if res['status'] == 0:
            return res['data']['uid']
        else:
            return None

    def get_group_member_info(self, group_id, user_id):
        res = self.coolq_api_query('get_group_member_info', group_id=group_id, user_id=user_id, no_cache=True)
        # res = self.coolq_bot.get_group_member_info(group_id=group_id, user_id=user_id, no_cache=True)
        return res
        pass

    def get_group_info(self, group_id, no_cache=True):
        if no_cache or not self.group_list:
            self.group_list = self.coolq_api_query('get_group_list')
        res = self.group_list
        # res = self.coolq_bot.get_group_list()
        for i in range(len(res)):
            if res[i]['group_id'] == group_id:
                return res[i]
        return None

    def coolq_send_message(self, msg_type, uid, message):
        keyword = msg_type if msg_type != 'private' else 'user'
        res = self.coolq_api_query('send_msg', message_type=msg_type, **{keyword + '_id': uid}, message=message)
        # res = self.coolq_bot.send_msg(message_type=msg_type, **{keyword + '_id': uid}, message=message)
        return str(uuid.uuid4()) + '_' + str(res['message_id'])

    def _coolq_api_wrapper(self, func_name, **kwargs):
        try:
            func = getattr(self.coolq_bot, func_name)
            res = func(**kwargs)
        except RequestException as e:
            raise CoolQDisconnectedException('Unable to connect to CoolQ Client! Error Message:\n{}'.format(repr(e)))
        except cqhttp.Error as ex:
            api_ex = CoolQAPIFailureException('CoolQ HTTP API encountered an error!\n'
                                              'Status Code:{} Return Code:{}'.format(ex.status_code, ex.retcode))
            # if ex.status_code == 200 and ex.retcode == 104:  # Cookie expired
            setattr(api_ex, 'status_code', ex.status_code)
            setattr(api_ex, 'retcode', ex.retcode)
            raise api_ex
        else:
            return res

    def check_running_status(self):
        res = self._coolq_api_wrapper('get_status')
        if res['good'] or res['online']:
            return True
        else:
            raise CoolQOfflineException("CoolQ Client isn't working correctly!")

    def coolq_api_query(self, func_name, **kwargs):
        """ # Do not call get_status too frequently
        if self.check_running_status():
            return self._coolq_api_wrapper(func_name, **kwargs)
        """
        if self.is_logged_in and self.is_connected:
            return self._coolq_api_wrapper(func_name, **kwargs)
        elif self.repeat_counter < 3:
            self.deliver_alert_to_master('Your status is offline.\n'
                                         'You may try login with /0_login')
            self.repeat_counter += 1

    def check_status_periodically(self, t_event):
        self.logger.debug('Start checking status...')
        flag = True
        interval = 300
        try:
            flag = self.check_running_status()
        except CoolQDisconnectedException as e:
            if self.repeat_counter < 3:
                self.deliver_alert_to_master("We're unable to communicate with CoolQ Client.\n"
                                             "Please check the connection and credentials provided.\n"
                                             "{}".format(repr(e)))
                self.repeat_counter += 1
            self.is_connected = False
            self.is_logged_in = False
            interval = 3600
        except (CoolQOfflineException, CoolQAPIFailureException):
            if self.repeat_counter < 3:
                self.deliver_alert_to_master('CoolQ is running in abnormal status.\n'
                                             'You may need to relogin your account or have a check in CoolQ Client.\n')
                self.repeat_counter += 1
            self.is_connected = True
            self.is_logged_in = False
            interval = 3600
        else:
            if not flag:
                if self.repeat_counter < 3:
                    self.deliver_alert_to_master("We don't know why, but status check failed.\n"
                                                 "Please enable debug mode and consult the log for more details.")
                    self.repeat_counter += 1
                self.is_connected = True
                self.is_logged_in = False
                interval = 3600
            else:
                self.logger.debug('Status: OK')
                self.is_connected = True
                self.is_logged_in = True
                self.repeat_counter = 0

        if t_event is not None and not t_event.is_set():
            threading.Timer(interval, self.check_status_periodically, [t_event]).start()

    def deliver_alert_to_master(self, message: str):
        context = {'message': message, 'uid_prefix': 'alert', 'event_description': 'CoolQ Alert'}
        self.send_msg_to_master(context)

    def update_friend_list(self):
        # Warning: Experimental API
        self.friend_list = self.coolq_api_query('_get_friend_list')
        self.logger.debug('Update friend list completed. Entries: %s', len(self.friend_list))

    def update_group_list(self):
        self.group_list = self.coolq_api_query('get_group_list')
        self.logger.debug('Update group list completed. Entries: %s', len(self.group_list))

    def update_contacts_periodically(self, t_event):
        self.logger.debug('Start updating friend & group list')
        interval = 1800
        if self.is_connected and self.is_logged_in:
            try:
                self.update_friend_list()
                self.update_group_list()
            except CoolQAPIFailureException as ex:
                if getattr(ex, 'status_code') == 200 and getattr(ex, 'retcode') == 104 and self.update_repeat_counter < 3:
                    self.send_cookie_expired_alarm()
                if self.update_repeat_counter < 3:
                    self.deliver_alert_to_master('Errors occurred when updating contacts: ' + getattr(ex, 'message'))
                    self.update_repeat_counter += 1
            else:
                self.update_repeat_counter = 0
        self.logger.debug('Update completed')
        if t_event is not None and not t_event.is_set():
            threading.Timer(interval, self.update_contacts_periodically, [t_event]).start()

    def get_friend_remark(self, uid):
        if not self.friend_list:
            try:
                self.update_friend_list()
            except CoolQAPIFailureException:
                self.deliver_alert_to_master('Failed to obtain friend remark name')
                return ''
        for i in range(len(self.friend_list)):  # friend group
            for j in range(len(self.friend_list[i]['friends'])):
                current_user = self.friend_list[i]['friends'][j]
                if current_user['user_id'] != uid:
                    continue
                if current_user['nickname'] == current_user['remark'] or current_user['nickname'] == '':
                    # no remark name
                    return current_user['nickname']
                else:
                    return current_user['remark']
        return ''

    def send_efb_group_notice(self, context):
        context['message_type'] = 'group'
        self.logger.debug(repr(context))
        msg = EFBMsg()
        msg.author = self.chat_manager.build_efb_chat_as_system_user(context)
        msg.chat = self.chat_manager.build_efb_chat_as_group(context)
        msg.deliver_to = coordinator.master
        msg.type = MsgType.Text
        msg.uid = "__group_notice__.%s" % int(time.time())
        msg.text = context['message']
        coordinator.send_message(msg)

    def send_msg_to_master(self, context):
        self.logger.debug(repr(context))
        msg = EFBMsg()
        msg.chat = msg.author = self.chat_manager.build_efb_chat_as_system_user(context)
        msg.deliver_to = coordinator.master
        msg.type = MsgType.Text
        msg.uid = "__{context[uid_prefix]}__.{uni_id}".format(context=context,
                                                              uni_id=str(int(time.time())))
        msg.text = context['message']
        coordinator.send_message(msg)

    # As the old saying goes
    # A programmer spent 20% of time on coding
    # while the rest 80% on considering a variable/function/class name
    # todo Deprecated
    def get_external_group_info(self, group_id):  # Special thanks to @lwl12 for thinking of this name
        res = self.coolq_api_query('_get_group_info',
                                   group_id=group_id)
        return res

    def send_status(self, status: 'EFBStatus'):
        if isinstance(status, EFBMessageRemoval):
            if not status.message.author.is_self:
                raise EFBMessageError('You can only recall your own messages.')
            try:
                uid_type = status.message.uid.split('_')
                self.recall_message(uid_type[1])
            except CoolQAPIFailureException:
                raise EFBMessageError(
                    'Failed to recall the message. This message may have already expired..')
        else:
            raise EFBOperationNotSupported()
        # todo

    def recall_message(self, message_id):
        self.coolq_api_query('delete_msg',
                             message_id=message_id)

    def send_cookie_expired_alarm(self):
        self.deliver_alert_to_master('Your cookie of CoolQ Client seems to be expired. '
                                     'Although it will not affect the normal functioning of sending/receiving '
                                     'messages, however, you may encounter issues like failing to retrieve '
                                     'friend list. Please consult '
                                     'https://github.com/milkice233/efb-qq-slave/wiki/Workaround-for-expired'
                                     '-cookies-of-CoolQ for solutions.')
