from __future__ import annotations

import asyncio
import uuid
from abc import ABC, abstractmethod
from typing import (
    List,
    Dict,
    Any,
    TYPE_CHECKING,
    Optional,
    Union
)

import discord
from discord.guild import VerificationLevel

from .Base import DatabaseChecker

if TYPE_CHECKING:
    from discord.ext import commands
    from .Database import Database

__all__ = (
    "DictionaryConvertible",
    "TemplateInfo",
    "TemplateRole",
    "TemplateCategory",
    "TemplateTextChannel",
    "TemplateVoiceChannel",
    "PartialTemplate",
    "Template",
    "TemplateManager"
)


class DictionaryConvertible(ABC):
    __slots__ = ()

    @classmethod
    @abstractmethod
    def from_dict(cls, convert_from: Dict[str, Any]) -> DictionaryConvertible:
        pass


class TemplateInfo(DictionaryConvertible):
    __slots__ = (
        "template_id",
        "guild",
        "afk_timeout",
        "mfa_level",
        "verification_level",
        "explict_content_filter",
        "system_channel",
        "afk_channel"
    )

    def __init__(self,
                 template_id: str,
                 guild: int,
                 afk_timeout: int,
                 mfa_level: int,
                 verification_level: VerificationLevel,
                 explict_content_filter: int,
                 system_channel: int,
                 afk_channel: int):
        self.template_id = template_id
        self.guild = guild
        self.afk_timeout = afk_timeout
        self.mfa_level = mfa_level
        self.verification_level = verification_level
        self.explict_content_filter = explict_content_filter
        self.system_channel = system_channel
        self.afk_channel = afk_channel

    def __repr__(self):
        return f"<TemplateInfo id={self.template_id} guild={self.guild} mfa_level={self.mfa_level}>"

    def __str__(self):
        return f"<TemplateInfo id={self.template_id} guild={self.guild}>"

    @classmethod
    def from_dict(cls, convert_from: Dict[str, Any]) -> TemplateInfo:
        convert_from["verification_level"] = VerificationLevel(convert_from["verification_level"] or 0)
        convert_from["explict_content_filter"] = discord.ContentFilter(convert_from["explict_content_filter"] or 0)

        return cls(*list(convert_from.values()))


class TemplateCategory(DictionaryConvertible):
    __slots__ = ("name", "position", "category_id", "overwrites")

    def __init__(self, name: str, position: int, category_id: int, overwrites: Dict[int, int]):
        self.name = name
        self.position = position
        self.category_id = category_id
        self.overwrites = overwrites

    def __repr__(self):
        return f"<TemplateCategory id={self.category_id} name={self.name!r} position={self.position}>"

    def __str__(self):
        return f"<TemplateCategory name={self.name!r}>"

    @classmethod
    def from_dict(cls, convert_from: Dict[Any, Any]) -> TemplateCategory:
        return cls(*list(convert_from.values())[1:])


class TemplateTextChannel(DictionaryConvertible):
    __slots__ = ("name", "position", "category", "topic", "slowmode", "nsfw", "channel_id", "overwrites")

    def __init__(self,
                 name: str,
                 position: int,
                 category: int,
                 topic: str,
                 slowmode: int,
                 nsfw: bool,
                 channel_id: int,
                 overwrites: Dict[int, discord.PermissionOverwrite]):
        self.name = name
        self.position = position
        self.category = category
        self.topic = topic
        self.slowmode = slowmode
        self.nsfw = nsfw
        self.channel_id = channel_id
        self.overwrites = overwrites

    def __repr__(self):
        return f"<TemplateTextChannel topic={self.topic} name={self.name!r} position={self.position}>"

    def __str__(self):
        return f"<TemplateTextChannel name={self.name!r}>"

    @classmethod
    def from_dict(cls, convert_from: Dict[str, Any]) -> TemplateTextChannel:
        convert_from["nsfw"] = bool(convert_from["nsfw"])

        return cls(*list(convert_from.values())[1:])


class TemplateVoiceChannel(DictionaryConvertible):
    __slots__ = ("name", "position", "category", "bitrate", "user_limit", "channel_id", "overwrites")

    def __init__(self,
                 name: str,
                 position: int,
                 category: int,
                 bitrate: int,
                 user_limit: int,
                 channel_id: int,
                 overwrites: Dict[int, discord.PermissionOverwrite]):
        self.name = name
        self.position = position
        self.category = category
        self.bitrate = bitrate
        self.user_limit = user_limit
        self.channel_id = channel_id
        self.overwrites = overwrites

    def __repr__(self):
        return f"<TemplateVoiceChannel limit={self.user_limit} name={self.name!r} position={self.position}>"

    def __str__(self):
        return f"<TemplateVoiceChannel name={self.name!r}>"

    @classmethod
    def from_dict(cls, convert_from: Dict[str, Any]) -> TemplateVoiceChannel:
        return cls(*list(convert_from.values())[1:])


