import random
import discord
from discord.ext import commands
import storybot_exceptions
from discord.interactions import MISSING
import cogs.views as views
from better_profanity import profanity

class GenericDiscord():
    user_manager = None
    file_manager = None
    
    def __init__(self, bot: commands.bot, config_manager) -> None:
        self.bot = bot
        self.config_manager = config_manager
        self.initialize_lists()
        
        profanity.load_censor_words()
        with open("required/profanity-list.txt", "r") as bad_words_file:
            BAD_WORDS = bad_words_file.read().strip().split("\n")
        profanity.add_censor_words(BAD_WORDS)
        
    def initialize_lists(self) -> None:
        """Initializes the list of tips"""
        with open("required/tips.txt", "r") as file:
            self.tips =  file.readlines()
            
    def get_random_tip(self, override: bool = False) -> str:
        """Returns a random tip if the odds are met

        Args:
            override (bool, optional): If true, will return a tip regardless of the odds. Defaults to False.

        Returns:
            str: Either None or a random fact
        """
        if override or random.randint(1, 30) == 1:
            q = ""
            while q == "":
                q = random.choice(self.tips)[:-1]
            return "Did you know that " + q
        else:
            return None
        
    async def create_embed(
        self,
        content: str = None,
        color: discord.Color = None,
        title: str = None,
        author_name: str = None,
        author_icon_url : str = None,
        random_fact: bool = True,
        user_id : int = None,
        footer: str = None,
        url: str = None,
        image_url: str = None
        ) -> discord.Embed:
        """Creates an embed with the given parameters. All values have defaults if not given

        Args:
            content (str, optional): The body of the embed. Defaults to None.
            color (discord.Color, optional): The color of the embed. Defaults to the color in the config manager
            title (str, optional): The title of the embed. Defaults to None.
            author_name (str, optional): The author name of the embed. Defaults to None.
            author_icon_url (str, optional): The user icon url. If this has a value, make sure to set a value for author_name. Defaults to None.
            random_fact (bool, optional): Determines whether a random fact may be randomly inserted into the footer. Defaults to True.
            user_id (int, optional): The user_id of the author. Determines if a random embed can be included in the footer. Defaults to None.
            footer (str, optional): The footer of the embed. Defaults to None.
            url (str, optional): The url the embed's title links to. Defaults to None.
            image_url (str, optional): the url of the embed's image (as opposed to the icon). Defaults to None.

        Returns:
            discord.Embed: An embed with the given values
        """
        
        if color == None:
            color = await self.config_manager.get_embed_color()
        emb = discord.Embed(description=content, color=color, title=title)
        if author_name != None or author_icon_url != None:
            emb.set_author(name=author_name, icon_url=author_icon_url)
        
        if random_fact and footer == None and (user_id != None and await self.config_manager.get_fun_facts_enabled(user_id=user_id)):
            emb.set_footer(text=self.get_random_tip())
        else:
            emb.set_footer(text=footer)
            
        emb.url = url
        
        if image_url != None:
            emb.set_image(url=image_url)
        
        return emb
    
    
    async def send_message_in_announcement_channel(self, guild_id: int, message: str = None, embed: discord.Embed = None, view = MISSING):
        """Sends a message in the announcement channel

        Args:
            guild_id (int): the guild to make the announcement in
            message (str): the text to send
            embed (discord.Embed): the embed to send
            view: the view to send

        Raises:
            storybot_exceptions.NoValidAnnouncementChannelException: there is no proper channel id
            discord.errors.Forbidden: The bot was missing the perms to send the message
        """
        
        if message == None and embed == None:
            raise ValueError("Both message and embed cannot be None for send_message_in_announcement_channel!")
        
        channel_int = await self.config_manager.get_story_announcement_channel(guild_id)
        if channel_int != None:
            channel = self.bot.get_channel(channel_int)
            
            if channel != None:
                await channel.send(embed = embed, content=message, view=view)
            else:
                raise storybot_exceptions.NoValidAnnouncementChannelException(f"Channel {channel_int} in {guild_id} isn't a proper announcement channel!")
            
        else:
            raise storybot_exceptions.NoValidAnnouncementChannelException(f"Guild {guild_id} doesn't have an announcement channel!")  
        
    @staticmethod
    def print_time(seconds:int, include_seconds: bool = False) -> str:
        """Generates a printable string based on a given number of seconds. `timedelta` has a built-in method for this, but it kept saying `Python int too large to convert to C int`

        Args:
            seconds (int): the amount of seconds to print

        Returns:
            str: the printable string. If the amount of seconds is negative, this will return "Should time out soon..."
        """
        
    
        print_seconds = seconds % 60
        if print_seconds == seconds:
            if seconds != 0:
                return f"around {print_seconds} s"
            else:
                return "a moment or two"
        print_minutes = int((seconds/60) % 60)
        print_hours = int((seconds/3600) % 24)
        print_days = int(seconds/(60 * 60 * 24))
        
        if(print_days < 0):
            return "Should time out soon..."
        elif(print_days > 0):
            s = f"{print_days} d, "
        else:
            s = ""
        
        s += f"{print_hours} h, {print_minutes} m"
        
        if include_seconds and print_seconds != 0:
            s += f", {print_seconds} s"
        
        return s
    
    async def reply_to_message(
        self,
        reply_to: discord.Message | discord.Interaction,
        content: str = "",
        embed: discord.Embed = None,
        file: discord.File = None,
        author_name: str = None,
        author_icon_url: str = None,
        title: str = None,
        ephemeral: bool = True,
        view: discord.ui.View = None,
        error: bool = False,
        random_fact: bool = True
        ) -> discord.Message:
        
        """Replies to a given message or interaction
        
        Args:
            reply_to (discord.Message | discord.Interaction): the message or interaction to reply to
            content (str): the text to reply with
            embed (discord.Embed): the embed to attach. If it is None, a new embed will be created with the content being `content`
            file (discord.File): the file to attach
            author_name (str): the name of the author of the message/generated embed
            author_icon_url (str): the icon url of the author of the message/generated embed
            title (str): the title of the message/generated embed
            ephemeral (bool): whether the message should be ephemeral. If `reply_to` is a `message`, this won't do anything.
            view (discord.ui.View): the view to attach to the message
            error (bool): whether the message is an error message. Makes the generated embed red and, if the view is None, will add a link to the support server
            random_fact (bool): whether or not to have a chance of attaching a random "Did you know?" fact
            
        Raises:
            TypeError: `reply_to` is not a message or interaction

        Returns:
            discord.Message: the reply sent. If `reply_to` is an interaction, this will be `None`
        """
        
        message = None
        interaction = None
        user_id = None
        
        if isinstance(reply_to, discord.Message):
            message = reply_to
            user_id = message.author.id
        elif isinstance(reply_to, discord.Interaction):
            interaction = reply_to
            user_id = interaction.user.id
        else:
            raise TypeError(f"reply_to must be of type discord.Message or discord.Interaction, not {type(reply_to)}!")
        
        
        if error and view == None:
            view = views.LinkToSupportServer()
        
        if embed == None: #Add the content into an embed
            
            if len(content) > 4096: # Check if the content needs to be divided into multiple segments
                text_list = self.pieMethod(content)
                llen = len(text_list)
                for i in range(llen):
                    text = text_list[i]
                    if i == 0:
                        # First element
                        await self.reply_to_message(
                            content=text, reply_to=reply_to, author_name=author_name, author_icon_url=author_icon_url, title=title, ephemeral=ephemeral, error=error, random_fact=False
                            #, file=None, view=None, embed=None
                        )
                        
                    elif i != llen - 1:
                        # Middle element
                        await self.reply_to_message(
                            content=text, reply_to=reply_to, ephemeral=ephemeral, error=error, random_fact=False
                            #, file=None, author_name=None, author_icon_url=None, title=None, embed=None, view=None
                        )
                    else:
                        # Last element
                        return await self.reply_to_message(
                            content=text, reply_to=reply_to, file=file, ephemeral=ephemeral, view=view, error=error, random_fact=random_fact, embed=embed
                            #, author_name=None, author_icon_url=None, title=None
                        )
            
            embed = await self.create_embed(content=content, title=title, random_fact = random_fact, user_id=user_id)
            
            if author_icon_url != None:
                embed.set_author(name=author_name, icon_url=author_icon_url)
                
            if error:
                embed.color = discord.Color.brand_red()
        
        try:
            if not message is None:
                return await message.reply(embed = embed, file = file, mention_author = False, view=view)
            elif not interaction is None:
                if file == None:
                    file = MISSING
                
                if view == None:
                    view = MISSING
                            
                if interaction.response.is_done():
                    return await interaction.followup.send(embed=embed, file=file, view = view, ephemeral=ephemeral)
                else:
                    return await interaction.response.send_message(embed=embed, view=view, ephemeral=ephemeral, file=file)
            
        except discord.errors.Forbidden:
            # TODO: Check for this perm properly
            pass
        
    @staticmethod
    def pieMethod(story: str, max_message_length: int = 2000) -> list[str]:
        """The all-powerful pieMethod. Splits the story into a list of strings if it is too long. Breaks the story at spaces and line breaks whenever possible. If there is not enough text to need to be split, the function will return the entire string as one element in a list.

        Args:
            story (str): the text to split

        Returns:
            list[str]: the list split up
        """
        text_len = len(story)
        if text_len <= max_message_length:
            return [story]
        list = []
        stored_last_character_index = 1
        
        
        while stored_last_character_index < text_len:
            new_last_index = max(
                story.rfind(" ", stored_last_character_index, stored_last_character_index + max_message_length),
                story.rfind("\n", stored_last_character_index, stored_last_character_index + max_message_length)
            )
            if new_last_index == -1:
                new_last_index = stored_last_character_index + max_message_length - 2
            
            text = story[stored_last_character_index - 1 : new_last_index + 1].strip()
            
            if text != "":
                list.append(text)
                
            stored_last_character_index = new_last_index + 2
            
        return list

    async def dm_user(self, user_id: int, message, file = None, embed = None, view = MISSING, raise_error_on_forbidden: bool = True, raise_error_on_not_found: bool = True):
        """Sends the given message to the current user
        
        Raises:
            discord.errors.Forbidden
            discord.errors.NotFound
        """
        if user_id == None: return
        if not await self.config_manager.get_dms_enabled(user_id): return
        
        try:
            channel = await (await self.bot.fetch_user(user_id)).create_dm()
            return await channel.send(message, embed=embed, file = file, view=view)
        except (discord.errors.Forbidden, discord.errors.NotFound) as ex:
            if raise_error_on_forbidden and isinstance(ex, discord.errors.Forbidden):
                raise ex
            elif raise_error_on_not_found and isinstance(ex, discord.errors.NotFound):
                raise ex
            
    async def get_author_role(self, guild: discord.Guild) -> discord.Role:
        """Returns the author role for a given guild

        Args:
            guild (discord.Guild): The guild to get the role for

        Returns:
            discord.Role: The author role for that server or None if none exists
        """
        #the column is `author_role_id`
        try:
            role_id = await self.file_manager.get_role_id(guild_id=guild.id)
            if role_id != None:
                return discord.utils.get(guild.roles, id=role_id)
            else:
                return None
        except (discord.errors.NotFound, discord.errors.Forbidden):
            return None
            
    async def is_moderator(self, user_id: int, guild_channel: discord.abc.GuildChannel) -> bool:
        """Determines if a user is a server moderator in a given guild

        Args:
            user_id (int): the userID to check
            guild_channel (discord.abc.GuildChannel): The channel to check

        Returns:
            bool: True if the user is a mod; false if they don't exist or if the user isn't a mod
        """
        try:
            user = await guild_channel.guild.fetch_member(user_id)
        except discord.errors.NotFound:
            print(f"No user found: {user_id} {guild_channel}")
            return False
        return guild_channel.permissions_for(user).moderate_members
    
    async def add_role_to_all_authors(self, guild: discord.Guild, role: discord.Role):
        """Adds the given role to all authors in a guild

        Args:
            guild (discord.Guild): the guild to add the role in
            role (discord.Role): the role to add

        Raises:
            storybot_exceptions.MissingPermissionsException: the bot is missing the `manage_roles` permission
            discord.errors.Forbidden: The bot gets this error when adding the role, likely meaning it's in the wrong order
        """
        if not guild.me.guild_permissions.manage_roles:
            raise storybot_exceptions.MissingPermissionsException("I don't have the manage_roles permission!")
        
        users_in_guild = await self.user_manager.get_active_and_inactive_users(guild_id=guild.id)
        
        for user_id in users_in_guild:
            try:
                mem: discord.Member = await guild.fetch_member(user_id)
            except (discord.errors.Forbidden, discord.errors.HTTPException, discord.errors.NotFound):
                continue
            
            try:
                # Add the Storybot Author role to the user
                if not role in mem.roles:
                    await mem.add_roles(role)
            except (discord.errors.Forbidden) as ex:
                raise ex
            
                
    async def choose_a_channel_in_a_server(self, guild_id: int, interaction: discord.Interaction, message=None) -> int:
        server_json = []
        self_member = self.bot.get_guild(guild_id).get_member(self.bot.user.id)
        
        for channel in self.bot.get_guild(guild_id).text_channels:
            if channel.permissions_for(self_member).send_messages == True:
                server_json.append({"id": channel.id, "name": channel.name})
        
        server_json.append({"id": -1, "name": "<Disabled>"})
        server_json.append({"id": 0, "name": "<Cancel>"})
        
        view = views.DropdownView(server_json, "Choose a channel")
        
        if message == None:
            message = "Which channel would you like to choose?"
        
        my_message = await self.reply_to_message(reply_to=interaction, content=message, view=view, ephemeral= True)
        
        await view.wait()
        
        await interaction.delete_original_response()
        
        if view.value == None:
            return None
        else:
            return int(view.value)
        
        
    def contains_profanity(self, content: str) -> bool:
        return profanity.contains_profanity(content)
    
    async def confirm(self, respond_to: discord.Message | discord.Interaction, message: str = None, embed: discord.Embed = None) -> bool:
        """Sends a confirmation message to the user

        Args:
            respond_to (discord.Message | discord.Interaction): the message or interaction to respond to
            message (str): the message to confirm
            embed (discord.Embed): the embed to send

        Returns:
            bool: True if the user said yes, False if they said no or the interaction times out
        """
        
        view = views.Confirm()
        try:
            if isinstance(respond_to, discord.Message):
                msg = await self.reply_to_message(reply_to=respond_to, embed = embed, content = message, random_fact=False, view = view, ephemeral=True)
            else:
                msg = await self.reply_to_message(reply_to=respond_to, embed = embed, content = message, random_fact=False, view = view, ephemeral=True)
        except discord.errors.InteractionResponded:
            if isinstance(respond_to, discord.Message):
                msg = await self.reply_to_message(reply_to=respond_to, embed = embed, content = message, random_fact=False, view = view, ephemeral=True)
            else:
                msg = await self.reply_to_message(reply_to=respond_to, embed = embed, content = message, random_fact=False, view = view, ephemeral=True)
            
        await view.wait()
        
        if view.interaction != None:
            await view.interaction.response.defer(ephemeral=True)
        
        await view.interaction.delete_original_response()
        
        
        if view.value is None:
            # They didn't press the button
            if isinstance(respond_to, discord.Message):
                await self.reply_to_message(reply_to=respond_to, content="This interaction timed out. Please try again!", error=True)
            else:
                await self.reply_to_message(reply_to=respond_to, content="This interaction timed out. Please try again!", error=True, ephemeral=True)
            return False
        elif view.value:
            # They pressed Confirm
            return True
        else:
            # They pressed Cancel
            return False
