from redbot.core.utils.chat_formatting import *
from redbot.core import Config, checks, commands, bank
from redbot.core.data_manager import cog_data_path
from typing import Literal
import discord

import aiohttp
import PIL
import os


class NitroEmoji(commands.Cog):
    """
    Reward nitro boosters with a custom emoji.

    Can also set roles that give emojis as well that stacks with boosting
    """

    def __init__(self, bot):
        self.bot = bot
        self.config = Config.get_conf(self, identifier=123859659843, force_registration=True)
        # roles maps: role id (str) -> number of allowed emojis (int)
        default_guild = {"channel": None, "disabled": False, "roles": {}}
        default_member = {"emojis": []}
        self.config.register_guild(**default_guild)
        self.config.register_member(**default_member)

    async def initialize(self):
        for member in self.bot.get_all_members():
            async with self.config.member(member).emojis() as data:
                to_remove = []
                for e in data:
                    emoji = self.find_emoji(member.guild, e)
                    # clean removed emojis if bot is down
                    if not emoji:
                        to_remove.append(e)

                for r in to_remove:
                    data.remove(r)

    async def get_boosts(self, member: discord.Member):
        # this will return number of boosts once api supports it
        # for now, set custom roles that give how ever emojis set
        # boosting stacks with custom roles and
        # custom roles stack with each other
        custom_roles = await self.config.guild(member.guild).roles()
        members_roles = [str(r.id) for r in member.roles]
        num_emojis = 0
        for c_role in custom_roles.keys():
            if c_role in members_roles:
                num_emojis += custom_roles[c_role]

        # TODO: add in checking for multiple emojis once supported
        if member.premium_since is not None:
            num_emojis += 1

        return num_emojis

    def find_emoji(self, guild, name):
        emoji = self.bot.get_emoji(name)
        # find by name:
        if not emoji:
            emoji = discord.utils.get(guild.emojis, name=name)
        return emoji

    async def add_emoji(self, member, name, attachment_or_url, reason=None):
        path = str(cog_data_path(cog_instance=self))
        path = os.path.join(path, str(member.id) + name)
        if isinstance(attachment_or_url, discord.Attachment):
            await attachment_or_url.save(path)
        elif isinstance(attachment_or_url, str):
            async with aiohttp.ClientSession(loop=self.bot.loop) as session:
                async with session.get(attachment_or_url) as r:
                    if r.status == 200:
                        with open(path, "wb") as f:
                            f.write(await r.read())

        # verify image
        im = PIL.Image.open(path)
        im.verify()

        # upload emoji
        with open(path, "rb") as f:
            emoji = await member.guild.create_custom_emoji(name=name, image=f.read())

        os.remove(path)

        async with self.config.member(member).emojis() as e:
            e.append(emoji.id)

        channel = await self.config.guild(member.guild).channel()
        channel = member.guild.get_channel(channel)
        if not channel:
            return

        embed = discord.Embed(title="Custom Emoji Added", colour=member.colour)
        embed.set_footer(text="User ID:{}".format(member.id))

        embed.set_author(name=str(member), url=emoji.url)
        embed.set_thumbnail(url=emoji.url)

        if reason:
            embed.add_field(name="Reason", value=reason)

        await channel.send(embed=embed)

    async def del_emoji(self, guild, member, emoji=None, reason=None):
        channel = await self.config.guild(member.guild).channel()
        channel = member.guild.get_channel(channel)
        if channel:
            embed = discord.Embed(title="Custom Emoji Removed", colour=member.colour)
            embed.set_footer(text="User ID:{}".format(member.id))

            embed.set_author(name=str(member), url=emoji.url)
            embed.set_thumbnail(url=emoji.url)
            embed.set_author(name=str(member))

            if reason:
                embed.add_field(name="Reason", value=reason)

            await channel.send(embed=embed)

        if emoji:
            try:
                await emoji.delete()
            except:
                pass
            async with self.config.member(member).emojis() as e:
                e.remove(emoji.id)

    @commands.group(name="nitroset")
    @commands.guild_only()
    @checks.admin()
    async def nitroset(self, ctx):
        """Manage nitro emoji settings."""
        pass

    @nitroset.command(name="channel")
    async def nitroset_channel(self, ctx, channel: discord.TextChannel):
        """
        Set the channel to log nitro events.

        Logs boosts, unboosts, and added emojis.
        """

        await self.config.guild(ctx.guild).channel.set(channel.id)
        await ctx.tick()

    @nitroset.command(name="disable")
    async def nitroset_disable(self, ctx, *, on_off: bool = None):
        """
        Disable users from adding more emojis.

        Users can still remove and list their own emojis.
        """
        if on_off is None:
            curr = await self.config.guild(ctx.guild).disabled()
            msg = "enabled" if not curr else "disabled"
            await ctx.send(f"Nitro emojis is {msg}.")
            return

        await self.config.guild(ctx.guild).disabled.set(on_off)
        await ctx.tick()

    @nitroset.command(name="role")
    async def nitroset_roles(self, ctx, num_emojis: int, *, role: discord.Role):
        """
        Set the amount of emojis role is allowed to add.

        Set to 0 to remove role.
        """
        async with self.config.guild(ctx.guild).roles() as roles:
            if num_emojis == 0:
                try:
                    del roles[str(role.id)]
                except:
                    pass
            else:
                roles[str(role.id)] = num_emojis

        await ctx.tick()

    @nitroset.command(name="list")
    async def nitroset_list(self, ctx):
        """
        List roles and the number of emojis they give.
        """
        msg = ""

        async with self.config.guild(ctx.guild).roles() as roles:
            if roles:
                msg += "Current roles:\n"
                for role_id in list(roles.keys()):
                    role = ctx.guild.get_role(int(role_id))
                    if not role:  # remove deleted roles silently
                        del roles[role_id]
                        continue
                    msg += f"{role.name}: {roles[role_id]}\n"

                for page in pagify(msg):
                    await ctx.send(box(page))
            else:
                await ctx.send("No roles defined.")

    @commands.group(name="nitroemoji")
    @checks.bot_has_permissions(manage_emojis=True)
    async def nitroemoji(self, ctx):
        """
        Manage your emojis if you boosted the server, or have a special role
        """
        pass

    @nitroemoji.command(name="add")
    async def nitroemoji_add(self, ctx, name: str, *, url: str = None):
        """
        Add an emoji to the server, if you boosted or have a special role

        Can only add an emoji for every boost you have in the server.
        """
        disabled = await self.config.guild(ctx.guild).disabled()
        if disabled:
            await ctx.send("Sorry, adding emojis is currently disabled right now.")
            return

        curr = await self.config.member(ctx.author).emojis()
        boosts = await self.get_boosts(ctx.author)
        if boosts and len(curr) < boosts:
            try:
                if url:
                    emoji = await self.add_emoji(ctx.author, name, url)
                else:
                    emoji = await self.add_emoji(ctx.author, name, ctx.message.attachments[0])
                await ctx.tick()
            except discord.errors.HTTPException as e:
                await ctx.send(e.text)
            except PIL.UnidentifiedImageError:
                await ctx.send("That is not a valid picture! Pictures must be in PNG, JPEG, or GIF format.")
            except:
                await ctx.send(
                    "Something went wrong, make sure to add a valid picture (PNG, JPG, or GIF) of the right size (256KB) and a valid name."
                )
                return
        elif not boosts:
            await ctx.send("Sorry, you need to be a nitro booster or have a special role to add an emoji!")
        elif curr:
            await ctx.send(
                "You already have the maximum number of custom emojis, please delete one first before adding another one."
            )

    @nitroemoji.command(name="rem")
    async def nitroemoji_rem(self, ctx, name: str):
        """
        Remove an emoji to the server, if you boosted or have a special role
        """
        curr = await self.config.member(ctx.author).emojis()
        emoji = self.find_emoji(ctx.guild, name)
        if emoji:
            if emoji.id in curr:
                await self.del_emoji(ctx.guild, ctx.author, emoji=emoji, reason="Removed by user.")
                await ctx.tick()
            else:
                await ctx.send("That isn't your custom emoji.")
        else:
            await ctx.send(warning("Emoji not found."))

    @nitroemoji.command(name="list")
    async def nitroemoji_list(self, ctx):
        """
        List your custom emojis in the server
        """
        curr = await self.config.member(ctx.author).emojis()
        boosts = await self.get_boosts(ctx.author)
        msg = f"Number of custom emojis used: {len(curr)}/{boosts}\n"
        if curr:
            msg += "Current emojis:\n"
            for emoji in curr:
                emoji = self.find_emoji(ctx.guild, emoji)
                if not emoji:
                    continue
                msg += f"{emoji.url}\n"
        else:
            msg += "`You have no custom emojis.`"

        for page in pagify(msg):
            await ctx.send(page)

    @commands.Cog.listener()
    async def on_member_update(self, before, after):
        if await self.bot.cog_disabled_in_guild(self, after.guild):
            return
        # check if they stopped boosting
        if (before.premium_since != after.premium_since and after.premium_since is None) or before.roles != after.roles:
            boosts = await self.get_boosts(after)
            emojis = await self.config.member(after).emojis()
            if len(emojis) > boosts:
                to_delete = []
                for i in range(len(emojis) - (len(emojis) - boosts), len(emojis)):
                    to_delete.append(emojis[i])

                for emoji in to_delete:
                    emoji = self.find_emoji(after.guild, emoji)
                    if not emoji:
                        continue

                    reason = ""
                    if before.premium_since != after.premium_since and after.premium_since is None:
                        reason += "Stopped boosting."
                    if before.roles != after.roles:
                        broles = set(before.roles)
                        aroles = set(after.roles)
                        removed = broles - aroles
                        reason += f"\nLost roles: {humanize_list(list(removed))}"

                    await self.del_emoji(after.guild, after, emoji=emoji, reason=reason)

    @commands.Cog.listener()
    async def on_member_leave(self, member):
        if await self.bot.cog_disabled_in_guild(self, member.guild):
            return
        # remove all member emojis on leave
        emojis = await self.config.member(member).emojis()
        for emoji in emojis:
            emoji = self.find_emoji(member.guild, emoji)
            if not emoji:
                continue
            await self.del_emoji(member.guild, member, emoji=emoji, reason="Member left.")

        await self.config.member(member).clear()

    @commands.Cog.listener()
    async def on_guild_emojis_update(self, guild, before, after):
        if await self.bot.cog_disabled_in_guild(self, guild):
            return
        b_e = set(before)
        a_e = set(after)
        diff = b_e - a_e
        if diff:
            for e in diff:
                user = None
                try:
                    async for entry in guild.audit_logs(limit=2):
                        if entry.action is discord.AuditLogAction.emoji_delete:
                            # if the bot didnt delete emoji
                            if entry.target.id != guild.me.id:
                                user = entry.user
                except:
                    continue
                # sanity check
                if not user or user.id == guild.me.id:
                    continue

                for member in guild.members:
                    curr = await self.config.member(member).emojis()
                    try:
                        async with self.config.member(member).emojis() as curr:
                            curr.remove(e.id)
                        await self.del_emoji(
                            guild, member, emoji=e, reason=f"Manually deleted by admin {user.name} (id: {user.id})"
                        )
                        break
                    except ValueError:
                        pass

    async def red_delete_data_for_user(
        self,
        *,
        requester: Literal["discord_deleted_user", "owner", "user", "user_strict"],
        user_id: int,
    ):
        pass