class TemplateRole(DictionaryConvertible):
    __slots__ = ("default_role", "name", "color", "hoist", "position", "mentionable", "role_id", "permissions")

    def __init__(self,
                 default_role: bool,
                 name: str,
                 color: int,
                 hoist: bool,
                 position: int,
                 mentionable: bool,
                 role_id: int,
                 permissions: discord.Permissions):
        self.default_role = default_role
        self.name = name
        self.color = color
        self.hoist = hoist
        self.position = position
        self.mentionable = mentionable
        self.role_id = role_id
        self.permissions = discord.Permissions(permissions)

    def get_raw(self):
        return {
            "name": self.name,
            "hoist": self.hoist,
            "mentionable": self.mentionable,
            "color": self.color,
            "permissions": self.permissions
        }

    def __repr__(self):
        return f"<TemplateRole hoist={self.hoist} name={self.name!r} position={self.position} color={self.color}>"

    def __str__(self):
        return f"<TemplateRole name={self.name!r}>"

    @classmethod
    def from_dict(cls, convert_from: Dict[str, Any]) -> TemplateRole:
        convert_from["default_role"] = bool(convert_from["default_role"])
        convert_from["hoist"] = bool(convert_from["hoist"])
        convert_from["mentionable"] = bool(convert_from["mentionable"])

        return cls(*list(convert_from.values())[1:])


class PartialTemplate:
    __slots__ = ("info", "categories", "text_channels", "voice_channels", "roles")

    def __init__(self,
                 info: TemplateInfo,
                 categories: List[TemplateCategory],
                 text_channels: List[TemplateTextChannel],
                 voice_channels: List[TemplateVoiceChannel],
                 roles: List[TemplateRole]):
        self.info = info
        self.categories = categories
        self.text_channels = text_channels
        self.voice_channels = voice_channels
        self.roles = roles


