"""
Set of bot commands designed for general leisure.
"""
import asyncio
import textwrap
from io import BytesIO
from math import ceil
from random import randint
from string import ascii_lowercase
from typing import List
from urllib.parse import urlencode

import asyncpg
from PIL import Image, ImageDraw, ImageFont
from aiohttp import ClientSession
from discord import (
    Colour,
    Embed,
    File,
    HTTPException,
    Message,
    NotFound,
    RawReactionActionEvent,
    embeds,
)
from discord.ext.commands import (
    Bot,
    BucketType,
    Cog,
    Context,
    UserConverter,
    command,
    cooldown,
)
from discord.utils import get

from cdbot.constants import (
    CYBERDISC_ICON_URL,
    EMOJI_LETTERS,
    FAKE_ROLE_ID,
    LOGGING_CHANNEL_ID,
    PostgreSQL,
    QUOTES_BOT_ID,
    QUOTES_CHANNEL_ID,
    QUOTES_DELETION_QUOTA,
    ROOT_ROLE_ID,
    STAFF_ROLE_ID,
    SUDO_ROLE_ID,
    WELCOME_BOT_ID,
)

ascii_lowercase += " !?$()"

REACT_TRIGGERS = {"kali": "\N{ONCOMING POLICE CAR}", "duck": "\N{DUCK}"}


def convert_emoji(message: str) -> List[str]:
    """Convert a string to a list of emojis."""
    emoji_trans = list(map(iter, EMOJI_LETTERS))
    # Enumerate characters in the message

    emojified = []

    for character in message:
        index = ascii_lowercase.find(character)
        if index == -1:
            continue
        # Yield the next iteration of the letter
        try:
            emojified.append(next(emoji_trans[index]))
        except StopIteration:
            continue

    return emojified


async def emojify(message: Message, string: str):
    """Convert a string to emojis, and add those emojis to a message."""
    for emoji in convert_emoji(string.lower()):
        if emoji is not None:
            await message.add_reaction(emoji)


class FormerUser(UserConverter):
    async def convert(self, ctx, argument):
        try:
            return await ctx.bot.fetch_user(argument)
        except (NotFound, HTTPException):
            return await super().convert(ctx, argument)


