import asyncio
import logging
from datetime import datetime, timedelta
from typing import Optional

import discord
from redbot.core import Config, checks, commands
from redbot.core.bot import Red
from redbot.core.commands import Cog, parse_timedelta
from redbot.core.utils import AsyncIter
from redbot.core.utils.chat_formatting import pagify

log = logging.getLogger("red.fox_v3.timerole")


async def sleep_till_next_hour():
    now = datetime.utcnow()
    next_hour = datetime(year=now.year, month=now.month, day=now.day, hour=now.hour + 1)
    log.debug("Sleeping for {} seconds".format((next_hour - datetime.utcnow()).seconds))
    await asyncio.sleep((next_hour - datetime.utcnow()).seconds)


async def announce_to_channel(channel, results, title):
    if channel is not None and results:
        await channel.send(title)
        for page in pagify(results, shorten_by=50):
            await channel.send(page)
    elif results:  # Channel is None, log the results
        log.info(results)


class Timerole(Cog):
    """Add roles to users based on time on server"""

    def __init__(self, bot: Red):
        super().__init__()
        self.bot = bot
        self.config = Config.get_conf(self, identifier=9811198108111121, force_registration=True)
        default_global = {}
        default_guild = {"announce": None, "reapply": True, "roles": {}, "skipbots": True}
        default_rolemember = {"had_role": False, "check_again_time": None}

        self.config.register_global(**default_global)
        self.config.register_guild(**default_guild)

        self.config.init_custom("RoleMember", 2)
        self.config.register_custom("RoleMember", **default_rolemember)

        self.updating = asyncio.create_task(self.check_hour())

    async def red_delete_data_for_user(self, **kwargs):
        """Nothing to delete"""
        return

    def cog_unload(self):
        self.updating.cancel()

    @commands.command()
    @checks.guildowner()
    @commands.guild_only()
    async def runtimerole(self, ctx: commands.Context):
        """
        Trigger the hourly timerole

        Useful for troubleshooting the initial setup
        """
        async with ctx.typing():
            pre_run = datetime.utcnow()
            await self.timerole_update()
            after_run = datetime.utcnow()
            await ctx.tick()

        await ctx.maybe_send_embed(f"Took {after_run-pre_run} seconds")

    @commands.group()
    @checks.mod_or_permissions(administrator=True)
    @commands.guild_only()
    async def timerole(self, ctx):
        """Adjust timerole settings"""
        pass

    @timerole.command()
    async def addrole(
        self, ctx: commands.Context, role: discord.Role, time: str, *requiredroles: discord.Role
    ):
        """Add a role to be added after specified time on server"""
        guild = ctx.guild

        try:
            parsed_time = parse_timedelta(time, allowed_units=["weeks", "days", "hours"])
        except commands.BadArgument:
            await ctx.maybe_send_embed("Error: Invalid time string.")
            return

        if parsed_time is None:
            return await ctx.maybe_send_embed("Error: Invalid time string.")

        days = parsed_time.days
        hours = parsed_time.seconds // 60 // 60

        to_set = {"days": days, "hours": hours, "remove": False}
        if requiredroles:
            to_set["required"] = [r.id for r in requiredroles]

        await self.config.guild(guild).roles.set_raw(role.id, value=to_set)
        await ctx.maybe_send_embed(
            f"Time Role for {role.name} set to {days} days  and {hours} hours until added"
        )

    @timerole.command()
    async def removerole(
        self, ctx: commands.Context, role: discord.Role, time: str, *requiredroles: discord.Role
    ):
        """
        Add a role to be removed after specified time on server

        Useful with an autorole cog
        """
        guild = ctx.guild
        try:
            parsed_time = parse_timedelta(time, allowed_units=["weeks", "days", "hours"])
        except commands.BadArgument:
            await ctx.maybe_send_embed("Error: Invalid time string.")
            return

        days = parsed_time.days
        hours = parsed_time.seconds // 60 // 60

        to_set = {"days": days, "hours": hours, "remove": True}
        if requiredroles:
            to_set["required"] = [r.id for r in requiredroles]

        await self.config.guild(guild).roles.set_raw(role.id, value=to_set)
        await ctx.maybe_send_embed(
            f"Time Role for {role.name} set to {days} days  and {hours} hours until removed"
        )

    @timerole.command()
    async def channel(self, ctx: commands.Context, channel: Optional[discord.TextChannel] = None):
        """Sets the announce channel for role adds"""
        guild = ctx.guild
        if channel is None:
            await self.config.guild(guild).announce.clear()
            await ctx.maybe_send_embed(f"Announce channel has been cleared")
        else:
            await self.config.guild(guild).announce.set(channel.id)
            await ctx.send(f"Announce channel set to {channel.mention}")

    @timerole.command()
    async def reapply(self, ctx: commands.Context):
        """Toggle reapplying roles if the member loses it somehow. Defaults to True"""
        guild = ctx.guild
        current_setting = await self.config.guild(guild).reapply()
        await self.config.guild(guild).reapply.set(not current_setting)
        await ctx.maybe_send_embed(f"Reapplying roles is now set to: {not current_setting}")

    @timerole.command()
    async def skipbots(self, ctx: commands.Context):
        """Toggle skipping bots when adding/removing roles. Defaults to True"""
        guild = ctx.guild
        current_setting = await self.config.guild(guild).skipbots()
        await self.config.guild(guild).skipbots.set(not current_setting)
        await ctx.maybe_send_embed(f"Skipping bots is now set to: {not current_setting}")

    @timerole.command()
    async def delrole(self, ctx: commands.Context, role: discord.Role):
        """Deletes a role from being added/removed after specified time"""
        guild = ctx.guild

        await self.config.guild(guild).roles.set_raw(role.id, value=None)
        await self.config.custom("RoleMember", role.id).clear()
        await ctx.maybe_send_embed(f"{role.name} will no longer be applied")

    @timerole.command()
    async def list(self, ctx: commands.Context):
        """Lists all currently setup timeroles"""
        guild = ctx.guild

        role_dict = await self.config.guild(guild).roles()
        out = "Current Timeroles:\n"
        for r_id, r_data in role_dict.items():
            if r_data is not None:
                role = discord.utils.get(guild.roles, id=int(r_id))
                r_roles = []
                if role is None:
                    role = r_id
                if "required" in r_data:
                    r_roles = [
                        str(discord.utils.get(guild.roles, id=int(new_id)))
                        for new_id in r_data["required"]
                    ]
                out += f"{role} | {r_data['days']} days | requires: {r_roles}\n"
        await ctx.maybe_send_embed(out)

    async def timerole_update(self):
        utcnow = datetime.utcnow()
        all_guilds = await self.config.all_guilds()

        # all_mrs = await self.config.custom("RoleMember").all()

        # log.debug(f"Begin timerole update")

        for guild in self.bot.guilds:
            guild_id = guild.id
            if guild_id not in all_guilds:
                log.debug(f"Guild has no configured settings: {guild}")
                continue

            add_results = ""
            remove_results = ""
            reapply = all_guilds[guild_id]["reapply"]
            role_dict = all_guilds[guild_id]["roles"]
            skipbots = all_guilds[guild_id]["skipbots"]

            if not any(role_dict.values()):  # No roles
                log.debug(f"No roles are configured for guild: {guild}")
                continue

            # all_mr = await self.config.all_custom("RoleMember")
            # log.debug(f"{all_mr=}")

            async for member in AsyncIter(guild.members, steps=10):

                if member.bot and skipbots:
                    continue

                addlist = []
                removelist = []

                for role_id, role_data in role_dict.items():
                    # Skip non-configured roles
                    if not role_data:
                        continue

                    mr_dict = await self.config.custom("RoleMember", role_id, member.id).all()

                    # Stop if they've had the role and reapplying is disabled
                    if not reapply and mr_dict["had_role"]:
                        log.debug(f"{member.display_name} - Not reapplying")
                        continue

                    # Stop if the check_again_time hasn't passed yet
                    if (
                        mr_dict["check_again_time"] is not None
                        and datetime.fromisoformat(mr_dict["check_again_time"]) >= utcnow
                    ):
                        log.debug(f"{member.display_name} - Not time to check again yet")
                        continue
                    member: discord.Member
                    has_roles = {r.id for r in member.roles}

                    # Stop if they currently have or don't have the role, and mark had_role
                    if (int(role_id) in has_roles and not role_data["remove"]) or (
                        int(role_id) not in has_roles and role_data["remove"]
                    ):
                        if not mr_dict["had_role"]:
                            await self.config.custom(
                                "RoleMember", role_id, member.id
                            ).had_role.set(True)
                        log.debug(f"{member.display_name} - applying had_role")
                        continue

                    # Stop if they don't have all the required roles
                    if role_data is None or (
                        "required" in role_data and not set(role_data["required"]) & has_roles
                    ):
                        continue

                    check_time = member.joined_at + timedelta(
                        days=role_data["days"],
                        hours=role_data.get("hours", 0),
                    )

                    # Check if enough time has passed to get the role and save the check_again_time
                    if check_time >= utcnow:
                        await self.config.custom(
                            "RoleMember", role_id, member.id
                        ).check_again_time.set(check_time.isoformat())
                        log.debug(
                            f"{member.display_name} - Not enough time has passed to qualify for the role\n"
                            f"Waiting until {check_time}"
                        )
                        continue

                    if role_data["remove"]:
                        removelist.append(role_id)
                    else:
                        addlist.append(role_id)

                # Done iterating through roles, now add or remove the roles
                if not addlist and not removelist:
                    continue

                # log.debug(f"{addlist=}\n{removelist=}")
                add_roles = [
                    discord.utils.get(guild.roles, id=int(role_id)) for role_id in addlist
                ]
                remove_roles = [
                    discord.utils.get(guild.roles, id=int(role_id)) for role_id in removelist
                ]

                if None in add_roles or None in remove_roles:
                    log.info(
                        f"Timerole ran into an error with the roles in: {add_roles + remove_roles}"
                    )

                if addlist:
                    try:
                        await member.add_roles(*add_roles, reason="Timerole", atomic=False)
                    except (discord.Forbidden, discord.NotFound) as e:
                        log.exception("Failed Adding Roles")
                        add_results += f"{member.display_name} : **(Failed Adding Roles)**\n"
                    else:
                        add_results += (
                            " \n".join(
                                f"{member.display_name} : {role.name}" for role in add_roles
                            )
                            + "\n"
                        )
                        for role_id in addlist:
                            await self.config.custom(
                                "RoleMember", role_id, member.id
                            ).had_role.set(True)

                if removelist:
                    try:
                        await member.remove_roles(*remove_roles, reason="Timerole", atomic=False)
                    except (discord.Forbidden, discord.NotFound) as e:
                        log.exception("Failed Removing Roles")
                        remove_results += f"{member.display_name} : **(Failed Removing Roles)**\n"
                    else:
                        remove_results += (
                            " \n".join(
                                f"{member.display_name} : {role.name}" for role in remove_roles
                            )
                            + "\n"
                        )
                        for role_id in removelist:
                            await self.config.custom(
                                "RoleMember", role_id, member.id
                            ).had_role.set(True)

            # Done iterating through members, now maybe announce to the guild
            channel = await self.config.guild(guild).announce()
            if channel is not None:
                channel = guild.get_channel(channel)

            if add_results:
                title = "**These members have received the following roles**\n"
                await announce_to_channel(channel, add_results, title)
            if remove_results:
                title = "**These members have lost the following roles**\n"
                await announce_to_channel(channel, remove_results, title)
        # End

    # async def announce_roles(self, title, role_list, channel, guild, to_add: True):
    #     results = ""
    #     async for member, role_id in AsyncIter(role_list):
    #         role = discord.utils.get(guild.roles, id=role_id)
    #         try:
    #             if to_add:
    #                 await member.add_roles(role, reason="Timerole")
    #             else:
    #                 await member.remove_roles(role, reason="Timerole")
    #         except (discord.Forbidden, discord.NotFound) as e:
    #             results += f"{member.display_name} : {role.name} **(Failed)**\n"
    #         else:
    #             results += f"{member.display_name} : {role.name}\n"
    #     if channel is not None and results:
    #         await channel.send(title)
    #         for page in pagify(results, shorten_by=50):
    #             await channel.send(page)
    #     elif results:  # Channel is None, log the results
    #         log.info(results)

    # async def check_required_and_date(self, role_list, check_roles, has_roles, member, role_dict):
    #     async for role_id in AsyncIter(check_roles):
    #         # Check for required role
    #         if "required" in role_dict[str(role_id)]:
    #             if not set(role_dict[str(role_id)]["required"]) & set(has_roles):
    #                 # Doesn't have required role
    #                 continue
    #
    #         if (
    #             member.joined_at
    #             + timedelta(
    #                 days=role_dict[str(role_id)]["days"],
    #                 hours=role_dict[str(role_id)].get("hours", 0),
    #             )
    #             <= datetime.utcnow()
    #         ):
    #             # Qualifies
    #             role_list.append((member, role_id))

    async def check_hour(self):
        await sleep_till_next_hour()
        while self is self.bot.get_cog("Timerole"):
            await self.timerole_update()
            await sleep_till_next_hour()