import discord
import traceback
import asyncio
import logging

from . import types

log = logging.getLogger(__name__)


class BackupSaver:
    def __init__(self, bot, session, guild):
        self.session = session
        self.bot = bot
        self.guild = guild
        self.data = {}

    @staticmethod
    def _overwrites_to_json(overwrites):
        try:
            return {str(target.id): overwrite._values for target, overwrite in overwrites.items()}
        except Exception:
            return {}

    async def _save_channels(self):
        for category in self.guild.categories:
            try:
                self.data["categories"].append({
                    "name": category.name,
                    "position": category.position,
                    "category": None if category.category is None else str(category.category.id),
                    "id": str(category.id),
                    "overwrites": self._overwrites_to_json(category.overwrites)
                })
            except Exception:
                pass

            await asyncio.sleep(0)

        for tchannel in self.guild.text_channels:
            try:
                self.data["text_channels"].append({
                    "name": tchannel.name,
                    "position": tchannel.position,
                    "category": None if tchannel.category is None else str(tchannel.category.id),
                    "id": str(tchannel.id),
                    "overwrites": self._overwrites_to_json(tchannel.overwrites),
                    "topic": tchannel.topic,
                    "slowmode_delay": tchannel.slowmode_delay,
                    "nsfw": tchannel.is_nsfw(),
                    "messages": [],
                    "webhooks": [{
                        "channel": str(webhook.channel.id),
                        "name": webhook.name,
                        "avatar": str(webhook.avatar_url),
                        "url": webhook.url

                    } for webhook in await tchannel.webhooks()]
                })
            except Exception:
                pass

            await asyncio.sleep(0)

        for vchannel in self.guild.voice_channels:
            try:
                self.data["voice_channels"].append({
                    "name": vchannel.name,
                    "position": vchannel.position,
                    "category": None if vchannel.category is None else str(vchannel.category.id),
                    "id": str(vchannel.id),
                    "overwrites": self._overwrites_to_json(vchannel.overwrites),
                    "bitrate": vchannel.bitrate,
                    "user_limit": vchannel.user_limit,
                })
            except Exception:
                pass

            await asyncio.sleep(0)

    async def _save_roles(self):
        for role in self.guild.roles:
            try:
                if role.managed:
                    continue

                self.data["roles"].append({
                    "id": str(role.id),
                    "default": role.is_default(),
                    "name": role.name,
                    "permissions": role.permissions.value,
                    "color": role.color.value,
                    "hoist": role.hoist,
                    "position": role.position,
                    "mentionable": role.mentionable
                })
            except Exception:
                pass

            await asyncio.sleep(0)

    async def _save_members(self):
        if self.guild.large:
            await self.bot.request_offline_members(self.guild)

        async for member in self.guild.fetch_members(limit=1000):
            try:
                self.data["members"].append({
                    "id": str(member.id),
                    "name": member.name,
                    "discriminator": member.discriminator,
                    "nick": member.nick,
                    "roles": [str(role.id) for role in member.roles[1:] if not role.managed]
                })
            except Exception:
                pass

            await asyncio.sleep(0)

    async def _save_bans(self):
        for reason, user in await self.guild.bans():
            try:
                self.data["bans"].append({
                    "user": str(user.id),
                    "reason": reason
                })
            except Exception:
                pass

            await asyncio.sleep(0)

    async def save(self):
        self.data = {
            "id": str(self.guild.id),
            "name": self.guild.name,
            "icon_url": str(self.guild.icon_url),
            "owner": str(self.guild.owner_id),
            "member_count": self.guild.member_count,
            "region": str(self.guild.region),
            "system_channel": str(self.guild.system_channel),
            "afk_timeout": self.guild.afk_timeout,
            "afk_channel": None if self.guild.afk_channel is None else str(self.guild.afk_channel.id),
            "mfa_level": self.guild.mfa_level,
            "verification_level": str(self.guild.verification_level),
            "explicit_content_filter": str(self.guild.explicit_content_filter),
            "large": self.guild.large,

            "text_channels": [],
            "voice_channels": [],
            "categories": [],
            "roles": [],
            "members": [],
            "bans": [],
        }

        execution_order = [self._save_roles, self._save_channels, self._save_members, self._save_bans]

        for method in execution_order:
            try:
                await method()
            except Exception:
                traceback.print_exc()

        return self.data

    def __dict__(self):
        return self.data


