import asyncio
import datetime

import discord
from discord import Emoji, TextChannel, Member
from discord.ext.commands import Bot
from emoji import demojize

import utils
from config import app_config as config, messages
from features.base_feature import BaseFeature
from repository.karma_repo import KarmaRepository
from repository.database.karma import Karma as Database_karma

cfg = config.Config
msg = messages.Messages


def test_emoji(db_emoji: bytearray, server_emoji: Emoji):
    try:
        custom_emoji = int(db_emoji)
        return custom_emoji == server_emoji.id
    except ValueError:
        return False


def is_unicode(text):
    demojized = demojize(text)
    if demojized.count(':') != 2:
        return False
    if demojized.split(':')[2] != '':
        return False
    return demojized != text


class Karma(BaseFeature):
    def __init__(self, bot: Bot, karma_repository: KarmaRepository):
        super().__init__(bot)
        self.repo = karma_repository

    async def emoji_process_vote(self, channel, emoji):
        delay = cfg.vote_minutes * 60
        message = utils.fill_message("karma_vote_message", emote=str(emoji))
        message += '\n'
        message += utils.fill_message("karma_vote_info", delay=str(delay//60), minimum=str(cfg.vote_minimum))
        message = await channel.send(message)
        await message.add_reaction("✅")
        await message.add_reaction("❌")
        await message.add_reaction("0⃣")
        await asyncio.sleep(delay)

        message = await channel.fetch_message(message.id)

        plus = 0
        minus = 0
        neutral = 0

        for reaction in message.reactions:
            if reaction.emoji == "✅":
                plus = reaction.count - 1
            elif reaction.emoji == "❌":
                minus = reaction.count - 1
            elif reaction.emoji == "0⃣":
                neutral = reaction.count - 1

        if plus + minus + neutral < cfg.vote_minimum:
            return None

        if plus > minus + neutral:
            return 1
        elif minus > plus + neutral:
            return -1
        else:
            return 0

    async def emoji_vote_value(self, message):
        if len(message.content.split()) != 2:
            await message.channel.send(
                msg.karma_vote_format)
            return

        emojis = self.repo.get_all_emojis()

        for server_emoji in message.guild.emojis:
            if not server_emoji.animated:
                e = list(
                    filter(
                        lambda x: test_emoji(x.emoji_ID, server_emoji),
                        emojis))

                if len(e) == 0:
                    self.repo.set_emoji_value(server_emoji, 0)
                    vote_value = await self.emoji_process_vote(message.channel,
                                                               server_emoji)
                    emoji = server_emoji  # Save for use outside loop
                    break
        else:
            await message.channel.send(msg.karma_vote_allvoted)
            return

        if vote_value is None:
            self.repo.remove_emoji(emoji)
            await message.channel.send(utils.fill_message("karma_vote_notpassed",
                                       emote=str(emoji), minimum=str(cfg.vote_minimum)))

        else:
            self.repo.set_emoji_value(emoji, vote_value)
            await message.channel.send(utils.fill_message("karma_vote_result",
                                       emote=str(emoji), result=str(vote_value)))

    async def emoji_revote_value(self, message):
        content = message.content.split()
        if len(content) != 3:
            await message.channel.send(msg.karma_revote_format)
            return

        emoji = content[2]
        if not is_unicode(emoji):
            try:
                emoji_id = int(emoji.split(':')[2][:-1])
                emoji = await message.channel.guild.fetch_emoji(emoji_id)
            except (ValueError, IndexError):
                await message.channel.send(msg.karma_revote_format)
                return
            except discord.NotFound:
                await message.channel.send(msg.karma_emote_not_found)
                return

        vote_value = await self.emoji_process_vote(message.channel, emoji)

        if vote_value is not None:
            self.repo.set_emoji_value(emoji, vote_value)
            await message.channel.send(utils.fill_message("karma_vote_result",
                                       emote=str(emoji), result=str(vote_value)))
        else:
            await message.channel.send(utils.fill_message("karma_vote_notpassed",
                                       emote=str(emoji), minimum=str(cfg.vote_minimum)))

    async def emoji_get_value(self, message):
        content = message.content.split()
        if len(content) != 3:
            return await self.emoji_list_all_values(message.channel)

        emoji = content[2]
        if not is_unicode(emoji):
            try:
                emoji_id = int(emoji.split(':')[2][:-1])
                emoji = await message.channel.guild.fetch_emoji(emoji_id)
            except (ValueError, IndexError):
                await message.channel.send(msg.karma_get_format)
                return
            except discord.NotFound:
                await message.channel.send(msg.karma_emote_not_found)
                return

        val = self.repo.emoji_value_raw(emoji)

        if val is not None:
            await message.channel.send(utils.fill_message("karma_get", emote=str(emoji), value=str(val)))
        else:
            await message.channel.send(utils.fill_message("karma_get_emote_not_voted", emote=str(emoji)))

    async def __make_emoji_list(self, guild, emojis):
        message = []
        line = ""
        is_error = False

        # Fetch all custom server emoji
        # They will be saved in the guild.emojis list
        await guild.fetch_emojis()

        for cnt, emoji_id in enumerate(emojis):
            if cnt % 8 == 0 and cnt:
                message.append(line)
                line = ""

            try:
                # Try and find the emoji in the server custom emoji list
                # (match by id) if the current emoji_id is not an int,
                # it's a unicode emoji, we'll handle that in the except
                # ValueError part. If it is an int and it's not found
                # in the emoji list, it will try to fetch it once again
                # and as that will probably fail anyway, it'll jump to
                # the discord.NotFound handler which will add it to
                # the error message
                emoji = next((x for x in guild.emojis if x.id == int(emoji_id)), None)

                if emoji is None:
                    emoji = await guild.fetch_emoji(int(emoji_id))

                line += str(emoji)

            except ValueError:
                if isinstance(emoji_id, bytearray):
                    line += emoji_id.decode()
                else:
                    line += str(emoji_id)

            except discord.NotFound:
                is_error = True
                if isinstance(emoji_id, bytearray):
                    self.repo.remove_emoji(emoji_id.decode())
                else:
                    self.repo.remove_emoji(str(emoji_id))

        message.append(line)
        message = [line for line in message if line != ""]
        return message, is_error

    async def emoji_list_all_values(self, channel):
        error = False
        for value in ['1', '-1']:
            emojis, is_error = await self.__make_emoji_list(
                    channel.guild,
                    self.repo.get_ids_of_emojis_valued(value))
            error |= is_error
            try:
                await channel.send("Hodnota " + value + ":")
                for line in emojis:
                    await channel.send(line)
            except discord.errors.HTTPException:
                pass  # TODO: error handling?

        if error:
            channel = await self.bot.fetch_channel(cfg.bot_dev_channel)
            await channel.send(msg.karma_get_missing)

    async def karma_give(self, message):
        input_string = message.content.split()
        if len(input_string) < 4:
            await message.channel.send(msg.karma_give_format)
        else:
            try:
                number = int(input_string[2])
            except ValueError:
                await message.channel.send(utils.fill_message("karma_give_format_number",
                                                              input=input_string[2]))
                return
            for member in message.mentions:
                self.repo.update_karma(member, message.author, number)
            if number >= 0:
                await message.channel.send(msg.karma_give_success)
            else:
                await message.channel.send(
                    msg.karma_give_negative_success
                )

    async def karma_transfer(self, message: TextChannel):
        input_string = message.content.split()
        if len(input_string) < 4 or len(message.mentions) < 2:
            await message.channel.send(msg.karma_transfer_format)
            return

        try:
            from_user: Member = message.mentions[0]
            to_user: Member = message.mentions[1]

            transfered = self.repo.transfer_karma(from_user, to_user)
            formated_message = utils.fill_message("karma_transfer_complete", from_user=from_user.name,
                                                  to_user=to_user.name, karma=transfered.karma,
                                                  positive=transfered.positive, negative=transfered.negative)

            await self.reply_to_channel(message.channel, formated_message)
        except ValueError:
            await self.reply_to_channel(message.channel, msg.karma_transfer_format)
            return

    def karma_get(self, author, target=None):
        if target is None:
            target = author
        k = self.repo.get_karma(target.id)
        return utils.fill_message(
            "karma",
            user=author.id,
            karma=k.karma.value,
            order=k.karma.position,
            target=target.display_name,
            karma_pos=k.positive.value,
            karma_pos_order=k.positive.position,
            karma_neg=k.negative.value,
            karma_neg_order=k.negative.position
        )

    async def message_karma(self, channel_out, msg):
        author = channel_out.author
        reactions = msg.reactions
        colour = 0x6d6a69
        output = {'-1': [], '1': [], '0': []}
        karma = 0
        for react in reactions:
            emoji = react.emoji
            val = self.repo.emoji_value_raw(emoji)
            if val == 1:
                output['1'].append(emoji)
                karma += react.count
                async for user in react.users():
                    if user.id == msg.author.id:
                        karma -= 1
                        break
            elif val == -1:
                output['-1'].append(emoji)
                karma -= react.count
                async for user in react.users():
                    if user.id == msg.author.id:
                        karma += 1
                        break
            else:
                output['0'].append(emoji)
        embed = discord.Embed(title='Karma zprávy')
        embed.add_field(name="Zpráva", value=msg.jump_url, inline=False)
        for key in ['1', '-1', '0']:
            if output[key]:
                message = ""
                for emoji in output[key]:
                    message += str(emoji) + ' '
                if key == '1':
                    name = 'Pozitivní'
                elif key == '0':
                    name = 'Neutrální'
                else:
                    name = 'Negativní'
                embed.add_field(name=name, value=message, inline=False)
        if karma > 0:
            colour = 0x34cb0b
        elif karma < 0:
            colour = 0xcb410b
        embed.colour = colour
        embed.set_footer(text=author, icon_url=author.avatar_url)
        embed.add_field(name='Celková karma za zprávu:', value=karma, inline=False)
        await channel_out.send(embed=embed)

    async def leaderboard(self, channel, action, order, start=1):
        if action == 'give':
            if order == "DESC":
                column = 'positive'
                attribute = Database_karma.positive.desc()
                emote = "<:peepolove:562305740132450359>"
                title = emote + "KARMA GIVINGBOARD " + emote
            else:
                column = 'negative'
                attribute = Database_karma.negative.desc()
                emote = "<:ishagrin:638277508651024394>"
                title = emote + " KARMA ISHABOARD " + emote
        elif action == 'get':
            column = 'karma'
            if order == "DESC":
                attribute = Database_karma.karma.desc()
                emote = ":trophy:"
                title = emote + " KARMA LEADERBOARD " + emote
            else:
                attribute = Database_karma.karma
                emote = "<:coolStoryArcasCZ:489539455271829514>"
                title = emote + " KARMA BAJKARBOARD " + emote
        else:
            raise Exception('Action neni get/give')

        board = self.repo.get_leaderboard(attribute, start-1)
        guild = self.bot.get_guild(cfg.guild_id)

        output = ""
        for i, user in enumerate(board, start):
            username = guild.get_member(int(user.member_ID))
            if username is None:
                line = '{} – *User left*: {} pts\n'.format(i, getattr(user, column))
            else:
                username = discord.utils.escape_markdown(username.display_name)
                line = '{} – **{}**: {} pts\n'.format(i, username, getattr(user, column))
            output += line

        embed = discord.Embed(title=title, description=output)
        embed.timestamp = datetime.datetime.now(tz=datetime.timezone.utc)

        if action == 'get' and order == "DESC":
            embed.add_field(name=msg.karma_web_title, value=msg.karma_web)

        await channel.send(embed=embed)