import asyncio
import threading
import time

import re
from mailbox import Message
from typing import TYPE_CHECKING

from discord import Activity, ActivityType, Message, Embed

from core.aochat.BaseModule import BaseModule
from core.chat_blob import ChatBlob
from core.command_param_types import Const

from core.db import DB
from core.decorators import instance, command, event, timerevent
from core.dict_object import DictObject
from core.logger import Logger
from modules.core.ban.ban_service import BanService

if TYPE_CHECKING:
    from modules.core.discord.discord_controller import DiscordController
    from core.message_hub_service import MessageHubService
    from core.command_service import CommandService


@instance()
class DiscordCommandHandler(BaseModule):
    DISCORD_CHANNEL = "discord"

    def __init__(self):
        self.logger = Logger(__name__)

    def inject(self, registry):
        self.bot = registry.get_instance("bot")
        self.discord: DiscordController = registry.get_instance("discord_controller")
        self.db: DB = registry.get_instance("db")
        self.relay_hub_service: MessageHubService = registry.get_instance("message_hub_service")
        self.command_service: CommandService = registry.get_instance("command_service")

    def pre_start(self):
        self.command_service.register_command_channel("Discord", self.DISCORD_CHANNEL)

    @event("discord_command", "should the bot take care of discord commands", False)
    def discord_command_handler(self, _, event_data):
        acc = event_data.account
        ctx: Message = event_data.message
        message = ctx.clean_content
        if message.startswith(self.discord.setting_service.get("symbol").get_value()):
            threading.Thread(target=self.command_service.process_command(
                self.command_service.trim_command_symbol(message),
                self.DISCORD_CHANNEL,
                acc.main,
                lambda msg: self.send_response(ctx, msg),
                self.discord.bot.get_conn(),
                False), daemon=True).start()

    def send_response(self, ctx: Message, reply):
        rsp = ""
        embeds = []
        if type(reply) == str:
            reply = self.discord.text.format_message(reply)
            rsp = f"> {self.parseDiscord(reply)}"
            self.discord.relay_hub_service.send_message(f"Discord_({ctx.channel.name})",
                                                        DictObject({'char_id': 0, 'name': ctx.author.name,
                                                                    'discord_handle':
                                                                        f'{ctx.author.name}#{ctx.author.discriminator}'}),
                                                        reply, reply)
        if type(reply) == ChatBlob:
            if reply.embed:
                embeds.append(Embed(title=self.parseDiscord(f"{reply.page_prefix} {reply.title} {reply.page_postfix}"),
                                    color=0x00FF00,
                                    description=self.parseDiscord(reply.msg).replace("\n> ", '\n')))

            else:
                reply: ChatBlob
                rsp = self.parseDiscord(reply.page_prefix) + "\n"
                if not (self.discord.text.strip_html_tags(reply.msg) or reply.msg).startswith(reply.title):
                    rsp += f"**__{reply.title}__**\n"
                rsp += "> " + self.parseDiscord(reply.msg)
                rsp += f"\n {self.parseDiscord(reply.page_postfix)}"
            self.discord.relay_hub_service.send_message(f"Discord_({ctx.channel.name})",
                                                        DictObject({'char_id': 0, 'name': ctx.author.name,
                                                                    'discord_handle':
                                                                        f'{ctx.author.name}#{ctx.author.discriminator}'}),
                                                        reply, reply)
        if len(rsp) > 2000:
            rsp = self.discord.text.split_by_separators(rsp, 2000)
            for page in rsp:
                self.discord.client.loop.create_task(ctx.reply(page))
        else:
            self.discord.client.loop.create_task(ctx.reply(rsp, embeds=embeds))

    def parseDiscord(self, ctx: str):
        proficon = {1: "sold", 2: "ma", 3: "eng", 4: "fixer", 5: "agent", 6: "adv", 7: "trad", 8: "crat", 9: "enf",
                    10: "doc", 11: "nt", 12: "mp", 14: "keep", 15: "shade"}
        m = re.findall(r"<img src='tdb://id:GFX_GUI_ICON_PROFESSION_(\d+)'>", ctx)
        for match in m:
            ctx = ctx.replace(f"<img src='tdb://id:GFX_GUI_ICON_PROFESSION_{match}'>",
                              f"#_{self.discord.util.get_profession(proficon.get(int(match)))}#_")

        for x in ["`", ' *', ' _', ' |']:
            ctx = ctx.replace(x, f' \\{x.strip()}')
        for pattern, sub in [(r"(<br>|\n|</br>)", r"\n> "),
                             (r"<highlight>(.*?)</highlight>", r"**\1**"),
                             (r"<title>(.*?)</title>", r"**__\1__**"),
                             (r"<header>(.*?)</header>", r"**__\1__**"),
                             (r"<header2>(.*?)</header2>", r"__\1__"),
                             (r"<notice>(.*?)</notice>", r"*\1*"),
                             (r"<font color=('*?).+?\1>(.*?)</font>", r'\2'),
                             (r"<myname>", self.bot.get_char_name()),
                             (r"<symbol>", self.discord.setting_service.get_value("symbol")),
                             (r"<a href=(.*?)itemref://\d+/\d+/\d+\1>(.+?)<\/a>", r"`\2`"),
                             (r"<(.+?)>\s*?<\1>", ''),
                             (r"<pagebreak>", ''),
                             (r"<a.+?href=\'.*?\'>(.*?)</a>", r'`\1`'),
                             (r"<clan>(.*?)</clan>", r":yellow_circle: \1"),
                             (r"<omni>(.*?)</omni>", r":blue_circle: \1"),
                             (r"<neutral>(.*?)</neutral>", r":white_circle: \1"),
                             (r"<tab>", "\t"),
                             (r"<img src=(.?)rdb://\d+\1>", ''),
                             ('#', ''),
                             ('&lt;', '<'),
                             ('&gt;', '>')]:
            ctx = re.sub(pattern, sub, ctx)

        cnt = 1
        while cnt > 0:
            ctx, cnt = re.subn(r"<(black|white|yellow|blue|green|red|orange|grey|cyan|violet)>(.*?|\d+?)</\1>", r"\2",
                               ctx)

        ctx.rstrip()
        ctx.rstrip(">")
        return ctx

    @command(command="discord", params=[Const("invite")], access_level="member",
             description="Get a personal Discord invite", sub_command="invite")
    def discord_invite_cmd(self, request, _):
        if not self.discord.client:
            return "Discord module has not been initiated yet. Please try again later."
        account = self.discord.account_service.get_account(request.sender.char_id)
        if account is None:
            return "You do not have an account"
        if account.disabled == 1:
            return "Your account is disabled"
        if account.discord_joined == 1:
            return "You have already joined the Discord server"
        if account.discord_invite != "":
            for ginvite in self.discord.invites:
                if ginvite.code == account.discord_invite:
                    asyncio.run_coroutine_threadsafe(self.discord.discord_delete_invite(ginvite), self.discord.loop)
        invite = asyncio.run_coroutine_threadsafe(self.discord.discord_create_invite(account.name), self.discord.loop)
        invite = invite.result()
        self.discord.data.set_discord_invite(account.char_id, invite.code)
        self.bot.send_mass_message(request.sender.char_id,
                                   f"Your personal Discord invite is: {invite} \n"
                                   f"This invite is only valid for 5 minutes.")

    @command(command="discord", params=[Const("update")], access_level="member",
             description="Update your discord information", sub_command="update")
    def discord_update_cmd(self, request, _):
        if not self.discord.client:
            return "Discord module has not been initiated yet. Please try again later."
        return self.discord.discord_update_account(request.sender.char_id)

    @command(command="discord", params=[Const("disconnect")], access_level="admin",
             description="Disconnect from Discord", sub_command="admin")
    def discord_disconnect_cmd(self, _, _1):
        if not self.discord.client:
            return "Discord module has not been initiated yet. Please try again later."
        if self.discord.client.is_closed():
            return "Discord is already disconnected"
        else:
            asyncio.run_coroutine_threadsafe(self.discord.discord_disconnect(), self.discord.loop)
            return "Disconnecting Discord"

    @command(command="discord",
             params=[Const("members")],
             access_level="admin",
             description="Get all discord members",
             sub_command="members")
    def discord_members_cmd(self, _, _1):
        if not self.discord.client:
            return "Discord module has not been initiated yet. Please try again later."
        blob = ""
        for member in self.discord.guild.members:
            member_roles = []
            for role in member.roles:
                if role.name == "@everyone":  # Skip @everyone as everyone has it.
                    continue
                member_roles.append(f"{role.name}")
            member_roles.sort(key=str.lower)
            blob += f"{member.name + '#' + member.discriminator} ({', '.join(member_roles)})\n"
        return ChatBlob(f"Discord Members ({len(self.discord.guild.members):d})", blob)

    @event(event_type="connect", description="Connects the Discord client automatically on startup, if a token exists")
    def handle_connect_event(self, _, _1):
        token = self.discord.setting_discord_token().get_value()
        if token.lower() in ["none", "", "null"]:
            return

        # noinspection PyTypeChecker
        self.discord.loop = asyncio.get_event_loop()
        self.discord.loop.create_task(self.discord.discord_connect(token))
        self.discord.thread = threading.Thread(target=self.discord.run_it_forever, daemon=True)
        self.discord.thread.start()

    @event(event_type=BanService.BAN_ADDED_EVENT, description="Ban user from Discord")
    def ban_added_event(self, _, event_data):
        token = self.discord.setting_discord_token().get_value()
        if token.lower() in ["none", "", "null"]:
            return
        account = self.discord.account_service.get_account(event_data.char_id)
        if account.discord_joined == 1 and account.discord_id != "":
            member = self.discord.guild.get_member(int(account.discord_id))
            if member is not None:
                ban = asyncio.run_coroutine_threadsafe(self.discord.discord_ban_user(member, event_data.reason),
                                                       self.discord.client.loop)

    @event(event_type=BanService.BAN_REMOVED_EVENT, description="Remove Discord ban")
    def ban_removed_event(self, _, event_data):
        token = self.discord.setting_discord_token().get_value()
        if token.lower() in ["none", "", "null"]:
            return
        account = self.discord.account_service.get_account(event_data.char_id)
        if account.discord_id != "":
            banlist = asyncio.run_coroutine_threadsafe(self.discord.discord_banlist(), self.discord.client.loop)
            banlist = banlist.result()
            for ban in banlist:
                if ban.user.id == int(account.discord_id):
                    unban = asyncio.run_coroutine_threadsafe(self.discord.discord_unban_user(ban.user),
                                                             self.discord.client.loop)

    @timerevent(budatime="1s", description="Handle Discord queue")
    def timer_check_discord_queue(self, _, _1):
        while self.discord.discord_queue:
            t = int(time.time())
            obj = self.discord.discord_queue.pop(0)
            if obj.type == "on_member_remove":
                member = obj.member
                handle = member.name + "#" + member.discriminator

                account = self.discord.data.get_account_discord_id(member.id)
                if not account:
                    log = '**%s** has left discord (**%s**)' % (member.nick or member.name, handle)
                    self.relay_hub_service.send_message("system_logger", 0, log, log)
                    self.logger.info(log)
                    continue
                main = self.discord.account_service.get_main(account.char_id)
                if account is None:
                    continue
                log = '**%s** has left discord (**%s**)' % (main.name, handle)
                self.relay_hub_service.send_message("system_logger", 0, log, log)
                self.logger.info(log)
                # self.bot.send_private_channel_message("%s has left Discord (%s)" % (main.name, handle))
                self.discord.data.set_discord_left(account.main)
                if account.discord_handle != handle:
                    self.discord.data.set_discord_handle(member.id, handle)
            if obj.type == "on_member_join":
                member = obj.member
                invite_used = obj.invite
                handle = member.name + "#" + member.discriminator
                if invite_used is None:
                    log = '**%s** joined discord with unknown invite' % handle
                    self.relay_hub_service. \
                        send_message("system_logger", 0,
                                     log + f" {self.get_role('Administrator', self.guild.roles).mention}'s, "
                                           f"check that!",
                                     log + f" {self.get_role('Administrator', self.guild.roles).mention}'s, "
                                           f"check that!")
                    self.logger.info(log)
                    continue
                self.discord.guild = self.discord.client.get_guild(self.discord.client.guilds[0].id)
                account = self.discord.data.get_discord_invite(invite_used.code)
                if account is None:
                    log = '**%s** joined discord with invite **%s** but couldnt find account!' % (
                        handle, invite_used.code)
                    self.relay_hub_service. \
                        send_message("system_logger", 0,
                                     log + f" {self.get_role('Administrator', self.guild.roles).mention}'s, "
                                           f"check that!",
                                     log + f" {self.get_role('Administrator', self.guild.roles).mention}'s, "
                                           f"check that!")
                    self.logger.info(log)
                    asyncio.run_coroutine_threadsafe(self.discord.discord_member_roles(member, None, None),
                                                     self.discord.loop)
                    continue

                main = self.discord.account_service.get_main(account.main)
                self.discord.data.set_discord_joined(account.main, handle, member.id)
                if self.discord.setting_service.get_value('is_alliance_bot') == '1':
                    nick = f"{f'[{self.discord.alias_controller.get_alias(main.org_id)}] '}{main.name}"
                else:
                    nick = f"{main.name}"
                nick = asyncio.run_coroutine_threadsafe(self.discord.discord_member_nick(member, nick),
                                                        self.discord.loop)
                access_level = access_level = self.discord.access_service.get_access_level(account.main)
                roles = asyncio.run_coroutine_threadsafe(
                    self.discord.discord_member_roles(member, account, access_level),
                    self.discord.loop)
                # noinspection LongLine
                log = '**%s** joined discord with invite **%s** and matches account **%s**' \
                      % (handle, invite_used.code,
                         f"{f'[{self.discord.alias_controller.get_alias(main.org_id)}] ' if self.discord.setting_service.get_value('is_alliance_bot') == '1' else ''}{main.name}")
                self.relay_hub_service.send_message("system_logger", account.main, log, log)
                self.logger.info(log)

    @timerevent(budatime="1h", description="Verify Discord members", run_at_startup=True)
    def timer_check_discord_members(self, event_type, event_data):
        token = self.discord.setting_discord_token().get_value()
        if token.lower() in ["none", "", "null"]:
            return
        if not self.bot.is_ready():
            return
        update = asyncio.run_coroutine_threadsafe(self.discord.discord_update_bot_basic(), self.discord.loop)
        # Update accounts that have left/joined discord without the bot being online
        accounts = self.db.query("SELECT * FROM account WHERE discord_id !=''")
        for account in accounts:
            match = False
            for member in self.discord.guild.members:
                handle = member.name + "#" + member.discriminator
                if member.id == int(account.discord_id):
                    match = True
                    if account.discord_joined == 0:
                        self.discord.data.set_discord_joined(account.main, handle, member.id)
                    break
            if match is False:
                if account.discord_joined == 1:
                    self.discord.data.set_discord_left(account.main)
        # Update current discord Members
        accounts = self.discord.db.query("SELECT * FROM account WHERE discord_joined = 1 and char_id = main")
        for member in self.discord.guild.members:
            if member.id == self.discord.client.user.id:
                continue
            member_account = None

            for account in list(accounts):
                if int(account.discord_id) == member.id:
                    member_account = account
                    accounts.remove(account)
                    break
            access_level = 0
            if member_account is not None:
                access_level = self.discord.access_service.get_access_level(member_account.main)
            roles = asyncio.run_coroutine_threadsafe(
                self.discord.discord_member_roles(member, member_account, access_level),
                self.discord.loop)
            if member_account is not None:
                main = self.discord.pork.get_character_info(member_account.main)
                # noinspection LongLine
                nick = f"{f'[{self.discord.alias_controller.get_alias(main.org_id)}] ' if self.discord.setting_service.get_value('is_alliance_bot') == '1' else ''}{main.name}"

                nick = asyncio.run_coroutine_threadsafe(self.discord.discord_member_nick(member, nick),
                                                        self.discord.loop)

    @event(event_type="main_changed", description="Fix discord names & ranks")
    def fix_ranks(self, _, data):
        row = self.db.query_single("SELECT * from account where char_id=?", [data.old_main_id])
        if row.discord_joined == 0:
            self.logger.debug(f"{data.old_main_id} was not in discord, ignoring main fixing")
            return
        elif row.discord_joined == 1:
            self.db.exec(
                "UPDATE account set discord_handle=?, discord_id=?, discord_invite=?, discord_joined=? where char_id=?",
                [row.discord_handle, row.discord_id, row.discord_invite, row.discord_joined, data.new_main_id])
            self.db.exec(
                "UPDATE account set discord_handle='', discord_id=0, discord_invite='', discord_joined=0 "
                "where char_id=?",
                [data.old_main_id])
            self.logger.info(f"{data.old_main_id} was in discord, overwriting {data.new_main_id} with {data}")

            self.discord.discord_update_account(data.new_main_id)

    @timerevent(budatime="5m", description="update activity")
    def change_count(self, _, _1):
        if hasattr(self, "loop"):
            count = self.db.query_single('SELECT count(*) as count from online '
                                         'where char_id NOT IN (select char_id from org_bots) and bot=?',
                                         [self.bot.get_char_id()]).count
            act = Activity(type=ActivityType.listening,
                           name=f"{count} Players")
            asyncio.run_coroutine_threadsafe(self.discord.client.change_presence(activity=act), self.discord.loop)
