from discord.ext import commands
import discord

from . import utils

import random
import pendulum
import re
import asyncio
import traceback


class Raffle:
    def __init__(self, bot):
        self.bot = bot
        self.bot.loop.create_task(self.raffle_task())

    async def raffle_task(self):
        while True:
            try:
                await self.check_raffles()
            except Exception as error:
                with open("error_log", 'a') as f:
                    traceback.print_tb(error.__traceback__, file=f)
                    print('{0.__class__.__name__}: {0}'.format(error), file=f)
            await asyncio.sleep(60)

    async def check_raffles(self):
        # This is used to periodically check the current raffles, and see if they have ended yet
        # If the raffle has ended, we'll pick a winner from the entrants
        raffles = self.bot.db.load('raffles')

        if raffles is None:
            return

        for raffle in raffles:
            server = self.bot.get_guild(int(raffle['server_id']))

            # Check to see if this cog can find the server in question
            if server is None:
                continue
            for r in raffle['raffles']:
                title = r['title']
                entrants = r['entrants']

                now = pendulum.utcnow()
                expires = pendulum.parse(r['expires'])

                # Now lets compare and see if this raffle has ended, if not just continue
                if expires > now:
                    continue

                # Make sure there are actually entrants
                if len(entrants) == 0:
                    fmt = 'Sorry, but there were no entrants for the raffle `{}`!'.format(title)
                else:
                    winner = None
                    count = 0
                    while winner is None:
                        winner = server.get_member(int(random.SystemRandom().choice(entrants)))

                        # Lets make sure we don't get caught in an infinite loop
                        # Realistically having more than 50 random entrants found that aren't in the server anymore
                        # Isn't something that should be an issue, but better safe than sorry
                        count += 1
                        if count >= 50:
                            break

                    if winner is None:
                        fmt = 'I couldn\'t find an entrant that is still in this server, for the raffle `{}`!'.format(
                            title)
                    else:
                        fmt = 'The raffle `{}` has just ended! The winner is {}!'.format(title, winner.display_name)

                # Get the notifications settings, get the raffle setting
                notifications = self.bot.db.load('server_settings', key=server.id, pluck='notifications') or {}
                # Set our default to either the one set
                default_channel_id = notifications.get('default')
                # If it is has been overriden by picarto notifications setting, use this
                channel_id = notifications.get('raffle') or default_channel_id
                if channel_id:
                    channel = self.bot.get_channel(int(channel_id))
                else:
                    continue
                try:
                    await channel.send(fmt)
                except (discord.Forbidden, AttributeError):
                    pass

                # No matter which one of these matches were met, the raffle has ended and we want to remove it
                raffle['raffles'].remove(r)
                entry = {
                    'server_id': raffle['server_id'],
                    'raffles': raffle['raffles']
                }
                self.bot.db.save('raffles', entry)

    @commands.command()
    @commands.guild_only()
    @utils.custom_perms(send_messages=True)
    @utils.check_restricted()
    async def raffles(self, ctx):
        """Used to print the current running raffles on the server

        EXAMPLE: !raffles
        RESULT: A list of the raffles setup on this server"""
        raffles = self.bot.db.load('raffles', key=ctx.message.guild.id, pluck='raffles')
        if not raffles:
            await ctx.send("There are currently no raffles setup on this server!")
            return

        # For EVERY OTHER COG, when we get one result, it is nice to have it return that exact object
        # This is the only cog where that is different, so just to make this easier lets throw it
        # back in a one-indexed list, for easier parsing
        if isinstance(raffles, dict):
            raffles = [raffles]
        fmt = "\n\n".join("**Raffle:** {}\n**Title:** {}\n**Total Entrants:** {}\n**Ends:** {} UTC".format(
            num + 1,
            raffle['title'],
            len(raffle['entrants']),
            raffle['expires']) for num, raffle in enumerate(raffles))
        await ctx.send(fmt)

    @commands.group(invoke_without_command=True)
    @commands.guild_only()
    @utils.custom_perms(send_messages=True)
    @utils.check_restricted()
    async def raffle(self, ctx, raffle_id: int = 0):
        """Used to enter a raffle running on this server
        If there is more than one raffle running, provide an ID of the raffle you want to enter

        EXAMPLE: !raffle 1
        RESULT: You've entered the first raffle!"""
        # Lets let people use 1 - (length of raffles) and handle 0 base ourselves
        raffle_id -= 1
        author = ctx.message.author
        key = str(ctx.message.guild.id)

        raffles = self.bot.db.load('raffles', key=key, pluck='raffles')
        if raffles is None:
            await ctx.send("There are currently no raffles setup on this server!")
            return

        raffle_count = len(raffles)

        # There is only one raffle, so use the first's info
        if raffle_count == 1:
            entrants = raffles[0]['entrants']
            # Lets make sure that the user hasn't already entered the raffle
            if str(author.id) in entrants:
                await ctx.send("You have already entered this raffle!")
                return
            entrants.append(str(author.id))

            update = {
                'raffles': raffles,
                'server_id': key
            }
            self.bot.db.save('raffles', update)
            await ctx.send("{} you have just entered the raffle!".format(author.mention))
        # Otherwise, make sure the author gave a valid raffle_id
        elif raffle_id in range(raffle_count):
            entrants = raffles[raffle_id]['entrants']

            # Lets make sure that the user hasn't already entered the raffle
            if str(author.id) in entrants:
                await ctx.send("You have already entered this raffle!")
                return
            entrants.append(str(author.id))

            # Since we have no good thing to filter things off of, lets use the internal rethinkdb id

            update = {
                'raffles': raffles,
                'server_id': key
            }
            self.bot.db.save('raffles', update)
            await ctx.send("{} you have just entered the raffle!".format(author.mention))
        else:
            fmt = "Please provide a valid raffle ID, as there are more than one setup on the server! " \
                  "There are currently `{}` raffles running, use {}raffles to view the current running raffles".format(
                    raffle_count, ctx.prefix)
            await ctx.send(fmt)

    @raffle.command(name='create', aliases=['start', 'begin', 'add'])
    @commands.guild_only()
    @utils.custom_perms(kick_members=True)
    @utils.check_restricted()
    async def raffle_create(self, ctx):
        """This is used in order to create a new server raffle

        EXAMPLE: !raffle create
        RESULT: A follow-along for setting up a new raffle"""

        author = ctx.message.author
        server = ctx.message.guild
        channel = ctx.message.channel
        now = pendulum.utcnow()

        await ctx.send(
            "Ready to start a new raffle! Please respond with the title you would like to use for this raffle!")

        check = lambda m: m.author == author and m.channel == channel
        try:
            msg = await self.bot.wait_for('message', check=check, timeout=120)
        except asyncio.TimeoutError:
            await ctx.send("You took too long! >:c")
            return

        title = msg.content

        fmt = "Alright, your new raffle will be titled:\n\n{}\n\nHow long would you like this raffle to run for? " \
              "The format should be [number] [length] for example, `2 days` or `1 hour` or `30 minutes` etc. " \
              "The minimum for this is 10 minutes, and the maximum is 3 months"
        await ctx.send(fmt.format(title))

        # Our check to ensure that a proper length of time was passed
        def check(m):
            if m.author == author and m.channel == channel:
                return re.search("\d+ (minutes?|hours?|days?|weeks?|months?)", m.content.lower()) is not None
            else:
                return False

        try:
            msg = await self.bot.wait_for('message', timeout=120, check=check)
        except asyncio.TimeoutError:
            await ctx.send("You took too long! >:c")
            return

        # Lets get the length provided, based on the number and type passed
        num, term = re.search("\d+ (minutes?|hours?|days?|weeks?|months?)", msg.content.lower()).group(0).split(' ')
        # This should be safe to convert, we already made sure with our check earlier this would match
        num = int(num)

        # Now lets ensure this meets our min/max
        if "minute" in term and (num < 10 or num > 129600):
            await ctx.send(
                "Length provided out of range! The minimum for this is 10 minutes, and the maximum is 3 months")
            return
        elif "hour" in term and num > 2160:
            await ctx.send(
                "Length provided out of range! The minimum for this is 10 minutes, and the maximum is 3 months")
            return
        elif "day" in term and num > 90:
            await ctx.send(
                "Length provided out of range! The minimum for this is 10 minutes, and the maximum is 3 months")
            return
        elif "week" in term and num > 12:
            await ctx.send(
                "Length provided out of range! The minimum for this is 10 minutes, and the maximum is 3 months")
            return
        elif "month" in term and num > 3:
            await ctx.send(
                "Length provided out of range! The minimum for this is 10 minutes, and the maximum is 3 months")
            return

        # Pendulum only accepts the plural version of terms, lets make sure this is added
        term = term if term.endswith('s') else term + 's'
        # If we're in the range, lets just pack this in a dictionary we can pass to set the time we want, then set that
        payload = {term: num}
        expires = now.add(**payload)

        # Now we're ready to add this as a new raffle
        entry = {
            'title': title,
            'expires': expires.to_datetime_string(),
            'entrants': [],
            'author': str(author.id),
        }

        raffles = self.bot.db.load('raffles', key=server.id, pluck='raffles') or []
        raffles.append(entry)
        update = {
            'server_id': str(server.id),
            'raffles': raffles
        }
        self.bot.db.save('raffles', update)
        await ctx.send("I have just saved your new raffle!")

    @raffle.command(name='alerts')
    @commands.guild_only()
    @utils.custom_perms(manage_guild=True)
    @utils.check_restricted()
    async def raffle_alerts_channel(self, ctx, channel: discord.TextChannel):
        """Sets the notifications channel for raffle notifications

        EXAMPLE: !raffle alerts #raffle
        RESULT: raffle notifications will go to this channel
        """
        entry = {
            'server_id': str(ctx.message.guild.id),
            'notifications': {
                'raffle': str(channel.id)
            }
        }
        self.bot.db.save('server_settings', entry)
        await ctx.send("All raffle notifications will now go to {}".format(channel.mention))


def setup(bot):
    bot.add_cog(Raffle(bot))