class BackupLoader:
    def __init__(self, bot, session, data):
        self.session = session
        self.data = data
        self.bot = bot
        self.id_translator = {}
        self.options = types.BooleanArgs([])
        self.semaphore = asyncio.Semaphore(2)

    async def _overwrites_from_json(self, json):
        overwrites = {}
        for union_id, overwrite in json.items():
            try:
                union = await self.guild.fetch_member(int(union_id))
            except discord.NotFound:
                roles = list(
                    filter(lambda r: r.id == self.id_translator.get(union_id), self.guild.roles))
                if len(roles) == 0:
                    continue

                union = roles[0]

            overwrites[union] = discord.PermissionOverwrite(**overwrite)

        return overwrites

    def _translate_mentions(self, text):
        if not text:
            return text

        formats = ["<#%s>", "<@&%s>"]
        for key, value in self.id_translator.items():
            for _format in formats:
                text = text.replace(_format % str(key), _format % str(value))

        return text

    async def run_tasks(self, coros, wait=True):
        async def executor(_coro):
            try:
                await _coro

            except Exception:
                pass

            finally:
                self.semaphore.release()

        tasks = []
        for coro in coros:
            await self.semaphore.acquire()
            tasks.append(self.bot.loop.create_task(executor(coro)))

        if wait and tasks:
            await asyncio.wait(tasks)

    async def _prepare_guild(self):
        log.debug(f"Deleting roles on {self.guild.id}")
        if self.options.roles:
            existing_roles = list(filter(
                lambda r: not r.managed and self.guild.me.top_role.position > r.position,
                self.guild.roles
            ))
            difference = len(self.data["roles"]) - len(existing_roles)
            for role in existing_roles:
                if difference < 0:
                    try:
                        await role.delete(reason=self.reason)
                    except Exception:
                        pass

                    else:
                        difference += 1

                else:
                    break

        if self.options.channels:
            log.debug(f"Deleting channels on {self.guild.id}")
            for channel in self.guild.channels:
                try:
                    await channel.delete(reason=self.reason)
                except Exception:
                    pass

    async def _load_settings(self):
        log.debug(f"Loading settings on {self.guild.id}")
        await self.guild.edit(
            name=self.data["name"],
            # region=discord.VoiceRegion(self.data["region"]),
            afk_channel=self.guild.get_channel(self.id_translator.get(self.data["afk_channel"])),
            afk_timeout=self.data["afk_timeout"],
            # verification_level=discord.VerificationLevel(self.data["verification_level"]),
            system_channel=self.guild.get_channel(self.id_translator.get(self.data["system_channel"])),
            reason=self.reason
        )

    async def _load_roles(self):
        log.debug(f"Loading roles on {self.guild.id}")
        existing_roles = list(reversed(list(filter(
            lambda r: not r.managed and not r.is_default()
                      and self.guild.me.top_role.position > r.position,
            self.guild.roles
        ))))
        for role in reversed(self.data["roles"]):
            try:
                if role["default"]:
                    await self.guild.default_role.edit(
                        permissions=discord.Permissions(role["permissions"])
                    )
                    new_role = self.guild.default_role
                else:
                    kwargs = {
                        "name": role["name"],
                        "hoist": role["hoist"],
                        "mentionable": role["mentionable"],
                        "color": discord.Color(role["color"]),
                        "permissions": discord.Permissions.none(),
                        "reason": self.reason
                    }

                    if len(existing_roles) == 0:
                        try:
                            new_role = await asyncio.wait_for(self.guild.create_role(**kwargs), 10)
                        except asyncio.TimeoutError:
                            # Probably hit the 24h rate limit. Just skip roles
                            break
                    else:
                        new_role = existing_roles.pop(0)
                        await new_role.edit(**kwargs)

                self.id_translator[role["id"]] = new_role.id
            except Exception:
                pass

    async def _load_role_permissions(self):
        tasks = []
        for role in self.data["roles"]:
            to_edit = self.guild.get_role(self.id_translator.get(role["id"]))
            if to_edit:
                tasks.append(to_edit.edit(permissions=discord.Permissions(role["permissions"])))

        await self.run_tasks(tasks)

    async def _load_categories(self):
        log.debug(f"Loading categories on {self.guild.id}")
        for category in self.data["categories"]:
            try:
                created = await self.guild.create_category_channel(
                    name=category["name"],
                    overwrites=await self._overwrites_from_json(category["overwrites"]),
                    reason=self.reason
                )
                self.id_translator[category["id"]] = created.id
            except Exception:
                pass

    async def _load_text_channels(self):
        log.debug(f"Loading text channels on {self.guild.id}")
        for tchannel in self.data["text_channels"]:
            try:
                created = await self.guild.create_text_channel(
                    name=tchannel["name"],
                    overwrites=await self._overwrites_from_json(tchannel["overwrites"]),
                    category=discord.Object(self.id_translator.get(tchannel["category"])),
                    reason=self.reason
                )

                self.id_translator[tchannel["id"]] = created.id
                await created.edit(
                    topic=self._translate_mentions(tchannel["topic"]),
                    nsfw=tchannel["nsfw"],
                )
            except Exception:
                pass

    async def _load_voice_channels(self):
        log.debug(f"Loading voice channels on {self.guild.id}")
        for vchannel in self.data["voice_channels"]:
            try:
                created = await self.guild.create_voice_channel(
                    name=vchannel["name"],
                    overwrites=await self._overwrites_from_json(vchannel["overwrites"]),
                    category=discord.Object(self.id_translator.get(vchannel["category"])),
                    reason=self.reason
                )
                await created.edit(
                    bitrate=vchannel["bitrate"],
                    user_limit=vchannel["user_limit"]
                )
                self.id_translator[vchannel["id"]] = created.id
            except Exception:
                pass

    async def _load_channels(self):
        await self._load_categories()
        await self._load_text_channels()
        await self._load_voice_channels()

    async def _load_bans(self):
        log.debug(f"Loading bans on {self.guild.id}")

        tasks = [
            self.guild.ban(user=discord.Object(int(ban["user"])), reason=ban["reason"])
            for ban in self.data["bans"]
        ]
        await self.run_tasks(tasks)

    async def _load_members(self):
        log.debug(f"Loading members on {self.guild.id}")

        async def edit_member(member, member_data):
            roles = [
                discord.Object(self.id_translator.get(role))
                for role in member_data["roles"]
                if role in self.id_translator.keys()
            ]

            if self.guild.me.top_role.position > member.top_role.position:
                try:
                    if member != self.guild.owner:
                        await member.edit(
                            nick=member_data.get("nick"),
                            roles=[r for r in member.roles if r.managed] + roles,
                            reason=self.reason
                        )

                except discord.Forbidden:
                    try:
                        await member.edit(
                            roles=[r for r in member.roles if r.managed] + roles,
                            reason=self.reason
                        )

                    except discord.Forbidden:
                        await member.add_roles(*roles)

            else:
                await member.add_roles(*roles)

        tasks = []
        default_data = {
            "nick": None,
            "roles": []
        }
        async for member in self.guild.fetch_members(limit=self.guild.member_count):
            fits = list(filter(lambda m: m["id"] == str(member.id), self.data["members"]))
            if fits:
                tasks.append(edit_member(member, fits[0]))

            else:
                tasks.append(edit_member(member, default_data))

        await self.run_tasks(tasks)

    async def load(self, guild, loader: discord.User, options: types.BooleanArgs = None):
        self.options = options or self.options
        self.guild = guild
        self.loader = loader
        self.reason = f"Backup loaded by {loader}"

        log.debug(f"Loading backup on {self.guild.id}")

        try:
            await self._prepare_guild()
        except Exception:
            traceback.print_exc()

        steps = [
            ("roles", self._load_roles),
            ("channels", self._load_channels),
            ("settings", self._load_settings),
            ("bans", self._load_bans),
            ("members", self._load_members),
            ("roles", self._load_role_permissions)
        ]
        for option, coro in steps:
            if self.options.get(option):
                try:
                    await coro()
                except Exception:
                    traceback.print_exc()

        log.debug(f"Finished loading backup on {self.guild.id}")


