import asyncio
import logging
import re
import time
from datetime import datetime, timedelta
from functools import partial
from html.parser import HTMLParser

# noinspection PyPackageRequirements
from typing import TYPE_CHECKING

# noinspection PyPackageRequirements
import discord
from discord import Message, TextChannel, Embed, Role, Guild
from core.chat_blob import ChatBlob
from core.db import DB
from core.decorators import instance, event, timerevent
from core.dict_object import DictObject
from core.logger import Logger
from core.setting_types import HiddenSettingType, NumberSettingType, TextSettingType, BooleanSettingType
from core.text import Text
from core.igncore import IgnCore
from core.util import Util
from modules.core.discord.discord_data import DiscordData
from modules.core.discord.discord_management import DiscordManager

if TYPE_CHECKING:
    from modules.core.accounting.services.access_service import AccessService
    from modules.core.accounting.services.account_service import AccountService
    from modules.onlinebot.online.org_alias_controller import OrgAliasController
    from modules.core.discord.discord_command_handler import DiscordCommandHandler
    from core.lookup.character_service import CharacterService
    from core.lookup.pork_service import PorkService
    from core.message_hub_service import MessageHubService
    from core.setting_service import SettingService


class MLStripper(HTMLParser):
    def error(self, message):
        pass

    def __init__(self):
        super().__init__()
        self.reset()
        self.strict = False
        self.convert_charrefs = True
        self.fed = []

    def handle_data(self, d):
        self.fed.append(d)

    def get_data(self):
        return "".join(self.fed)


