import asyncio
import logging
import time
import wave

import assemblyai as aai
import discord
from discord.ext import commands, tasks, voice_recv
from discord.opus import Decoder as OpusDecoder

from constants import ASSEMBLYAI_API_KEY, TRANSCRIPTS_PATH

# Credentials come from constants (env ASSEMBLYAI_API_KEY, or the 'assemblyai'
# machine in the netrc at CONJURER_NETRC_FILE). Raising here means the guarded
# extension loader logs the reason and disables ONLY this cog.
if not ASSEMBLYAI_API_KEY:
    raise RuntimeError(
        "AssemblyAI API key not configured (netrc machine 'assemblyai' or "
        "ASSEMBLYAI_API_KEY env) - voice recognition stays disabled"
    )
aai.settings.api_key = ASSEMBLYAI_API_KEY

discord.opus._load_default()
CHANNELS = OpusDecoder.CHANNELS
SAMPLE_WIDTH = OpusDecoder.SAMPLE_SIZE // OpusDecoder.CHANNELS
SAMPLING_RATE = OpusDecoder.SAMPLING_RATE

# rotate file after there is 0.5s between last received pcm for user.
# delete messsages after user disconnect

LOCATION = TRANSCRIPTS_PATH


class CommunicationObject:
    def __init__(self, msg_type, user, data):
        self.logger = logging.getLogger("discord")
        self.type = msg_type
        self.user = user
        self.data = data

    def __repr__(self):
        return f"{self.type} : {self.user} : {self.data}"


class WaveWriter:
    def __init__(self, user_id, queue, location):
        self.queue = queue
        self.user = user_id
        self.username = str(user_id.id)
        self.location = location
        # here we can add hashing function to make transcription files not possible to be connected with discord id
        self.present_file_id = 0
        self.file_name_past = None
        self.file_name_present = (
            self.location + self.username + "_" + str(self.present_file_id) + ".mp3"
        )
        self.transcript_file_present: wave.Wave_write = wave.open(
            self.file_name_present, "wb"
        )
        self.transcript_file_present.setnchannels(CHANNELS)
        self.transcript_file_present.setsampwidth(SAMPLE_WIDTH)
        self.transcript_file_present.setframerate(SAMPLING_RATE)
        self.file_name_future = (
            self.location + self.username + "_" + str(self.present_file_id + 1) + ".mp3"
        )
        self.transcript_file_future: wave.Wave_write = wave.open(
            self.file_name_future, "wb"
        )
        self.transcript_file_future.setnchannels(CHANNELS)
        self.transcript_file_future.setsampwidth(SAMPLE_WIDTH)
        self.transcript_file_future.setframerate(SAMPLING_RATE)

    async def rotate(self):

        self.transcript_file_present.close()
        self.file_name_past = self.file_name_present
        if self.present_file_id > 10:
            self.present_file_id = 0
        else:
            self.present_file_id += 1
        self.file_name_present = self.file_name_future
        self.transcript_file_present = self.transcript_file_future
        self.file_name_future = (
            self.location + self.username + "_" + str(self.present_file_id + 1) + ".mp3"
        )
        self.transcript_file_future: wave.Wave_write = wave.open(
            self.file_name_future, "wb"
        )
        self.transcript_file_future.setnchannels(CHANNELS)
        self.transcript_file_future.setsampwidth(SAMPLE_WIDTH)
        self.transcript_file_future.setframerate(SAMPLING_RATE)
        operation = CommunicationObject(
            msg_type="send_file", user=self.user, data=[self.file_name_past]
        )

        await self.queue.put(operation)

    def writeframes(self, pcmdata):
        self.transcript_file_present.writeframes(pcmdata)

    def cleanup(self):
        self.logger.info("Cleanup for user %s", self.username)
        self.transcript_file_present.close()
        self.transcript_file_future.close()


class SRBuffer(voice_recv.AudioSink):
    """Endpoint AudioSink that generates a wav file.
    Best used in conjunction with a silence generating sink. (TBD)
    """

    # on member join dodajemytypa do listy
    # on member disconnect - dropujemy go
    def __init__(self, queue, location):
        super().__init__()
        self.queue = queue
        self.wavewriter = {}
        self.logger = logging.getLogger("discord")
        self.location = location

    def on_user_connect(self, username):
        self.wavewriter[str(username.id)] = [
            WaveWriter(username, self.queue, self.location),
            time.time_ns(),
            time.time_ns(),
            False,
            False,
        ]

    def on_user_disconnect(self, username):
        self.wavewriter[str(username.id)].cleanup()
        self.wavewriter.pop(str(username.id))

    def wants_opus(self) -> bool:
        return False

    def write(self, user, data) -> None:
        # logger.info("DAta write")
        if user:
            self.wavewriter[str(user.id)][0].writeframes(data.pcm)
            self.wavewriter[str(user.id)][1] = time.time_ns()  # time from last write
            self.wavewriter[str(user.id)][3] = True  # data written in last iter
            self.wavewriter[str(user.id)][4] = True  # data in buff
            time.sleep(0.001)

    def cleanup(self) -> None:
        try:
            self.logger.info("Cleanup for SRBuffer")
            for item in self.wavewriter.values():
                item[0].cleanup()
        except Exception:
            self.logger.warning(
                "WaveSink got error closing file on cleanup", exc_info=True
            )


