import logging
from typing import cast, Any, AsyncGenerator, Coroutine, Dict, Optional, Tuple, List
import bumble
import bumble.device
import bumble.transport
import bumble.utils

import asyncio
import aioconsole
from auracast import multicast
from auracast import auracast_config

class Multicaster:
    """
    A class responsible for managing the multicasting and audio streaming process.

    It provides methods to initialize and shutdown the broadcasting, as well as start and stop the streaming.
    The class also manages the underlying device and advertising sets.
    """
    def __init__(
            self,
            global_conf: auracast_config.AuracastGlobalConfig,
            big_conf: List[auracast_config.AuracastBigConfig]
            ):
        self.is_auracast_init = False
        self.is_audio_init = False
        self.streaming = False
        self.global_conf = global_conf
        self.big_conf = big_conf
        self.device = None
        self.bigs = None
        self.streamer = None

    def get_status(self):
        streaming = self.streamer.is_streaming if self.streamer is not None else False
        return {
            'is_initialized': self.is_auracast_init,
            'is_streaming': streaming,
        }

    def get_audio_levels(self) -> list[float]:
        """Return current RMS audio levels (0.0-1.0) for each BIG."""
        if self.streamer is not None and self.streamer.is_streaming:
            return self.streamer.get_audio_levels()
        return []
    
    async def init_broadcast(self):
        self.device_acm = multicast.create_device(self.global_conf)

        agen = self.device_acm.__aenter__()  # Manually triggering setup
        device = await agen

        self.bigs = await multicast.init_broadcast( # the bigs dictionary contains all the global configurations
            device,
            self.global_conf,
            self.big_conf
        )
        self.device = device
        self.is_auracast_init =  True

    async def start_streaming(self):
        """Start streaming; if an old stream is running, stop it first to release audio devices."""
        if self.streamer is not None:
            await self.stop_streaming()
            # Brief pause to ensure ALSA/PortAudio fully releases the input device
            await asyncio.sleep(0.5)
        self.streamer = multicast.Streamer(self.bigs, self.global_conf, self.big_conf)
        self.streamer.start_streaming()


    async def stop_streaming(self):
        if self.streamer is not None:
            await self.streamer.stop_streaming()
            self.streamer = None

    async def reset(self):
        await self.shutdown()  # Manually triggering teardown
        self.__init__(self.global_conf, self.big_conf)

    async def shutdown(self):
        # Ensure streaming is fully stopped before tearing down Bluetooth resources
        if self.streamer is not None:
            await self.stop_streaming()

        self.is_auracast_init = False
        self.is_audio_init = False
        
        if self.bigs:
            for big in self.bigs.values():
                if big.get('audio_input'):
                    if hasattr(big['audio_input'], 'aclose'):
                        await big['audio_input'].aclose()

        if self.device:
            await self.device.stop_advertising()
            if self.bigs:
                for big in self.bigs.values():
                    if big.get('advertising_set'):
                        await big['advertising_set'].stop()
            # Explicitly power off the device to ensure a clean state before closing the transport
            await self.device.power_off()
            await self.device_acm.__aexit__(None, None, None)  # Manually triggering teardown


# example commandline ui
async def command_line_ui(caster: Multicaster):
    while True:
        command = await aioconsole.ainput("\nCommands: [start_audio|stop_audio|stop|init|init_audio|quit] > ")
        
        if command.strip().lower() == "start_audio":
            caster.start_streaming()
            print("Audio started!")

        elif command.strip().lower() == "stop_audio":
            caster.stop_streaming()
            print("Audio stopped!")

        elif command.strip().lower() == "stop":
            print("👋 Stopping...")
            caster.stop_streaming()
            await caster.reset()

        elif command.strip().lower() == "init":
            await caster.reset()
            await caster.init_broadcast()
            await caster.init_audio() 
        elif command.strip().lower() == "init_audio":
            await caster.init_audio() 

        elif command.strip().lower() == "quit":
            print("👋 Exiting...")
            if caster.device:
                caster.stop_streaming()
                await caster.shutdown()
            break  # Exit loop
        else:
            print("Invalid command.")

async def main():
    import os
    
    logging.basicConfig(
        level=os.environ.get('LOG_LEVEL', logging.DEBUG),
        format='%(module)s.py:%(lineno)d %(levelname)s: %(message)s'
    )
    
    # Enable debug logging for bumble
    # logging.getLogger('bumble').setLevel(logging.DEBUG)
    
    os.chdir(os.path.dirname(__file__))

    global_conf = auracast_config.AuracastGlobalConfig(
        qos_config=auracast_config.AuracastQosRobust()
    )
    #global_conf.transport='serial:/dev/serial/by-id/usb-SEGGER_J-Link_001057705357-if02,1000000,rtscts' # transport for nrf54l15dk
    global_conf.transport='serial:/dev/serial/by-id/usb-ZEPHYR_Zephyr_HCI_UART_sample_81BD14B8D71B5662-if00,115200,rtscts' #nrf52dongle hci_uart usb cdc

    big_conf = [
        auracast_config.AuracastBigConfigDeu(),
        auracast_config.AuracastBigConfigEng(),
        auracast_config.AuracastBigConfigFra(),
        #auracast_config.broadcast_es,
        #auracast_config.broadcast_it,
        ]
    for conf in big_conf:
        conf.loop = False

    # look into:
    #async with MyAPI() as api:
        #pass

    caster = Multicaster(global_conf, big_conf)
    await caster.init_broadcast()

    await command_line_ui(caster)

if __name__ == '__main__':
    # Run the application
    asyncio.run(main())