import asyncio
import re
from typing import Any

import discord
from redbot.core import Config, checks
from redbot.core import commands
from redbot.core.utils.chat_formatting import pagify, box

Cog: Any = getattr(commands, "Cog", object)


class CCRole(Cog):
    """
    Custom commands
    Creates commands used to display text and adjust roles
    """

    def __init__(self, bot):
        self.bot = bot
        self.config = Config.get_conf(self, identifier=9999114111108101)
        default_guild = {"cmdlist": {}, "settings": {}}

        self.config.register_guild(**default_guild)

    @commands.guild_only()
    @commands.group()
    async def ccrole(self, ctx):
        """Custom commands management with roles

        Highly customizable custom commands with role management."""
        if not ctx.invoked_subcommand:
            pass

    @ccrole.command(name="add")
    @checks.mod_or_permissions(administrator=True)
    async def ccrole_add(self, ctx, command: str):
        """Adds a custom command with roles

        When adding text, put arguments in `{}` to eval them
        Options: `{author}`, `{target}`, `{server}`, `{channel}`, `{message}`"""
        command = command.lower()
        if command in self.bot.all_commands:
            await ctx.send("That command is already a standard command.")
            return

        guild = ctx.guild
        author = ctx.author
        channel = ctx.channel

        cmd_list = self.config.guild(guild).cmdlist

        if await cmd_list.get_raw(command, default=None):
            await ctx.send(
                "This command already exists. Delete it with `{}ccrole delete` first.".format(
                    ctx.prefix
                )
            )
            return

        # Roles to add
        await ctx.send(
            "What roles should it add? (Must be **comma separated**)\nSay `None` to skip adding roles"
        )

        def check(m):
            return m.author == author and m.channel == channel

        try:
            answer = await self.bot.wait_for("message", timeout=120, check=check)
        except asyncio.TimeoutError:
            await ctx.send("Timed out, canceling")
            return

        arole_list = []
        if answer.content.upper() != "NONE":
            arole_list = await self._get_roles_from_content(ctx, answer.content)
            if arole_list is None:
                await ctx.send("Invalid answer, canceling")
                return

        # Roles to remove
        await ctx.send(
            "What roles should it remove? (Must be comma separated)\nSay `None` to skip removing roles"
        )
        try:
            answer = await self.bot.wait_for("message", timeout=120, check=check)
        except asyncio.TimeoutError:
            await ctx.send("Timed out, canceling")
            return

        rrole_list = []
        if answer.content.upper() != "NONE":
            rrole_list = await self._get_roles_from_content(ctx, answer.content)
            if rrole_list is None:
                await ctx.send("Invalid answer, canceling")
                return

        # Roles to use
        await ctx.send(
            "What roles are allowed to use this command? (Must be comma separated)\nSay `None` to allow all roles"
        )

        try:
            answer = await self.bot.wait_for("message", timeout=120, check=check)
        except asyncio.TimeoutError:
            await ctx.send("Timed out, canceling")
            return

        prole_list = []
        if answer.content.upper() != "NONE":
            prole_list = await self._get_roles_from_content(ctx, answer.content)
            if prole_list is None:
                await ctx.send("Invalid answer, canceling")
                return

        # Selfrole
        await ctx.send(
            "Is this a targeted command?(yes//no)\nNo will make this a selfrole command"
        )

        try:
            answer = await self.bot.wait_for("message", timeout=120, check=check)
        except asyncio.TimeoutError:
            await ctx.send("Timed out, canceling")
            return

        if answer.content.upper() in ["Y", "YES"]:
            targeted = True
            await ctx.send("This command will be **`targeted`**")
        else:
            targeted = False
            await ctx.send("This command will be **`selfrole`**")

        # Message to send
        await ctx.send(
            "What message should the bot say when using this command?\n"
            "Say `None` to send the default `Success!` message\n"
            "Eval Options: `{author}`, `{target}`, `{server}`, `{channel}`, `{message}`\n"
            "For example: `Welcome {target.mention} to {server.name}!`"
        )

        try:
            answer = await self.bot.wait_for("message", timeout=120, check=check)
        except asyncio.TimeoutError:
            await ctx.send("Timed out, canceling")
            return

        text = "Success!"
        if answer.content.upper() != "NONE":
            text = answer.content

        # Save the command

        out = {
            "text": text,
            "aroles": arole_list,
            "rroles": rrole_list,
            "proles": prole_list,
            "targeted": targeted,
        }

        await cmd_list.set_raw(command, value=out)

        await ctx.send("Custom Command **`{}`** successfully added".format(command))

    @ccrole.command(name="delete")
    @checks.mod_or_permissions(administrator=True)
    async def ccrole_delete(self, ctx, command: str):
        """Deletes a custom command

        Example:
        `[p]ccrole delete yourcommand`"""
        guild = ctx.guild
        command = command.lower()
        if not await self.config.guild(guild).cmdlist.get_raw(command, default=None):
            await ctx.send("That command doesn't exist")
        else:
            await self.config.guild(guild).cmdlist.set_raw(command, value=None)
            await ctx.send("Custom command successfully deleted.")

    @ccrole.command(name="details")
    async def ccrole_details(self, ctx, command: str):
        """Provide details about passed custom command"""
        guild = ctx.guild
        command = command.lower()
        cmd = await self.config.guild(guild).cmdlist.get_raw(command, default=None)
        if cmd is None:
            await ctx.send("That command doesn't exist")
            return

        embed = discord.Embed(
            title=command,
            description="{} custom command".format(
                "Targeted" if cmd["targeted"] else "Non-Targeted"
            ),
        )

        def process_roles(role_list):
            if not role_list:
                return "None"
            return ", ".join(
                [discord.utils.get(ctx.guild.roles, id=roleid).name for roleid in role_list]
            )

        embed.add_field(name="Text", value="```{}```".format(cmd["text"]))
        embed.add_field(name="Adds Roles", value=process_roles(cmd["aroles"]), inline=True)
        embed.add_field(name="Removes Roles", value=process_roles(cmd["rroles"]), inline=True)
        embed.add_field(name="Role Restrictions", value=process_roles(cmd["proles"]), inline=True)

        await ctx.send(embed=embed)

    @ccrole.command(name="list")
    async def ccrole_list(self, ctx):
        """Shows custom commands list"""
        guild = ctx.guild
        cmd_list = await self.config.guild(guild).cmdlist()
        cmd_list = {k: v for k, v in cmd_list.items() if v}
        if not cmd_list:
            await ctx.send(
                "There are no custom commands in this server. Use `{}ccrole add` to start adding some.".format(
                    ctx.prefix
                )
            )
            return

        cmd_list = ", ".join([ctx.prefix + c for c in sorted(cmd_list.keys())])
        cmd_list = "Custom commands:\n\n" + cmd_list

        if (
            len(cmd_list) < 1500
        ):  # I'm allowed to have arbitrary numbers for when it's too much to dm dammit
            await ctx.send(box(cmd_list))
        else:
            for page in pagify(cmd_list, delims=[" ", "\n"]):
                await ctx.author.send(box(page))
            await ctx.send("Command list DM'd")

    async def on_message(self, message):
        if len(message.content) < 2 or message.guild is None:
            return

        guild = message.guild
        try:
            prefix = await self.get_prefix(message)
        except ValueError:
            return

        cmdlist = self.config.guild(guild).cmdlist
        cmd = message.content[len(prefix) :].split()[0].lower()
        cmd = await cmdlist.get_raw(cmd, default=None)

        if cmd is not None:
            await self.eval_cc(cmd, message)

    async def _get_roles_from_content(self, ctx, content):
        content_list = content.split(",")
        try:
            role_list = [
                discord.utils.get(ctx.guild.roles, name=role.strip(" ")).id
                for role in content_list
            ]
        except (discord.HTTPException, AttributeError):  # None.id is attribute error
            return None
        else:
            return role_list

    async def get_prefix(self, message: discord.Message) -> str:
        """
        Borrowed from alias cog
        Tries to determine what prefix is used in a message object.
            Looks to identify from longest prefix to smallest.

            Will raise ValueError if no prefix is found.
        :param message: Message object
        :return:
        """
        content = message.content
        prefix_list = await self.bot.command_prefix(self.bot, message)
        prefixes = sorted(prefix_list, key=lambda pfx: len(pfx), reverse=True)
        for p in prefixes:
            if content.startswith(p):
                return p
        raise ValueError

    async def eval_cc(self, cmd, message):
        """Does all the work"""
        if cmd["proles"] and not (
            set(role.id for role in message.author.roles) & set(cmd["proles"])
        ):
            return  # Not authorized, do nothing

        if cmd["targeted"]:
            try:
                target = discord.utils.get(
                    message.guild.members, mention=message.content.split()[1]
                )
            except IndexError:  # .split() return list of len<2
                target = None

            if not target:
                out_message = "This custom command is targeted! @mention a target\n`{} <target>`".format(
                    message.content.split()[0]
                )
                await message.channel.send(out_message)
                return
        else:
            target = message.author

        if cmd["aroles"]:
            arole_list = [
                discord.utils.get(message.guild.roles, id=roleid) for roleid in cmd["aroles"]
            ]
            # await self.bot.send_message(message.channel, "Adding: "+str([str(arole) for arole in arole_list]))
            try:
                await target.add_roles(*arole_list)
            except discord.Forbidden:
                await message.channel.send("Permission error: Unable to add roles")
        await asyncio.sleep(1)

        if cmd["rroles"]:
            rrole_list = [
                discord.utils.get(message.guild.roles, id=roleid) for roleid in cmd["rroles"]
            ]
            # await self.bot.send_message(message.channel, "Removing: "+str([str(rrole) for rrole in rrole_list]))
            try:
                await target.remove_roles(*rrole_list)
            except discord.Forbidden:
                await message.channel.send("Permission error: Unable to remove roles")

        out_message = self.format_cc(cmd, message, target)
        await message.channel.send(out_message)

    def format_cc(self, cmd, message, target):
        out = cmd["text"]
        results = re.findall("{([^}]+)\}", out)
        for result in results:
            param = self.transform_parameter(result, message, target)
            out = out.replace("{" + result + "}", param)
        return out

    def transform_parameter(self, result, message, target):
        """
        For security reasons only specific objects are allowed
        Internals are ignored
        """
        raw_result = "{" + result + "}"
        objects = {
            "message": message,
            "author": message.author,
            "channel": message.channel,
            "server": message.guild,
            "guild": message.guild,
            "target": target,
        }
        if result in objects:
            return str(objects[result])
        try:
            first, second = result.split(".")
        except ValueError:
            return raw_result
        if first in objects and not second.startswith("_"):
            first = objects[first]
        else:
            return raw_result
        return str(getattr(first, second, raw_result))