class Transcriber(commands.Cog):
    def __init__(self, bot):
        self.bot = bot
        self.threads = []
        self.comm_queue = asyncio.Queue()
        self.wsink = SRBuffer(self.comm_queue, LOCATION)
        self.worker = None
        self.mc = None
        self.vc = None
        self.logger = logging.getLogger("discord")

    @commands.hybrid_command(name="transcribe")
    @commands.has_any_role('Nocna Zmiana', 'Jarl', 'Thane' , 'Bartender')
    async def test(self, ctx):
        if self.vc:
            vc = self.vc  # to juz powinien byc voice channel z funkcja conenct
        else:
            vc = None
        self.mc = ctx.message.channel
        if self.bot.voice_clients:
            if isinstance(self.bot.voice_clients[0], voice_recv.VoiceRecvClient):
                self.logger.debug("Already transcribing")
            else:
                self.logger.debug("Already connected with other client")
        else:
            self.logger.debug("Connected")
            vc = await ctx.author.voice.channel.connect(cls=voice_recv.VoiceRecvClient)
            self.logger.info(self.bot.voice_clients)
            self.check_data.start()
            self.worker = asyncio.create_task(self.transcribe_output_queue())
            vc.listen(self.wsink)

    @tasks.loop(seconds=0.5)
    async def check_data(self):
        for item in self.wsink.wavewriter.values():
            if not item[3]:
                timediff_rotation = time.time_ns() - item[2]
                timediff_write = time.time_ns() - item[1]
                if (
                    timediff_rotation > 13000149433
                    and timediff_write > 500014943
                    and item[4]
                ):
                    self.logger.info(
                        "File rotation time since last write %s %s",
                        timediff_write,
                        timediff_write / 1e9,
                    )
                    self.logger.info(
                        "File rotation time since last rotation %s %s",
                        timediff_rotation,
                        timediff_rotation / 1e9,
                    )
                    item[2] = time.time_ns()
                    item[4] = False
                    await item[0].rotate()
            item[3] = False

    @commands.Cog.listener()
    async def on_voice_state_update(self, user, before, after):
        if user == self.bot.user:
            self.logger.debug("Ignoring self")
            return

        if before.channel is None:
            self.logger.info(
                "User %s connected to channel %s", user, after.channel.name
            )
            self.wsink.on_user_connect(user)
        elif after.channel is None:
            self.logger.info(
                "User %s disconnected from channel %s", user, before.channel.name
            )
            operation = CommunicationObject(
                msg_type="user_cleanup", user=user, data=None
            )
            await self.comm_queue.put(operation)
        else:
            self.logger.debug("User VC status changed %s", user.id)
            self.logger.debug("Before %s", before)
            self.logger.debug("After %s", after)

    @commands.command(name="stop_transcribe")
    @commands.has_any_role('Nocna Zmiana', 'Jarl', 'Thane' , 'Bartender')
    async def stop(self, ctx):
        self.check_data.stop()
        stop_token = CommunicationObject("STOP", None, None)

        await self.comm_queue.put(stop_token)
        await ctx.voice_client.disconnect()

    async def transcribe_output_queue(self):
        self.logger.info("Transcript uploader start")
        config = aai.TranscriptionConfig(language_code="pl")
        transcriber = aai.Transcriber()
        self.logger.debug("Transcriber queue id: %s", id(self.comm_queue))

        while True:
            self.logger.debug("waiting for tasks")
            item = await self.comm_queue.get()
            self.logger.debug("Got %s", item)
            if "STOP" in item.type:
                self.logger.debug("Queue ended")
                break
            elif "send_file" in item.type:
                self.logger.debug("Sending file for transcription")
                transcript = None
                for iter in item.data:
                    try:
                        coro = asyncio.to_thread(
                            transcriber.transcribe, iter, config=config
                        )
                        transcript = await coro
                    except Exception as e:
                        self.logger.warn("Exceptiom occured %s", e)
                    await self.mc.send(f"{item.user} : {transcript.text}")
                    if transcript.error:
                        self.logger.error(transcript.error)
                        raise AssertionError
            elif "user_cleanup" in item.type:
                self.logger.info("User %s disconnected - cleanup action")
            else:
                self.logger.warn("Something went wrong with object %s", item)
        self.logger.info("Transcript uploader stoped")


async def setup(bot):
    logger = logging.getLogger("discord")
    await bot.add_cog(Transcriber(bot))
    logger.info("Loading voice transcriber module done")
