import logging
from typing import Optional, Union

import discord
from discord import Color, Embed, Member, Message, RawReactionActionEvent, User, errors
from discord.ext.commands import Cog

from bot import constants
from bot.bot import Bot
from bot.utils.messages import send_attachments, sub_clyde

log = logging.getLogger(__name__)


class DuckPond(Cog):
    """Relays messages to #duck-pond whenever a certain number of duck reactions have been achieved."""

    def __init__(self, bot: Bot):
        self.bot = bot
        self.webhook_id = constants.Webhooks.duck_pond
        self.bot.loop.create_task(self.fetch_webhook())

    async def fetch_webhook(self) -> None:
        """Fetches the webhook object, so we can post to it."""
        await self.bot.wait_until_guild_available()

        try:
            self.webhook = await self.bot.fetch_webhook(self.webhook_id)
        except discord.HTTPException:
            log.exception(f"Failed to fetch webhook with id `{self.webhook_id}`")

    @staticmethod
    def is_staff(member: Union[User, Member]) -> bool:
        """Check if a specific member or user is staff."""
        if hasattr(member, "roles"):
            for role in member.roles:
                if role.id in constants.STAFF_ROLES:
                    return True
        return False

    async def has_green_checkmark(self, message: Message) -> bool:
        """Check if the message has a green checkmark reaction."""
        for reaction in message.reactions:
            if reaction.emoji == "✅":
                async for user in reaction.users():
                    if user == self.bot.user:
                        return True
        return False

    async def send_webhook(
        self,
        content: Optional[str] = None,
        username: Optional[str] = None,
        avatar_url: Optional[str] = None,
        embed: Optional[Embed] = None,
    ) -> None:
        """Send a webhook to the duck_pond channel."""
        try:
            await self.webhook.send(
                content=content,
                username=sub_clyde(username),
                avatar_url=avatar_url,
                embed=embed
            )
        except discord.HTTPException:
            log.exception("Failed to send a message to the Duck Pool webhook")

    async def count_ducks(self, message: Message) -> int:
        """
        Count the number of ducks in the reactions of a specific message.

        Only counts ducks added by staff members.
        """
        duck_count = 0
        duck_reactors = []

        for reaction in message.reactions:
            async for user in reaction.users():

                # Is the user a staff member and not already counted as reactor?
                if not self.is_staff(user) or user.id in duck_reactors:
                    continue

                # Is the emoji a duck?
                if hasattr(reaction.emoji, "id"):
                    if reaction.emoji.id in constants.DuckPond.custom_emojis:
                        duck_count += 1
                        duck_reactors.append(user.id)
                elif isinstance(reaction.emoji, str):
                    if reaction.emoji == "🦆":
                        duck_count += 1
                        duck_reactors.append(user.id)
        return duck_count

    async def relay_message(self, message: Message) -> None:
        """Relays the message's content and attachments to the duck pond channel."""
        clean_content = message.clean_content

        if clean_content:
            await self.send_webhook(
                content=message.clean_content,
                username=message.author.display_name,
                avatar_url=message.author.avatar_url
            )

        if message.attachments:
            try:
                await send_attachments(message, self.webhook)
            except (errors.Forbidden, errors.NotFound):
                e = Embed(
                    description=":x: **This message contained an attachment, but it could not be retrieved**",
                    color=Color.red()
                )
                await self.send_webhook(
                    embed=e,
                    username=message.author.display_name,
                    avatar_url=message.author.avatar_url
                )
            except discord.HTTPException:
                log.exception("Failed to send an attachment to the webhook")

        await message.add_reaction("✅")

    @staticmethod
    def _payload_has_duckpond_emoji(payload: RawReactionActionEvent) -> bool:
        """Test if the RawReactionActionEvent payload contains a duckpond emoji."""
        if payload.emoji.is_custom_emoji():
            if payload.emoji.id in constants.DuckPond.custom_emojis:
                return True
        elif payload.emoji.name == "🦆":
            return True

        return False

    @Cog.listener()
    async def on_raw_reaction_add(self, payload: RawReactionActionEvent) -> None:
        """
        Determine if a message should be sent to the duck pond.

        This will count the number of duck reactions on the message, and if this amount meets the
        amount of ducks specified in the config under duck_pond/threshold, it will
        send the message off to the duck pond.
        """
        # Is the emoji in the reaction a duck?
        if not self._payload_has_duckpond_emoji(payload):
            return

        channel = discord.utils.get(self.bot.get_all_channels(), id=payload.channel_id)
        message = await channel.fetch_message(payload.message_id)
        member = discord.utils.get(message.guild.members, id=payload.user_id)

        # Is the member a human and a staff member?
        if not self.is_staff(member) or member.bot:
            return

        # Does the message already have a green checkmark?
        if await self.has_green_checkmark(message):
            return

        # Time to count our ducks!
        duck_count = await self.count_ducks(message)

        # If we've got more than the required amount of ducks, send the message to the duck_pond.
        if duck_count >= constants.DuckPond.threshold:
            await self.relay_message(message)

    @Cog.listener()
    async def on_raw_reaction_remove(self, payload: RawReactionActionEvent) -> None:
        """Ensure that people don't remove the green checkmark from duck ponded messages."""
        channel = discord.utils.get(self.bot.get_all_channels(), id=payload.channel_id)

        # Prevent the green checkmark from being removed
        if payload.emoji.name == "✅":
            message = await channel.fetch_message(payload.message_id)
            duck_count = await self.count_ducks(message)
            if duck_count >= constants.DuckPond.threshold:
                await message.add_reaction("✅")


def setup(bot: Bot) -> None:
    """Load the DuckPond cog."""
    bot.add_cog(DuckPond(bot))