import discord, logging, re, sys, traceback, asyncio, datetime, os, time
from minecraft_manager.models import Application, Player
from minecraft_manager.api import api
from django.conf import settings
from django.db import close_old_connections
from threading import Thread


logger = logging.getLogger(__name__)


class Discord(discord.Client):
    discord_game = 'MCM'
    prefix = getattr(settings, 'DISCORD_BOT_PREFIX', '!')
    auth_roles = getattr(settings, 'DISCORD_BOT_ROLES', [])
    error_users = getattr(settings, 'DISCORD_ERROR_USERS', [])
    token = None

    def __init__(self, token, **kwargs):
        super().__init__(**kwargs)
        self.token = token

    @asyncio.coroutine
    def on_ready(self):
        print('Logged in as')
        print(self.user.name)
        print(self.user.id)
        print(discord.__version__)
        print('Voice Loaded: {0}'.format(discord.opus.is_loaded()))
        print('OAuth URL: https://discordapp.com/api/oauth2/authorize?client_id={0}&permissions=0&scope=bot'.format(self.user.id))
        print('------')
        logger.info('Logged in as {0} ({1}) with discord.py v{2}'.format(self.user.name, self.user.id, discord.__version__))
        yield from self.change_presence(game=discord.Game(name=self.discord_game))

    @asyncio.coroutine
    def discord_message(self, channel, message):
        if isinstance(message, discord.Embed):
            for idx, field in enumerate(message.fields):
                if not field.value:
                    message.set_field_at(idx, name=field.name, value="N/A")
            yield from self.send_message(channel, embed=message)
        else:
            yield from self.send_message(channel, message)

    @asyncio.coroutine
    def on_message(self, message):

        # IGNORE DM AND BOTS
        if message.author.bot is True or message.channel.is_private is True:
            return

        member_roles = [role.id for role in message.author.roles]

        # IF MEMBER IS NOT AUTHORIZED, IGNORE
        if not any(role in self.auth_roles for role in member_roles):
            return

        # FIX STALE DB CONNECTIONS
        close_old_connections()

        # HELP
        match = re.match("[{0}]help$".format(self.prefix), message.content)
        if match:
            embed = discord.Embed(colour=discord.Colour(0x417505))
            embed.set_thumbnail(url="https://cdn.discordapp.com/avatars/454457830918062081/b5792489bc43d9e17b8f657880a17dd4.png")
            embed.add_field(name="Minecraft Manager Help", value="-----------------------------")
            embed.add_field(name="{}[app ]search <username>".format(self.prefix), value="Search for applications by partial or exact username.")
            embed.add_field(name="{}[app ]info <app ID>".format(self.prefix), value="Get detailed information about a specific application.")
            embed.add_field(name="{}[app ]accept|deny <app ID>".format(self.prefix), value="Take action on an application.")
            embed.add_field(name="{}compare".format(self.prefix), value="Compare Discord users to the Whitelist")
            yield from self.discord_message(message.channel, embed)
        # APP COMMANDS WITH APP ID
        match = re.match("[{0}](?:app )?(i|info|a|accept|d|deny) (\d+)$".format(self.prefix), message.content)
        if match:
            if match.group(1) and match.group(2):
                action = match.group(1)[:1]
                action_display = "accept" if action == "a" else "deny" if action == "d" else ""
                yield from self.discord_message(message.channel, "{0} App ID **{1}**".format(
                    "Retrieving info for" if action == "i" else "{0}ing".format(action_display.capitalize()),
                    match.group(2)))
                application = None
                try:
                    application = Application.objects.get(id=match.group(2))
                except:
                    yield from self.discord_message(message.channel, "An Application with that ID doesn't exist.")
                    return
                if action == "i":
                    # Info
                    msg = self.build_info(application)
                else:
                    # Action
                    accept = True if action == "a" else False
                    if not application.accepted:
                        application.accepted = accept
                        application.save()
                        msg = "App ID **{0}** was successfully {1}.".format(match.group(2), "accepted" if accept else "denied")
                        api.plugin("accept" if accept else "deny", application.username)
                    else:
                        msg = "App ID **{0}** was already {1}.".format(match.group(2), "accepted" if application.accepted else "denied")
                yield from self.discord_message(message.channel, msg)
                return
        # APP INFO WITH PARTIAL NAME SEARCH
        match = re.match("[{0}](?:app )?(?:search|info) (\S+)?$".format(self.prefix), message.content)
        if match:
            search = match.group(1)
            yield from self.discord_message(message.channel,"Searching for applications whose username contains '{0}'".format(search))
            applications = Application.objects.filter(username__icontains=search)[:10]
            count = Application.objects.filter(username__icontains=search).count()
            if count > 0:
                if count == 1:
                    info = self.build_info(applications[0])
                else:
                    info = "**Found the following applications**"
                    for app in applications:
                        info += "\n{0} - {1} ({2})".format(app.id, app.username.replace("_", "\\_"), app.status)
                    if count > 10:
                        info += "\n**This is only 10 applications out of {0} found. Please narrow your search if possible.**".format(len(applications))
            else:
                info = "No applications matched that search."
            yield from self.discord_message(message.channel, info)
        # COMPARE DISCORD USERS TO WHITELIST
        match = re.match("[{0}]compare".format(self.prefix), message.content)
        if match:
            yield from self.delete_message(message)
            yield from self.send_typing(message.channel)
            no_player = []
            no_application = []
            for member in message.server.members:
                if member.bot:
                    continue
                name = member.nick if member.nick else member.name
                try:
                    Player.objects.get(username__iexact=name)
                except:
                    no_player.append(name)
                    try:
                        Application.objects.get(username__iexact=name)
                    except:
                        no_player = no_player[:-1]
                        no_application.append(name)
            header = "**The following users have an application match, but no player match on the whitelist:**\n"
            yield from self.discord_message(message.author, "{}```{}```".format(header, "\n".join(no_player)))
            header = "**The following users do not have an application or player match on the whitelist:**\n"
            yield from self.discord_message(message.author, "{}```{}```".format(header, "\n".join(no_application)))


    def build_info(self, application):
        embed = discord.Embed(colour=discord.Colour(0x417505))
        embed.set_thumbnail(
            url="https://minotar.net/helm/{0}/100.png".format(application.username))
        embed.add_field(name="Application ID", value=application.id)
        embed.add_field(name="Username", value=application.username.replace("_", "\\_"))
        embed.add_field(name="Age", value=application.age)
        embed.add_field(name="Type of Player", value=application.player_type)
        embed.add_field(name="Ever been banned", value=application.ever_banned)
        if application.ever_banned:
            embed.add_field(name="Reason for being banned", value=application.ever_banned_explanation)
        embed.add_field(name="Reference", value=application.reference)
        embed.add_field(name="Read the Rules", value=application.read_rules)
        embed.add_field(name="Date", value=application.date_display)
        embed.add_field(name="Status", value=application.status)
        return embed

    @asyncio.coroutine
    def on_error(self, event, *args, **kwargs):
        print(sys.exc_info())
        print("Exception raised by " + event)
        error = '{0}\n{1}'.format(sys.exc_info()[1], ''.join(traceback.format_tb(sys.exc_info()[2])))
        logger.error(error)
        for user in self.error_users:
            try:
                user = discord.User(id=user)
                yield from self.discord_message(user, '```python\n{}```'.format(error))
            except:
                pass


    def run_bot(self):
        self.run(self.token)