class BackupInfo:
    def __init__(self, bot, data):
        self.bot = bot
        self.data = data

    @property
    def icon_url(self):
        return self.data["icon_url"]

    @property
    def name(self):
        return self.data["name"]

    def channels(self, limit=1000):
        ret = "```"
        for channel in self.data["text_channels"]:
            if channel.get("category") is None:
                ret += "\n#\u200a" + channel["name"]

        for channel in self.data["voice_channels"]:
            if channel.get("category") is None:
                ret += "\n \u200a" + channel["name"]

        ret += "\n"
        for category in self.data["categories"]:
            ret += "\n⯆\u200a" + category["name"]
            for channel in self.data["text_channels"]:
                if channel.get("category") == category["id"]:
                    ret += "\n  #\u200a" + channel["name"]

            for channel in self.data["voice_channels"]:
                if channel.get("category") == category["id"]:
                    ret += "\n   \u200a" + channel["name"]

            ret += "\n"

        return ret[:limit - 10] + "```"

    def roles(self, limit=1000):
        ret = "```"
        for role in reversed(self.data["roles"]):
            ret += "\n" + role["name"]

        return ret[:limit - 10] + "```"

    @property
    def member_count(self):
        return self.data["member_count"]

    @property
    def chatlog(self):
        max_messages = 0
        for channel in self.data["text_channels"]:
            if len(channel["messages"]) > max_messages:
                max_messages = len(channel["messages"])

        return max_messages