# noinspection PyUnusedLocal
@instance()
class DiscordController:
    MESSAGE_SOURCE = "discord"

    def __init__(self):
        self.logger = Logger(__name__)
        logging.getLogger("discord.gateway").setLevel(logging.WARN)
        logging.getLogger("discord.client").setLevel(logging.WARN)
        intents = discord.Intents.all()
        intents.message_content = True
        self.client = discord.Client(intents=intents, chunk_guilds_at_startup=True)
        self.manager: DiscordManager = DiscordManager(self)
        self.client.event(self.on_message)
        self.guild: Guild = None
        self.channel = None
        self.thread = None
        self.invites = None
        self.discord_roles = DictObject(
            {"override": "Override",
             "admin": "Administrator",
             "council": "Council",
             "leader": "Raid Leader",
             "president": "President",
             "general": "General",
             "officer": "Officer",
             "member": "Member",
             "failed": "failed"})
        # logging.getLogger("discord").setLevel(logging.INFO)
        self.discord_queue = []
        self.ao_queue = []

    # noinspection PyAttributeOutsideInit
    def inject(self, registry):
        self.bot: IgnCore = registry.get_instance("bot")
        self.db: DB = registry.get_instance("db")
        self.text: Text = registry.get_instance("text")
        self.character_service: CharacterService = registry.get_instance("character_service")
        self.util: Util = registry.get_instance("util")
        self.access_service: AccessService = registry.get_instance("access_service")
        self.pork: PorkService = registry.get_instance("pork_service")
        self.org: PorkService = registry.get_instance("pork_service")
        self.relay_hub_service: MessageHubService = registry.get_instance("message_hub_service")
        self.setting_service: SettingService = registry.get_instance("setting_service")
        self.alias_controller: OrgAliasController = registry.get_instance("org_alias_controller")
        self.account_service: AccountService = registry.get_instance("account_service")
        self.setting_service: SettingService = registry.get_instance("setting_service")

        self.data: DiscordData = DiscordData(self.db, self.client)
        self.cmd: DiscordCommandHandler = registry.get_instance("discord_command_handler")

    def pre_start(self):
        self.setting_service.register(self.module_name, "discord_token", "", HiddenSettingType(allow_empty=True),
                                      "Enter your Discord token here")
        self.setting_service.register(self.module_name, "public_relay", "[0]", TextSettingType(),
                                      "Public discord relay (purpose: autodelete messages after 1h)")
        self.setting_service.register(self.module_name, "channel_blacklist", "[0]", TextSettingType(),
                                      "Channels which should not trigger the command handler")
        self.bot.event_service.register_event_type("discord_command")

    def get_name(self, discord_id):
        data = self.db.query_single(
            "SELECT d.*, p.* FROM account d LEFT JOIN player p on d.main=p.char_id where discord_id=?", [discord_id])
        return data.name if data else None

    def setting_discord_token(self):
        return self.setting_service.get("discord_token")

    def run_it_forever(self):
        self.loop.run_forever()

    def discord_update_account(self, char_id):
        account = self.account_service.get_account(char_id)
        if account is None:
            return
        if account.discord_id != "" and account.discord_joined == 1:
            member = self.guild.get_member(int(account.discord_id))
            if member is not None:
                access_level = self.access_service.get_access_level(account.main)
                roles = asyncio.run_coroutine_threadsafe(self.discord_member_roles(member, account, access_level),
                                                         self.loop)

                # noinspection LongLine
                nick = f"{f'[{self.alias_controller.get_alias(account.org_id)}] ' if self.setting_service.get_value('is_alliance_bot') == '1' else ''}{account.name}"
                nick = asyncio.run_coroutine_threadsafe(self.discord_member_nick(member, nick), self.loop)
                return "Processing..."
        return "No Discord account found"

    def get_role(self, name, roles) -> Role or None:
        for role in roles:
            if role.name == name:
                return role
        return None

    async def discord_update_bot_basic(self):
        self.guild = self.client.get_guild(self.client.guilds[0].id)
        self.channel = self.client.get_channel(self.guild.text_channels[0].id)

    async def discord_update_bot_full(self):
        await self.discord_update_bot_basic()
        self.invites = await self.guild.invites()

    async def discord_disconnect(self):
        await self.client.close()

    async def discord_connect(self, token):
        try:
            if not self.client.is_closed():
                self.logger.info("Logging into Discord...")
                await self.client.start(token)

        except discord.ClientException as exc:
            self.logger.error(f"Something broke, I'm out!: {str(exc)}")

    async def setup_relayhub(self):
        for channel in self.guild.channels:
            if type(channel) == TextChannel:
                channel: TextChannel
                self.relay_hub_service.register_message_source(f"Discord_({channel.name.replace(' ', '_')})")
                self.relay_hub_service.register_message_destination(f"Discord_({channel.name.replace(' ', '_')})",
                                                                    partial(self.relay, channel.id), [], [f"Discord_({channel.name.replace(' ', '_')})"])

    def relay(self, channel, ctx):
        ch: TextChannel = self.guild.get_channel(channel)
        msg = ctx.formatted_message
        del_after = 3600 if f"{ch.id}" in self.setting_service.get_value("public_relay").lstrip("[").rstrip("]").split(',') else None
        if ctx.source == "alliance":
            msg = self.cmd.parseDiscord(ctx.message)
        elif type(msg) == ChatBlob:
            if msg.embed:
                msg: ChatBlob
                blob = Embed(title=msg.title, color=0x00FF00, description=self.cmd.parseDiscord(msg.msg).replace("\n> ", '\n'))
                asyncio.run_coroutine_threadsafe(ch.send("", delete_after=del_after, embed=blob), self.loop)
                return
            rsp = self.cmd.parseDiscord(ctx.formatted_message.page_prefix) + "\n"
            if not self.text.strip_html_tags(ctx.formatted_message.msg).startswith(ctx.formatted_message.title):
                rsp += f"**__{ctx.formatted_message.title}__**\n"
            rsp += "> " + self.cmd.parseDiscord(ctx.formatted_message.msg)
            rsp += f"\n {self.cmd.parseDiscord(ctx.formatted_message.page_postfix)}"
            msg = rsp
        else:
            msg = self.cmd.parseDiscord(ctx.formatted_message)
        msg: Message = asyncio.run_coroutine_threadsafe(ch.send(msg, delete_after=del_after), self.loop)

    async def discord_create_invite(self, reason=""):
        created_invite = await self.channel.create_invite(max_age=300, max_uses=2, reason=reason)
        self.guild = self.client.get_guild(self.client.guilds[0].id)
        return created_invite

    async def discord_delete_invite(self, invite):
        await invite.delete()

    async def discord_banlist(self):
        return await self.guild.bans()

    async def discord_ban_user(self, user, reason=""):
        await self.guild.ban(user, reason=reason)

    async def discord_unban_user(self, user):
        await self.guild.unban(user)

    async def discord_member_nick(self, account, nick):
        if self.get_role(self.discord_roles.override, account.roles):
            return  # We do not process accounts that have `override` role
        if account.nick != nick:
            result = await account.edit(nick=nick)

    async def discord_member_roles(self, discord_user: discord.Member, account, access_level):
        if self.get_role(self.discord_roles.override, discord_user.roles):
            return  # We do not process accounts that have `override` role
        addroles = []
        remroles = []
        if account is None:  # We assign failed role
            discrole = self.get_role(self.discord_roles.failed, discord_user.roles)
            self.logger.info("%s role result is %s" % (discord_user.nick, discrole))
            if discrole is None:
                failed = self.get_role(self.discord_roles.failed, self.guild.roles)
                if failed is None:
                    return
                addroles.append(failed)
                remroles = discord_user.roles
        else:
            # Superadmin
            if access_level["level"] in [10, 20]:
                if self.get_role(self.discord_roles.admin, discord_user.roles) is None:
                    rank = self.get_role(self.discord_roles.admin, self.guild.roles)
                    if rank is not None:
                        addroles.append(rank)
            else:
                rank = self.get_role(self.discord_roles.admin, discord_user.roles)
                if rank is not None:
                    remroles.append(rank)

            # Member Role
            if 100 > access_level["level"]:
                rank = self.get_role(self.discord_roles.member, self.guild.roles) or None
                if rank is not None:
                    addroles.append(rank)
            else:
                rank = self.get_role(self.discord_roles.member, self.guild.roles) or None
                if rank is not None:
                    remroles.append(rank)

            # Leader
            if self.account_service.check_leader(account.main):
                rank = self.get_role(self.discord_roles.leader, self.guild.roles) or None
                if rank is not None:
                    addroles.append(rank)
            else:
                rank = self.get_role(self.discord_roles.leader, self.guild.roles) or None
                if rank is not None:
                    remroles.append(rank)

            if self.setting_service.get_value("is_alliance_bot") == "1":

                if self.account_service.check_council(account.main):
                    rank = self.get_role(self.discord_roles.council, self.guild.roles) or None
                    if rank is not None:
                        addroles.append(rank)
                else:
                    rank = self.get_role(self.discord_roles.council, self.guild.roles) or None
                    if rank is not None:
                        remroles.append(rank)

                # President
                if self.account_service.check_president(account.main):
                    rank = self.get_role(self.discord_roles.president, self.guild.roles) or None
                    if rank is not None:
                        addroles.append(rank)
                else:
                    rank = self.get_role(self.discord_roles.president, self.guild.roles) or None
                    if rank is not None:
                        remroles.append(rank)

                # General
                if self.account_service.check_general(account.main):
                    rank = self.get_role(self.discord_roles.general, self.guild.roles) or None
                    if rank is not None:
                        addroles.append(rank)
                else:
                    rank = self.get_role(self.discord_roles.general, self.guild.roles) or None
                    if rank is not None:
                        remroles.append(rank)

                # Officer
                if self.account_service.check_officer(account.main):
                    rank = self.get_role(self.discord_roles.officer, self.guild.roles) or None
                    if rank is not None:
                        addroles.append(rank)
                else:
                    rank = self.get_role(self.discord_roles.officer, self.guild.roles) or None
                    if rank is not None:
                        remroles.append(rank)

            if account.disabled == 1 or access_level == 0 or access_level == 100:
                addroles = [self.get_role(self.discord_roles.failed, self.guild.roles)]
                remroles = discord_user.roles
            else:
                remroles.append(self.get_role(self.discord_roles.failed, self.guild.roles))
        # Execute
        if len(addroles) > 0:
            addroles = list(dict.fromkeys(addroles))  # Remove Duplicates
            for role in addroles:
                await discord_user.add_roles(role)
        if len(remroles) > 0:
            remroles = list(dict.fromkeys(remroles))  # Remove Duplicates
            for role in remroles:
                if role.name == "@everyone":  # Skip @everyone as we cant remove that
                    continue
                await discord_user.remove_roles(role)

    async def on_message(self, msg: Message):
        if f"{msg.channel.id}" in self.setting_service.get_value("public_relay").lstrip("[").rstrip("]").split(','):
            await msg.delete(delay=3600)
        if msg.author.id == self.client.user.id:
            return
        channel: TextChannel = msg.channel
        acc = self.data.get_account_discord_id(msg.author.id)
        if acc:
            acc.discord_nick = msg.author.nick or msg.author.name+'#'+msg.author.discriminator
            prefix = f"[{self.alias_controller.get_alias(acc.member)}] {acc.name}: " if self.alias_usage().get_value() else f"[DC] {acc.name}: "
            self.relay_hub_service.send_message("Discord_(" + channel.name+")",
                                                DictObject(acc), msg.content,
                                                prefix + msg.content)
        else:
            prefix = f"[-UNK-] {msg.author.username}: " if self.alias_usage().get_value() else f"[DC] {msg.author.username}: "
            self.relay_hub_service.send_message("Discord_(" + channel.name+")",
                                                DictObject({'char_id': 0, 'name': msg.author.username, 'discord_handle': f'{msg.author.name}#{msg.author.discriminator}'}), msg.content,
                                                prefix + msg.content)
        if str(channel.id) not in self.channel_blacklist().get_value().lstrip("[").rstrip("]").split(','):
            self.bot.event_service.fire_event("discord_command", DictObject({'account': acc, 'message': msg}))

        if self.is_command(msg.content):
            admin = self.get_role("Administrator", self.guild.roles)
            council = self.get_role("Council", self.guild.roles)
            if msg.content[:4] == "!pin":
                if admin in msg.author.roles:
                    matches = re.findall(pattern="<head>(.+?)<\/head>(.+)", string=msg.content[4:].strip(),
                                         flags=re.DOTALL)
                    if matches:
                        mess = await channel.send(
                            embed=Embed(color=3066993, title=matches[0][0], description=matches[0][1]))
                    else:
                        mess = await channel.send(embed=Embed(color=3066993, description=msg.content[4:]))
                    await mess.pin()

            if msg.content[:6] == "!note ":
                if admin in msg.author.roles:
                    matches = re.findall(pattern="<head>(.+?)<\/head>(.+)", string=msg.content[5:].strip(),
                                         flags=re.DOTALL)
                    if matches:
                        mess = await channel.send(
                            embed=Embed(color=3066993, title=matches[0][0], description=matches[0][1]))
                    else:
                        mess = await channel.send(embed=Embed(color=3066993, description=msg.content[5:]))

            if msg.content[:6] == "!purge":
                if admin in msg.author.roles:
                    count: str = msg.content[6:].strip()
                    if type(count) == str:
                        if count.isdigit():
                            # noinspection PyTypeChecker
                            count = int(count)
                    if type(count) == int:
                        # noinspection PyTypeChecker
                        while count > 100:
                            await msg.channel.purge(check=self.check)
                            count -= 100
                        else:
                            await msg.channel.purge(check=self.check, limit=count)
            await msg.delete(delay=0)

    def is_command(self, message: str):
        for x in ["subscribe", "pin", "purge", "note"]:
            if message.startswith("!" + x):
                return True

    def check(self, msg: Message):
        if msg.pinned or (len(msg.embeds) > 0 and (msg.created_at.utcnow() < datetime.utcnow()-timedelta(hours=12))):
            return False
        else:
            return True

    def alias_usage(self) -> BooleanSettingType:
        return self.setting_service.get("is_alliance_bot")

    def channel_blacklist(self) -> TextSettingType:
        return self.setting_service.get("channel_blacklist")

    def public_relay(self) -> TextSettingType:
        return self.setting_service.get("public_relay")
