from redbot.core.utils.chat_formatting import *
from redbot.core import Config, checks, commands, bank
from redbot.core.data_manager import cog_data_path
import discord

import os
import glob
import asyncio
from difflib import get_close_matches
from typing import Literal
import tabulate

from .utils import saysound, code_path

EXT = ("mp3", "flac", "ogg", "wav")


class SFX(commands.Cog):
    """Play saysounds in VC's in your guild
    Supports costs, files, and links.
    """

    def __init__(self, bot):
        self.bot = bot
        self.config = Config.get_conf(self, identifier=8495126065166516132, force_registration=True)

        # saysounds maps saysound name (str) -> saysound data (dict)
        default_guild = {"saysounds": {}, "FREE_ROLES": []}
        default_global = {"attachments": True}

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

        global PATH
        global AUDIO_CODE_PATH
        PATH = str(cog_data_path(cog_instance=self))
        audio_cog = bot.get_cog("Audio")
        if audio_cog:
            AUDIO_CODE_PATH = str(code_path(cog_instance=audio_cog))
        else:
            AUDIO_CODE_PATH = None

    # integrates cost manager free roles!
    # if cost_manager isn't loaded, use this cog's free roles
    async def get_cost(self, member: discord.Member, sound: dict):
        cost = sound["cost"]
        cost_manager = self.bot.get_cog("CostManager")

        if cost_manager:
            free_roles = await cost_manager.config.guild(member.guild).FREE_ROLES()
        else:
            free_roles = await self.config.guild(member.guild).FREE_ROLES()

        member_roles = {r.id for r in member.roles if r.name != "@everyone"}
        found_roles = set(free_roles) & member_roles
        if found_roles:
            cost = 0

        return cost

    # plays sound in vc of author
    async def play(self, ctx, sound: dict):
        audio_cog = self.bot.get_cog("Audio")
        if not audio_cog:
            await ctx.send(error("Unable to load audio cog, please contact bot owner!"))
            return

        try:
            query = sound["url"] if sound["url"] else sound["filepath"]
            await audio_cog.sfx_play(ctx, query, volume=sound["volume"])
        except Exception as e:
            await ctx.send(error("Unable to play sound, please contact bot owner!"))
            print(e)  # TODO add logging properly

    @commands.group()
    @checks.admin_or_permissions(administrator=True)
    async def sfxset(self, ctx):
        """
        Manage settings for saysounds
        """
        pass

    @sfxset.command(name="vol")
    @commands.guild_only()
    async def sfxset_vol(self, ctx, vol: int, *, name: str):
        """
        Change volume of sfx sound
        """
        async with self.config.guild(ctx.guild).saysounds() as saysounds:
            try:
                saysounds[name]["volume"] = vol
            except:
                await ctx.send(error("Sound not found, try again!"))
                return

        await ctx.tick()

    @sfxset.command(name="cost")
    @commands.guild_only()
    async def sfxset_cost(self, ctx, cost: int, *, name: str):
        """
        Change cost of sfx sound
        """
        async with self.config.guild(ctx.guild).saysounds() as saysounds:
            try:
                saysounds[name]["cost"] = cost
            except:
                await ctx.send(error("Sound not found, try again!"))
                return

        await ctx.tick()

    @sfxset.command(name="name")
    @commands.guild_only()
    async def sfxset_name(self, ctx, old_name: str, *, new_name: str):
        """
        Change name of sfx sound

        **OLD_NAME MUST be in quotes if it has spaces!**
        """
        async with self.config.guild(ctx.guild).saysounds() as saysounds:
            try:
                temp = saysounds[old_name]
                saysounds[new_name] = temp
                saysounds[new_name]["name"] = new_name
                del saysounds[old_name]
            except:
                await ctx.send(error("Sound not found, try again!"))
                return

        await ctx.tick()

    @sfxset.command(name="add")
    @commands.guild_only()
    async def sfxset_add(self, ctx, cost: int, vol: int, *, name: str):
        """
        Add an sfx sound for the guild.

        Attach the audio file to the message.
        """
        can_use = await self.config.attachments()
        name = name.lower()

        if not can_use:
            await ctx.send(error("Sorry, I only allow adding say sounds using URLs."))
            return

        if len(ctx.message.attachments) < 1:
            await ctx.send(error("Please provide an attachment."))
            return

        sounds = await self.config.guild(ctx.guild).saysounds()
        try:
            x = sounds[name]
            await ctx.send(error("Sound already exists by that name, please delete it first!"))
            return
        except:
            pass

        file = ctx.message.attachments[0]
        ext = file.filename.split(".")[-1]

        if ext not in EXT:
            await ctx.send(error("Audio file must one of `mp3, wav, flac, or ogg` formats."))
            return

        save_path = os.path.join(PATH, str(ctx.guild.id))
        if not os.path.exists(save_path):
            os.makedirs(save_path)

        save_path = os.path.join(save_path, file.filename)

        try:
            await file.save(save_path)
        except:
            await ctx.send(error("Error saving file, please try again."))
            return

        author = f"{ctx.author} id: {ctx.author.id}"
        new_sound = saysound(name, author, cost=cost, volume=vol, filepath=save_path)
        sounds[name] = new_sound

        await self.config.guild(ctx.guild).saysounds.set(sounds)

        await ctx.tick()

    @sfxset.command(name="addurl")
    @commands.guild_only()
    async def sfxset_addurl(self, ctx, cost: int, vol: int, url: str, *, name: str):
        """
        Add an sfx sound for the guild.

        URL must be a direct link to the audio file, a youtube link, spotify link,
        soundcloud link etc.
        You can test if it will work using the `play` command on the bot.
        """
        name = name.lower()
        async with self.config.guild(ctx.guild).saysounds() as saysounds:
            try:
                x = saysounds[name]
                await ctx.send(error("Sound already exists by that name, please delete it first!"))
                return
            except:
                pass
            author = f"{ctx.author} id: {ctx.author.id}"
            new_sound = saysound(name, author, cost=cost, volume=vol, url=url)
            saysounds[name] = new_sound

        await ctx.tick()

    @sfxset.command(name="del")
    @commands.guild_only()
    async def sfxset_del(self, ctx, *, name: str):
        """
        Delete an sfx sound for the guild.
        """
        name = name.lower()
        async with self.config.guild(ctx.guild).saysounds() as saysounds:
            try:
                del saysounds[name]
                await ctx.tick()
            except KeyError:
                await ctx.send(error("That say sound does not exist!"))

    @sfxset.command(name="file")
    @checks.is_owner()
    async def sfxset_file(self, ctx, *, on_off: bool = None):
        """
        Enable or disable allowing to save audio files directly for playback
        """
        curr = await self.config.attachments()
        if on_off is None:
            msg = "on" if curr else "off"
            await ctx.send(f"Allowing saving of audio files is currently {msg}.")
            return

        await self.config.attachments.set(on_off)
        await ctx.tick()

    @sfxset.command(name="setup")
    @checks.is_owner()
    async def sfxset_setup(self, ctx):
        """
        Run this first to inject code into audio cog for proper sfx usage.

        After injection, reload audio cog and run this again to confirm injection
        was successful.
        """
        audio_cog = self.bot.get_cog("Audio")

        if not audio_cog:
            await ctx.send(error("Audio cog not loaded, load it first before running this."))
            return

        try:
            if callable(audio_cog.sfx_play):
                await ctx.send("Injection successful, SFX is ready to use!")
            else:
                await ctx.send("Function found but not callable, unknown error.")
            return
        except:
            pass

        if not AUDIO_CODE_PATH:
            await ctx.send(error("Please reload the cog after the Audio cog has been loaded!"))
            return

        inject_path = str(code_path(cog_instance=self) / "injection.txt")
        with open(inject_path, "r") as f:
            injection = f.read()

        inject_path = os.path.join(AUDIO_CODE_PATH, "utilities", "player.py")
        with open(inject_path, "a") as f:
            f.write(injection)

        await ctx.send("Injection complete, reload audio cog then run this command again to make sure it worked.")

    @commands.command(name="sfx")
    @commands.guild_only()
    @commands.cooldown(rate=1, per=10, type=commands.BucketType.user)
    async def sfx(self, ctx, *, name: str):
        """
        Play a say sound!
        """
        # TODO: cost manager receipt integration
        if not ctx.author.voice or ctx.author.voice.channel is None:
            await ctx.send(error("Connect to a voice channel to use this command."))
            return

        # TODO: create my own queue to fix this issue
        if ctx.guild.me.voice and ctx.guild.me.voice.channel != ctx.author.voice.channel:
            await ctx.send(error("Please wait for the bot to disconnect from it's VC before using the command."))
            return

        name = name.lower()
        saysounds = await self.config.guild(ctx.guild).saysounds()
        audio_cog = self.bot.get_cog("Audio")
        play = self.bot.get_command("play")
        volume = self.bot.get_command("volume")
        if not play:
            await ctx.send(error("Audio cog not loaded! Please contact bot owner."))
            return

        try:
            sound = saysounds[name]
        except KeyError:
            # name doesn't have to be full name, will find closest match
            matches = get_close_matches(name, list(saysounds.keys()), n=1, cutoff=0.7)
            if matches:
                sound = saysounds[matches[0]]
            else:
                await ctx.send(error("Say sound could not be found!"))
                return

        # found saysound

        # charge user
        cost = await self.get_cost(ctx.author, sound)
        msg = None
        if cost > 0:
            currency_name = await bank.get_currency_name(ctx.guild)
            try:
                await bank.withdraw_credits(ctx.author, cost)
                balance = await bank.get_balance(ctx.author)
                msg = await ctx.send(f"Charged: {cost}, Balance: {balance}")
            except ValueError:
                balance = await bank.get_balance(ctx.author)
                msg = await ctx.send(
                    error(
                        f"Sorry {ctx.author.name}, you do not have enough {currency_name} to use that say sound. (Cost: {cost}, Balance: {balance})"
                    )
                )
                await asyncio.sleep(10)
                await msg.delete()
                return

        await self.play(ctx, sound)

        if msg:
            await asyncio.sleep(5)
            await msg.delete()

    @commands.command(name="sfxlist")
    async def sfx_list(self, ctx):
        """
        List all say sounds for guild.
        """

        saysounds = await self.config.guild(ctx.guild).saysounds()
        msg = []

        keys = sorted(list(saysounds.keys()))

        for sound_name in keys:
            msg.append((sound_name, saysounds[sound_name]["cost"]))

        msg = tabulate.tabulate(msg, ["Sound", "Cost"], tablefmt="github")

        pages = pagify(msg)

        for page in pages:
            try:
                await ctx.author.send(box(page))
            except:
                await ctx.send("Please allow DMs from server members so I can DM you the list!")
                return

    async def red_delete_data_for_user(
        self,
        *,
        requester: Literal["discord_deleted_user", "owner", "user", "user_strict"],
        user_id: int,
    ):
        pass