class Template:
    __slots__ = ("database", "tables", "info", "categories", "text_channels", "voice_channels", "roles")

    def __init__(self,
                 database: Database,
                 tables: Dict[str, str],
                 info: TemplateInfo,
                 categories: List[TemplateCategory],
                 text_channels: List[TemplateTextChannel],
                 voice_channels: List[TemplateVoiceChannel],
                 roles: List[TemplateRole]):
        self.database = database
        self.tables = tables
        self.info = info
        self.categories = categories
        self.text_channels = text_channels
        self.voice_channels = voice_channels
        self.roles = roles

    def __repr__(self):
        return f"<Template template={self.info} categories={self.categories} text_channels={self.text_channels}>"

    def __str__(self):
        return f"Template template={self.info}"

    async def delete(self) -> PartialTemplate:
        checks = {"id": self.info.template_id}

        partial = PartialTemplate(self.info, self.categories, self.text_channels, self.voice_channels, self.roles)
        await self.database.delete(self.tables["templates"], checks)
        await self.database.delete(self.tables["categories"], checks)
        await self.database.delete(self.tables["text_channels"], checks)
        await self.database.delete(self.tables["voice_channels"], checks)
        await self.database.delete(self.tables["roles"], checks)
        await self.database.delete(self.tables["overwrites"], checks)
        return partial

    @staticmethod
    def format_overwrites(overwrites: Dict[int, discord.PermissionOverwrite],
                          roles: Dict[int, discord.Role]) -> Dict[discord.Role, discord.PermissionOverwrite]:
        result_overwrites = {}

        for role in roles:
            if role in overwrites:
                result_overwrites[roles[role]] = overwrites[role]

        return result_overwrites

    async def apply_settings(self,
                             guild: discord.Guild,
                             reason: str,
                             channels: Dict[int, Union[discord.VoiceChannel, discord.TextChannel]]) -> None:
        await guild.edit(
            afk_timeout=self.info.afk_timeout,
            verification_level=self.info.verification_level,
            explicit_content_filter=self.info.explict_content_filter,
            afk_channel=channels.get(self.info.afk_channel),
            system_channel=channels.get(self.info.system_channel),
            reason=reason
        )

    async def apply_roles(self, guild: discord.Guild, reason: str) -> Dict[int, discord.Role]:
        roles = await asyncio.gather(
            *[
                guild.default_role.edit(permissions=role.permissions, reason=reason)
                if role.default_role else
                guild.create_role(**role.get_raw())
                for role in reversed(self.roles)
            ]
        )

        return dict(zip((x.role_id for x in reversed(self.roles)),
                        (role if role else guild.default_role for role in roles)))

    async def apply_categories(self, guild: discord.Guild, reason: str) -> Dict[int, discord.CategoryChannel]:
        categories = await asyncio.gather(
            *[
                guild.create_category_channel(name=category.name, reason=reason)
                for category in self.categories
            ]
        )

        return dict(zip((category.category_id for category in self.categories), categories))

    async def apply_channels(self,
                             guild: discord.Guild,
                             reason: str,
                             categories: Dict[int, discord.CategoryChannel],
                             roles: Dict[int, discord.Role]) -> Dict[int, discord.TextChannel]:
        text_channels = await asyncio.gather(*[guild.create_text_channel(
            name=channel.name,
            position=channel.position,
            slowmode_delay=channel.slowmode,
            topic=channel.topic,
            nsfw=channel.nsfw,
            category=categories.get(channel.category),
            overwrites=self.format_overwrites(channel.overwrites, roles),
            reason=reason) for channel in self.text_channels])

        return dict(zip((channel.channel_id for channel in self.text_channels), text_channels))

    async def apply_voice_channels(self,
                                   guild: discord.Guild,
                                   reason: str,
                                   categories: Dict[int, discord.CategoryChannel],
                                   roles: Dict[int, discord.Role]) -> Dict[int, discord.VoiceChannel]:
        voice_channels = await asyncio.gather(*[guild.create_voice_channel(
            name=channel.name,
            position=channel.position,
            category=categories.get(channel.category),
            bitrate=channel.bitrate,
            user_limit=channel.user_limit,
            overwrites=self.format_overwrites(channel.overwrites, roles),
            reason=reason) for channel in self.voice_channels])

        return dict(zip((channel.channel_id for channel in self.voice_channels), voice_channels))

    async def apply(self, guild: discord.Guild) -> None:
        roles_to_delete = list(filter(
            lambda r: not r.managed and guild.default_role != r and guild.me.top_role.position > r.position, guild.roles
        ))

        reason = f"Applying template {self.info.template_id}"

        await asyncio.gather(*[role.delete(reason=reason) for role in roles_to_delete])
        await asyncio.gather(*[channel.delete(reason=reason) for channel in guild.channels])

        roles = await self.apply_roles(guild, reason)
        categories = await self.apply_categories(guild, reason)
        text_channels = await self.apply_channels(guild, reason, categories, roles)
        voice_channels = await self.apply_voice_channels(guild, reason, categories, roles)
        await self.apply_settings(guild, reason, {**text_channels, **voice_channels})

    @staticmethod
    def get_overwrite(overwrites: List[Dict[str, Any]],
                      overwrite_object: int) -> Dict[int, discord.PermissionOverwrite]:
        result_overwrites = {}

        for overwrite in overwrites:
            if overwrite["overwrite_object"] == overwrite_object:
                result_overwrites[overwrite["overwrite_key"]] = discord.PermissionOverwrite.from_pair(
                    discord.Permissions(overwrite["overwrite_pair"]),
                    discord.Permissions(overwrite["overwrite_second_pair"])
                )

        return result_overwrites

    @classmethod
    async def get_template(cls, database: Database, tables: Dict[str, str], template_id: str) -> Optional[Template]:
        checks = {'id': template_id}

        raw_info = await database.select(tables["templates"], [], checks)
        if not raw_info:
            return None

        raw_categories = await database.select(tables["categories"], [], checks, fetchall=True)
        raw_text_channels = await database.select(tables["text_channels"], [], checks, fetchall=True)
        raw_voice_channels = await database.select(tables["voice_channels"], [], checks, fetchall=True)
        raw_roles = await database.select(tables["roles"], [], checks, fetchall=True)
        overwrites = await database.select(tables["overwrites"], [], checks, fetchall=True)

        return cls(
            database,
            tables,
            TemplateInfo.from_dict(raw_info),
            [TemplateCategory.from_dict(dict(x, **{"overwrites": cls.get_overwrite(overwrites, x["category_id"])}))
             for x in raw_categories],
            [TemplateTextChannel.from_dict(dict(x, **{"overwrites": cls.get_overwrite(overwrites, x["channel_id"])}))
             for x in raw_text_channels],
            [TemplateVoiceChannel.from_dict(dict(x, **{"overwrites": cls.get_overwrite(overwrites, x["channel_id"])}))
             for x in raw_voice_channels],
            [TemplateRole.from_dict(x) for x in raw_roles]
        )