class OreAlert:
    # Options
    log = os.path.join(settings.MINECRAFT_BASE_DIR, 'logs/latest.log')
    purge = 30  # How long until we purge, in minutes
    rotate = 5  # How long without input before we assume the log has rotated
    notify_start = 5  # How many veins found within the above purge minutes to notify
    notify_each = 1  # After the initial alert, how many should be found in addition before more alerts?
    notify_ping = 5  # After the initial alert, how many should be found in addition before more pings?

    playerList = []

    class Player:

        def __init__(self, name, time, prev=[]):
            self.name = name
            self.time = time
            self.prev = prev

        def compare(self, player):
            if self.name == player.name:
                return True
            else:
                return False

        def to_string(self):
            return str(self.name) + ": " + str(self.time) + ", previous: " + self.prev_to_string()

        def prev_to_string(self):
            ret = ""
            if len(self.prev) > 0:
                for p in self.prev:
                    ret += str(p) + " "

                return ret.rstrip()
            else:
                return ret

    def minute_interval(self, start, end):
        reverse = False
        if start > end:
            start, end = end, start
            reverse = True
        delta = (end.hour - start.hour) * 60 + end.minute - start.minute + (end.second - start.second) / 60.0
        if reverse:
            delta = 24 * 60 - delta
        return delta

    def follow(self, filename):
        thefile = open(filename, 'r', encoding='utf-8')
        thefile.seek(0, 2)  # Go to the end of the file
        start = datetime.datetime.now()
        end = datetime.datetime.now()
        api.discord_notification('OreAlert has started successfully.')
        while True:
            line = thefile.readline()
            if not line:
                if self.minute_interval(start, end) > self.rotate:
                    thefile.close()
                    time.sleep(5)
                    thefile = open(filename, 'r', encoding='utf-8')
                    thefile.seek(0, 2)  # Go to the end of the file
                    start = datetime.datetime.now()
                    end = datetime.datetime.now()
                    # api.discord_notification('OreAlert has closed and re-opened the log...hopefully it just rotated.')
                    continue
                end = end + datetime.timedelta(milliseconds=100)
                time.sleep(0.1)  # Sleep briefly
                continue
            start = datetime.datetime.now()
            end = datetime.datetime.now()
            yield line

    def run_bot(self):
        cur_line = ""
        try:
            loglines = self.follow(self.log)
            for line in loglines:
                # [00: 03:46] [Server thread / INFO]: [MinecraftManager]: [OreAlert]: Etzelia
                if "MinecraftManager" in line and "OreAlert" in line:  # Filter out non-OreAlert log statements
                    cur_time = line.split()[0].replace("[", "").replace("]", "")
                    if ":" in cur_time:  # Make sure we have a time.
                        time_array = cur_time.split(":")
                        name = line.split()[-1]
                        dt = datetime.time(int(time_array[0]), int(time_array[1]), int(time_array[2]))
                        p = self.Player(name, dt)
                        new_player = True
                        for player in self.playerList:
                            if p.compare(player):
                                new_player = False
                                player.prev[:] = [x for x in player.prev if not self.minute_interval(x,
                                                                                                dt) > self.purge]  # First, purge any times older than our configured amount
                                player.prev.append(dt)  # Add the new time
                                if len(player.prev) >= self.notify_start:
                                    # MCM Alert
                                    if len(player.prev) == self.notify_start:
                                        api.create_alert("OreAlert: {0}".format(player.name))
                                    if len(player.prev) % self.notify_each == 0:
                                        # In-game Notification
                                        api.plugin(api.PLUGIN_STAFF_CHAT,
                                                       "OreAlert {0} has found {1} diamond ore veins within {2} minutes.".format(
                                                           player.name, len(player.prev), self.purge))
                                        # Discord Notification
                                        ping = True if len(player.prev) % self.notify_ping == 0 else False
                                        api.discord_notification(
                                            '{0} has found {1} diamond ore veins within {2} minutes.'.format(
                                                player.name.replace("_", "\\_"), len(player.prev), self.purge), ping=ping)
                        if new_player:
                            p.prev = [p.time]
                            self.playerList.append(p)
        except KeyboardInterrupt:
            api.discord_notification("OreAlert has been stopped manually.")
        except:
            api.discord_notification("OreAlert has crashed!", ping=True)


