#!/usr/bin/env python3
"""WebSocket <-> SvxLink UDP audio bridge.

Audio format on both sides: signed little-endian 16-bit, mono, 48 kHz.
SvxLink itself handles the reflector login and talkgroup routing.
"""
import asyncio
import json
import logging
import websockets
from websockets.exceptions import ConnectionClosed

LOG = logging.getLogger("zello-fd-bridge")
WS_HOST = "0.0.0.0"
WS_PORT = 8765
SVX_AUDIO_IN_HOST = "127.0.0.1"
SVX_AUDIO_IN_PORT = 4302
SVX_AUDIO_OUT_PORT = 4301


class Bridge:
    def __init__(self) -> None:
        self.clients = set()
        self.tx_client = None
        self.udp_transport = None

    async def send_json(self, ws, payload) -> None:
        try:
            await ws.send(json.dumps(payload, separators=(",", ":")))
        except Exception:
            pass

    async def broadcast_audio(self, payload: bytes, exclude=None) -> None:
        if not self.clients:
            return
        recipients = [client for client in tuple(self.clients) if client is not exclude]
        if not recipients:
            return
        results = await asyncio.gather(
            *(client.send(payload) for client in recipients),
            return_exceptions=True,
        )
        for client, result in zip(recipients, results):
            if isinstance(result, Exception):
                self.clients.discard(client)

    async def client(self, ws, *_) -> None:
        LOG.info("Web client connected: %s", ws.remote_address)
        self.clients.add(ws)
        try:
            await self.send_json(ws, {"type": "status", "connected": True, "name": "Zello-FD"})
            async for message in ws:
                if isinstance(message, str):
                    try:
                        event = json.loads(message)
                    except json.JSONDecodeError:
                        continue
                    if event.get("type") == "hello":
                        await self.send_json(ws, {"type": "status", "connected": True, "name": "Zello-FD"})
                    elif event.get("type") == "tx":
                        enabled = bool(event.get("enabled"))
                        if enabled:
                            if self.tx_client is not None and self.tx_client is not ws:
                                await self.send_json(self.tx_client, {"type": "tx", "enabled": False})
                            self.tx_client = ws
                        elif self.tx_client is ws:
                            self.tx_client = None
                    continue
                if self.udp_transport is None:
                    continue
                if 320 <= len(message) <= 4096 and len(message) % 2 == 0:
                    self.udp_transport.sendto(message, (SVX_AUDIO_IN_HOST, SVX_AUDIO_IN_PORT))
                    # Audio an andere Clients weiterleiten, den Absender (ws) aber ausschließen (kein Echo)
                    await self.broadcast_audio(message, exclude=ws)
        except ConnectionClosed:
            pass
        finally:
            self.clients.discard(ws)
            if self.tx_client is ws:
                self.tx_client = None
            LOG.info("Web client disconnected: %s", ws.remote_address)


class SvxAudioProtocol(asyncio.DatagramProtocol):
    def __init__(self, bridge: Bridge) -> None:
        self.bridge = bridge

    def connection_made(self, transport) -> None:
        self.bridge.udp_transport = transport
        LOG.info("Listening for SvxLink audio on UDP %d", SVX_AUDIO_OUT_PORT)

    def datagram_received(self, data, _) -> None:
        if len(data) % 2 == 0 and data:
            asyncio.create_task(self.bridge.broadcast_audio(data))

    def error_received(self, exc) -> None:
        LOG.warning("SvxLink UDP error: %s", exc)


class SvxAudioInProtocol(asyncio.DatagramProtocol):
    def __init__(self, bridge: Bridge) -> None:
        self.bridge = bridge

    def connection_made(self, transport) -> None:
        LOG.info("Listening for incoming SvxLink audio on UDP %d", SVX_AUDIO_IN_PORT)

    def datagram_received(self, data, addr) -> None:
        if len(data) % 2 == 0 and data:
            asyncio.create_task(self.bridge.broadcast_audio(data))


async def main() -> None:
    logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
    bridge = Bridge()
    loop = asyncio.get_running_loop()
    
    # Endpunkt für Audio OUT zu SVXLink
    await loop.create_datagram_endpoint(
        lambda: SvxAudioProtocol(bridge),
        local_addr=("127.0.0.1", SVX_AUDIO_OUT_PORT),
    )
    
    # Endpunkt für den Eingang von SVXLink
    await loop.create_datagram_endpoint(
        lambda: SvxAudioInProtocol(bridge),
        local_addr=("127.0.0.1", SVX_AUDIO_IN_PORT),
    )

    async with websockets.serve(bridge.client, WS_HOST, WS_PORT, max_size=65536, ping_interval=20, ping_timeout=60):
        LOG.info("WebSocket audio bridge listening on ws://0.0.0.0:%d", WS_PORT)
        await asyncio.Future()


if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        pass