class TemplateManager(DatabaseChecker):
    __slots__ = ("bot",)

    def __init__(self, bot: commands.Bot):
        super().__init__(
            [
                {
                    'id': "string",
                    'guild': 'snowflake',
                    "afk_timeout": "number",
                    "mfa_level": "smallnumber",
                    "verification_level": "smallnumber",
                    "explict_content_filter": "smallnumber",
                    "system_channel": "snowflake",
                    "afk_channel": "snowflake"
                },
                {
                    'id': "string",
                    "name": "string",
                    "position": "number",
                    "category_id": "snowflake"
                },
                {
                    'id': "string",
                    "name": "string",
                    "position": "number",
                    "category": "snowflake",
                    "topic": "string",
                    "slowmode": "number",
                    "nsfw": "smallnumber",
                    "channel_id": "snowflake"
                },
                {
                    'id': "string",
                    "name": "string",
                    "position": "number",
                    "category": "snowflake",
                    "bitrate": "smallnumber",
                    "user_limit": "smallnumber",
                    "channel_id": "snowflake"
                },
                {
                    "id": "string",
                    "default_role": "smallnumber",
                    "name": "string",
                    "color": "number",
                    "hoist": "smallnumber",
                    "position": "number",
                    "mentionable": "smallnumber",
                    "role_id": "snowflake",
                    "permissions": "number"
                },
                {
                    "id": "string",
                    "overwrite_object": "snowflake",
                    "overwrite_key": "snowflake",
                    "overwrite_pair": "number",
                    "overwrite_second_pair": "number"
                }
            ],
            ['templates', 'categories', 'text_channels', 'voice_channels', 'roles', 'overwrites']
        )
        self.bot = bot

    async def get_templates(self, guild: discord.Guild = None) -> List[Template]:
        return [await Template.get_template(self.database, self.tables, template["id"])
                for template in await self.database.select(self.tables["templates"],
                                                           ['id'],
                                                           {'guild': guild.id} if guild else {},
                                                           fetchall=True)]

    async def get_template(self, template_id: str) -> Optional[Template]:
        return await Template.get_template(self.database, self.tables, template_id)

    async def write_overwrites(self,
                               template_id: str,
                               overwrites_object: int,
                               overwrites: discord.PermissionOverwrite) -> None:
        for x, y in overwrites.items():
            pairs = [pair.value for pair in y.pair()]

            await self.database.insert(self.tables["overwrites"],
                                       {
                                           "id": template_id,
                                           "overwrite_object": overwrites_object,
                                           "overwrite_key": x.id,
                                           "overwrite_pair": pairs[0],
                                           "overwrite_second_pair": pairs[1]
                                       })

    async def create_template(self, guild: discord.Guild) -> Template:
        template_id = str(uuid.uuid4())

        await self.database.insert(self.tables["templates"],
                                   {
                                       'guild': guild.id,
                                       'id': template_id,
                                       'afk_timeout': guild.afk_timeout,
                                       'mfa_level': guild.mfa_level,
                                       'verification_level': guild.verification_level.value,
                                       'explict_content_filter': guild.explicit_content_filter.value,
                                       "system_channel": guild.system_channel and guild.system_channel.id,
                                       "afk_channel": guild.afk_channel and guild.afk_channel.id
                                   })

        for category in guild.categories:
            await self.write_overwrites(template_id, category.id, category.overwrites)
            await self.database.insert(self.tables["categories"],
                                       {
                                           "id": template_id,
                                           "name": category.name,
                                           "position": category.position,
                                           "category_id": category.id
                                       })

        for channel in guild.text_channels:
            await self.write_overwrites(template_id, channel.id, channel.overwrites)
            await self.database.insert(self.tables["text_channels"],
                                       {
                                           "id": template_id,
                                           "name": channel.name,
                                           "position": channel.position,
                                           "category": channel.category_id,
                                           "topic": channel.topic,
                                           "slowmode": channel.slowmode_delay,
                                           "nsfw": int(channel.is_nsfw()),
                                           "channel_id": channel.id
                                       })

        for voice_channel in guild.voice_channels:
            await self.write_overwrites(template_id, voice_channel.id, voice_channel.overwrites)
            await self.database.insert(self.tables["voice_channels"],
                                       {
                                           "id": template_id,
                                           "name": voice_channel.name,
                                           "position": voice_channel.position,
                                           "category": voice_channel.category_id,
                                           "bitrate": voice_channel.bitrate,
                                           "user_limit": voice_channel.user_limit,
                                           "channel_id": voice_channel.id
                                       })

        for role in guild.roles:
            await self.database.insert(self.tables["roles"],
                                       {
                                           "id": template_id,
                                           "default_role": int(role.is_default()),
                                           "name": role.name,
                                           "color": role.color.value,
                                           "hoist": int(role.hoist),
                                           "position": role.position,
                                           "mentionable": int(role.mentionable),
                                           "role_id": role.id,
                                           "permissions": role.permissions.value
                                       })

        return await Template.get_template(self.database, self.tables, template_id)