class Fun(Cog):
    """
    Commands for fun!
    """

    # Embed sent when users try to ping staff
    ping_embed = (
        Embed(
            colour=0xFF0000,
            description="⚠ **Please make sure you have taken the following into account:** ",
        )
        .set_footer(
            text="To continue with the ping, react \N{THUMBS UP SIGN}, To delete this message and move on,"
            " react \N{THUMBS DOWN SIGN}"
        )
        .add_field(
            name="Cyber Discovery staff will not provide help for challenges.",
            value="If you're looking for help, feel free to ask questions in one of our topical channels.",
        )
        .add_field(
            name="Make sure you have emailed support before pinging here.",
            value="`support@joincyberdiscovery.com` are available to answer any and all questions!",
        )
    )

    def __init__(self, bot: Bot):
        self.bot = bot
        self.staff_role = None
        self.quote_channel = None
        self.fake_staff_role = None

    async def migrate_quotes(self):
        """Create and initialise the `quotes` table with user quotes."""
        async with self.bot.pool.acquire() as connection:
            await connection.execute(
                "CREATE TABLE IF NOT EXISTS quotes (quote_id bigint PRIMARY KEY, author_id bigint)"
            )
        quote_channel = self.bot.get_channel(QUOTES_CHANNEL_ID)
        async for quote in quote_channel.history(limit=None):
            await self.add_quote_to_db(quote)
        print("Quotes successfully imported.")

    @Cog.listener()
    async def on_ready(self):
        guild = self.bot.guilds[0]

        if self.staff_role is None:
            self.staff_role = guild.get_role(STAFF_ROLE_ID)

        if self.fake_staff_role is None:
            self.fake_staff_role = guild.get_role(FAKE_ROLE_ID)

        self.bot.pool = await asyncpg.create_pool(
            host=PostgreSQL.PGHOST,
            port=PostgreSQL.PGPORT,
            user=PostgreSQL.PGUSER,
            password=PostgreSQL.PGPASSWORD,
            database=PostgreSQL.PGDATABASE,
        )

        await self.migrate_quotes()

    @cooldown(1, 60, BucketType.user)
    @cooldown(4, 60, BucketType.channel)
    @cooldown(6, 3600, BucketType.guild)
    @Cog.listener()
    async def on_message(self, message: Message):
        # If a new quote is added, add it to the database.
        if message.channel.id == QUOTES_CHANNEL_ID and (
            message.author.id == QUOTES_BOT_ID or message.mentions is not None
        ):
            await self.add_quote_to_db(message)
            print(f"Message #{message.id} added to database.")

        if self.fake_staff_role in message.role_mentions and not message.author.bot:
            # A user has requested to ping official staff
            sent = await message.channel.send(embed=self.ping_embed, delete_after=30)
            await sent.add_reaction("\N{THUMBS UP SIGN}")
            await sent.add_reaction("\N{THUMBS DOWN SIGN}")

            def check(reaction, user):
                """Check if the reaction was valid."""
                user_is_staff = user.top_role.id in (ROOT_ROLE_ID, SUDO_ROLE_ID)
                return all(
                    (
                        user == message.author or user_is_staff,
                        str(reaction.emoji) in "\N{THUMBS UP SIGN}\N{THUMBS DOWN SIGN}",
                    )
                )

            try:
                # Get the user's reaction
                reaction, _ = await self.bot.wait_for(
                    "reaction_add", timeout=30, check=check
                )

            except asyncio.TimeoutError:
                pass

            else:
                if str(reaction) == "\N{THUMBS UP SIGN}":
                    # The user wants to continue with the ping
                    await self.staff_role.edit(mentionable=True)
                    staff_ping = Embed(
                        title="This user has requested an official staff ping!",
                        colour=0xFF0000,
                        description=message.content,
                    ).set_author(
                        name=f"{message.author.name}#{message.author.discriminator}",
                        icon_url=message.author.avatar_url,
                    )
                    # Send the embed with the user's content
                    await message.channel.send(
                        self.staff_role.mention, embed=staff_ping
                    )
                    await self.staff_role.edit(mentionable=False)
                    # Delete the original message
                    await message.delete()

            finally:
                await sent.delete()

        ctx = await self.bot.get_context(message)

        if ctx.valid:
            # Don't react to valid commands
            return

        for word in message.content.lower().split():
            # Check if the message contains a trigger
            if word in REACT_TRIGGERS:
                to_react = REACT_TRIGGERS[word]

                if len(to_react) > 1:  # We have a string to react with
                    await emojify(message, to_react)
                else:
                    await message.add_reaction(to_react)

                return  # Only one auto-reaction per message

        # Adds waving emoji when a new user joins.
        if all(
            (
                "Welcome to the Cyber Discovery" in message.content,
                message.author.id == WELCOME_BOT_ID,
            )
        ):
            await message.add_reaction("\N{WAVING HAND SIGN}")

    @Cog.listener()
    async def on_raw_reaction_add(self, raw_reaction: RawReactionActionEvent):
        thumbs_down = "\N{THUMBS DOWN SIGN}"
        if all(
            (
                str(raw_reaction.emoji) == thumbs_down,
                raw_reaction.channel_id == QUOTES_CHANNEL_ID,
            )
        ):
            quotes_channel = self.bot.get_channel(QUOTES_CHANNEL_ID)
            logs_channel = self.bot.get_channel(LOGGING_CHANNEL_ID)
            message = await quotes_channel.fetch_message(raw_reaction.message_id)
            reaction = [
                react for react in message.reactions if str(react.emoji) == thumbs_down
            ][0]
            if reaction.count >= QUOTES_DELETION_QUOTA:
                async with self.bot.pool.acquire() as connection:
                    await connection.execute(
                        "DELETE FROM quotes WHERE quote_id = $1", reaction.message.id
                    )
                mentions = ", ".join(user.mention async for user in reaction.users())
                for quote_embed in reaction.message.embeds:
                    embed = Embed(
                        color=Colour.blue(),
                        title="Quote Deleted",
                        description=quote_embed.description
                    )
                    embed.add_field(name="Deleted By", value=mentions)
                    embed.set_author(name=quote_embed.author.name, icon_url=quote_embed.author.icon_url)

                await reaction.message.delete()
                await logs_channel.send(embed=embed)

    @command()
    async def lmgtfy(self, ctx: Context, *args: str):
        """
        Returns a LMGTFY URL for a given user argument.
        """
        # Creates a lmgtfy.com url for the given query.
        request_data = {
            "q": " ".join(arg for arg in args if not arg.startswith("-")),
            "ie": int("-ie" in args),
        }
        url = "https://lmgtfy.com/?" + urlencode(request_data)

        await ctx.send(url)

        if "-d" in args:
            await ctx.message.delete()

    # Ratelimit to one use per user every minute and 4 usages per minute per channel
    @command(aliases=["emojify"])
    @cooldown(1, 60, BucketType.user)
    @cooldown(4, 60, BucketType.channel)
    async def react(self, ctx, *, message: str):
        """
        Emojifies a given string, and reacts to a previous message
        with those emojis.
        """
        if ctx.channel.id == QUOTES_CHANNEL_ID:
            await ctx.send("This command is disabled in this channel!", delete_after=10)
            return
        limit, _, output = message.partition(" ")
        if limit.isdigit():
            limit = int(limit)
        else:
            output = message
            limit = 2
        async for target in ctx.channel.history(limit=limit):
            pass
        await emojify(target, output)

    @command()
    async def xkcd(self, ctx: Context, number: str = None):
        """
        Fetches xkcd comics.
        If number is left blank, automatically fetches the latest comic.
        If number is set to '?', a random comic is fetched.
        """

        # Creates endpoint URI
        if number is None or number == "?":
            endpoint = "https://xkcd.com/info.0.json"
        else:
            endpoint = f"https://xkcd.com/{number}/info.0.json"

        # Fetches JSON data from endpoint
        async with ClientSession() as session:
            async with session.get(endpoint) as response:
                data = await response.json()

        # Updates comic number
        if number == "?":
            number = randint(1, int(data["num"]))  # noqa: B311
            endpoint = f"https://xkcd.com/{number}/info.0.json"
            async with ClientSession() as session:
                async with session.get(endpoint) as response:
                    data = await response.json()
        else:
            number = data["num"]

        # Creates date object (Sorry, but I'm too tired to use datetime.)
        date = f"{data['day']}/{data['month']}/{data['year']}"

        # Creates Rich Embed, populates it with JSON data and sends it.
        comic = Embed()
        comic.title = data["safe_title"]
        comic.set_footer(text=data["alt"])
        comic.set_image(url=data["img"])
        comic.url = f"https://xkcd.com/{number}"
        comic.set_author(
            name="xkcd",
            url="https://xkcd.com/",
            icon_url="https://xkcd.com/s/0b7742.png",
        )
        comic.add_field(name="Number:", value=number)
        comic.add_field(name="Date:", value=date)
        comic.add_field(name="Explanation:", value=f"https://explainxkcd.com/{number}")

        await ctx.send(embed=comic)

    @command()
    async def quotes(self, ctx: Context, member: FormerUser = None):
        """
        Returns a random quotation from the #quotes channel.
        A user can be specified to return a random quotation from that user.
        """
        quote_channel = self.bot.get_channel(QUOTES_CHANNEL_ID)

        async with self.bot.pool.acquire() as connection:
            if member is None:
                # fetchval() returns the first result of a query.
                message_id = await connection.fetchval(
                    "SELECT quote_id FROM quotes ORDER BY random() LIMIT 1"
                )
            else:
                message_id = await connection.fetchval(
                    "SELECT quote_id FROM quotes WHERE author_id=$1 ORDER BY random() LIMIT 1",
                    member.id,
                )

        if message_id is None:
            return await ctx.send("No quotes found.")

        message = await quote_channel.fetch_message(message_id)
        embed = None
        content = message.clean_content
        attachment_urls = [attachment.url for attachment in message.attachments]

        if message.embeds:
            embed = message.embeds[0]
        elif len(attachment_urls) == 1:
            image_url = attachment_urls.pop(0)
            embed = Embed()
            embed.set_image(url=image_url)

        for url in attachment_urls:
            content += "\n" + url

        await ctx.send(content, embed=embed)

    @command()
    async def quotecount(self, ctx: Context, member: FormerUser = None):
        """
        Returns the number of quotes in the #quotes channel.
        A user can be specified to return the number of quotes from that user.
        """
        async with self.bot.pool.acquire() as connection:
            total_quotes = await connection.fetchval("SELECT count(*) FROM quotes")

            if member is None:
                await ctx.send(f"There are {total_quotes} quotes in the database")
            else:
                user_quotes = await connection.fetchval(
                    "SELECT count(*) FROM quotes WHERE author_id=$1", member.id
                )
                await ctx.send(
                    f"There are {user_quotes} quotes from {member} in the database "
                    f"({user_quotes / total_quotes:.2%})"
                )

    @command()
    async def quoteboard(self, ctx: Context, page: int = 1):
        """Show a leaderboard of users with the most quotes."""
        users = ""
        current = 1
        start_from = (page - 1) * 10

        async with self.bot.pool.acquire() as connection:
            page_count = ceil(
                await connection.fetchval(
                    "SELECT count(DISTINCT author_id) FROM quotes"
                ) / 10
            )

            if 1 > page > page_count:
                return await ctx.send(":no_entry_sign: Invalid page number")

            for result in await connection.fetch(
                "SELECT author_id, COUNT(author_id) as quote_count FROM quotes "
                "GROUP BY author_id ORDER BY quote_count DESC LIMIT 10 OFFSET $1",
                start_from,
            ):
                author, quotes = result.values()
                users += f"{start_from + current}. <@{author}> - {quotes}\n"
                current += 1

        embed = Embed(colour=Colour(0xAE444A))
        embed.add_field(name=f"Page {page}/{page_count}", value=users)
        embed.set_author(name="Quotes Leaderboard", icon_url=CYBERDISC_ICON_URL)

        await ctx.send(embed=embed)

    async def add_quote_to_db(self, quote: Message):
        """
        Adds a quote message ID to the database, and attempts to identify the author of the quote.
        """
        author_id = None
        if quote.author.id == QUOTES_BOT_ID:
            if not quote.embeds:
                return
            embed = quote.embeds[0]
            icon_url = embed.author.icon_url
            if type(icon_url) == embeds._EmptyEmbed or "twimg" in icon_url:
                author_id = QUOTES_BOT_ID
            elif "avatars" in icon_url:
                try:
                    author_id = int(icon_url.split("/")[-2])
                except ValueError:
                    author_id = 0
            else:
                author_info = embed.author.name.split("#")
                if len(author_info) == 1:
                    author_info.append("0000")
                author = get(
                    quote.guild.members,
                    name=author_info[0],
                    discriminator=author_info[1],
                )
                author_id = author.id if author is not None else None
        else:
            author_id = quote.mentions[0].id if quote.mentions else None

        async with self.bot.pool.acquire() as connection:
            if author_id is not None:
                await connection.execute(
                    "INSERT INTO quotes(quote_id, author_id) VALUES($1, $2) ON CONFLICT DO NOTHING",
                    quote.id,
                    author_id,
                )
            else:
                await connection.execute(
                    "INSERT INTO quotes(quote_id) VALUES($1) ON CONFLICT DO NOTHING",
                    quote.id,
                )

        print(f"Quote ID: {quote.id} has been added to the database.")

    async def create_text_image(self, ctx: Context, person: str, text: str):
        """
        Creates an image of a given person with the specified text.
        """
        if len(text) > 100:
            return await ctx.send(
                ":no_entry_sign: Your text must be shorter than 100 characters."
            )
        drawing_text = textwrap.fill(text, 20)
        font = ImageFont.truetype("cdbot/resources/Dosis-SemiBold.ttf", 150)

        text_layer = Image.new("RGBA", (1920, 1080), (0, 0, 0, 0))
        text_layer_drawing = ImageDraw.Draw(text_layer)
        text_layer_drawing.text(
            (0, 0), drawing_text, fill=(0, 0, 0), align="center", font=font
        )

        cropped_text_layer = text_layer.crop(text_layer.getbbox())
        cropped_text_layer.thumbnail((170, 110))

        image = Image.open(f"cdbot/resources/{person}SaysBlank.png")

        x = int((image.width / 5 + 20) - (cropped_text_layer.width / 2))
        y = int((image.height / 5 + 50 / 2) - (cropped_text_layer.height / 2))

        image.paste(cropped_text_layer, (x, y), cropped_text_layer)
        image_bytes = BytesIO()
        image.save(image_bytes, format="PNG")
        image_bytes.seek(0)
        await ctx.send(file=File(image_bytes, filename=f"{person}.png"))

    @command()
    async def agentj(self, ctx: Context, *, text: str):
        """
        Creates an image of Agent J with the specified text.
        """
        await self.create_text_image(ctx, "AgentJ", text)

    @command()
    async def jibhat(self, ctx: Context, *, text: str):
        """
        Creates an image of Jibhat with the specified text.
        """
        await self.create_text_image(ctx, "Jibhat", text)

    @command()
    async def agentq(self, ctx: Context, *, text: str):
        """
        Creates an image of Agent Q with the specified text.
        """
        await self.create_text_image(ctx, "AgentQ", text)

    @command()
    async def angryj(self, ctx: Context, *, text: str):
        """
        Creates an image of Angry Agent J with the specified text.
        """
        await self.create_text_image(ctx, "AngryJ", text)

    @command()
    async def angrylyne(self, ctx: Context, *, text: str):
        """
        Creates an image of Angry James Lyne with the specified text.
        """
        await self.create_text_image(ctx, "AngryLyne", text)

    @command()
    async def baldj(self, ctx: Context, *, text: str):
        """
        Creates an image of Bald Agent J with the specified text.
        """
        await self.create_text_image(ctx, "AgentJBadHairDay", text)


def setup(bot):
    """
    Required boilerplate for adding functionality of cog to bot.
    """
    bot.add_cog(Fun(bot))