import asyncio
import time
import audioop

class UdpClientProtocol(asyncio.DatagramProtocol):
    def __init__(self):
        self.transport = None
        self.future = None

    def connection_made(self, transport):
        self.transport = transport

    def datagram_received(self, data, addr):
        if self.future and not self.future.done():
            self.future.set_result(data)

class VocoderEngine:
    def __init__(self, mode="emulator", host="127.0.0.1"):
        self.host = host
        self.ports = list(range(2470, 2480))
        self.pool = {}         # porta -> (transport, protocol, lock)
        self.stream_alloc = {} # stream_id -> porta
        self.last_seen = {}    # stream_id -> timestamp ultimo pacchetto

    async def avvia(self):
        loop = asyncio.get_running_loop()
        for p in self.ports:
            transport, protocol = await loop.create_datagram_endpoint(
                lambda: UdpClientProtocol(),
                remote_addr=(self.host, p)
            )
            # Aggiungiamo un Lock esclusivo per ogni porta UDP
            self.pool[p] = (transport, protocol, asyncio.Lock())
        print(f"[Vocoder] Pool UDP Multi-Porta Sincronizzato: {self.ports}")

    def pulisci_stream_inattivi(self):
        """Libera automaticamente le porte dei Talkgroup/SIP muti da più di 2.5 secondi"""
        ora = time.time()
        inattivi = [s for s, t in self.last_seen.items() if ora - t > 2.5]
        for s in inattivi:
            if s in self.stream_alloc:
                del self.stream_alloc[s]
            del self.last_seen[s]

    def assegna_porta(self, stream_id):
        self.pulisci_stream_inattivi()
        self.last_seen[stream_id] = time.time()

        if stream_id in self.stream_alloc:
            return self.stream_alloc[stream_id]

        porte_usate = set(self.stream_alloc.values())
        porte_libere = [p for p in self.ports if p not in porte_usate]

        if porte_libere:
            porta_scelta = porte_libere[0]
        else:
            # Se tutte le 3 porte sono saturate contemporaneamente, usiamo la prima come fallback
            porta_scelta = self.ports[0]

        self.stream_alloc[stream_id] = porta_scelta
        return porta_scelta

    def rilascia_porta(self, stream_id):
        if stream_id in self.stream_alloc:
            del self.stream_alloc[stream_id]
        if stream_id in self.last_seen:
            del self.last_seen[stream_id]

    async def _scambia_dati(self, porta, dati):
        transport, protocol, lock = self.pool[porta]
        
        # Garantisce che un solo pacchetto alla volta usi questa porta UDP!
        async with lock:
            loop = asyncio.get_running_loop()
            protocol.future = loop.create_future()
            transport.sendto(dati)
            try:
                return await asyncio.wait_for(protocol.future, timeout=0.08)
            except asyncio.TimeoutError:
                return b""
            finally:
                protocol.future = None

    async def converti_ambe_in_pcm(self, stream_id, ambe_bytes):
        porta = self.assegna_porta(stream_id)
        # Invio dati RAW
        return await self._scambia_dati(porta, ambe_bytes)

    async def converti_rtp_in_ambe(self, stream_id, pcm_bytes):
        porta = self.assegna_porta(stream_id)
        
        # Se riceviamo 160 byte dal SIP, li convertiamo in PCM lineare 16-bit (320 byte)
        if len(pcm_bytes) == 160:
            # Uso alaw2lin per PCMA (A-Law, standard europeo).
            # (Se il tuo centralino usa u-Law, ti basterà cambiare in ulaw2lin)
            pcm_bytes = audioop.alaw2lin(pcm_bytes, 2)
            
        return await self._scambia_dati(porta, pcm_bytes)
