import asyncio
import json

from microdot import Microdot
from microdot.websocket import WebSocketError, with_websocket

import espnow
import network
from util import format_mac, parse_mac


app = Microdot()
_esp = None
_known_peers = set()
_ws_clients = set()


def _init_espnow():
    global _esp
    sta = network.WLAN(network.STA_IF)
    sta.active(True)
    _esp = espnow.ESPNow()
    _esp.active(True)


def _validate_envelope(obj):
    if obj.get("v") != "1":
        raise ValueError("message.v must be '1'")
    devices = obj["devices"]
    for address in devices.keys():
        parse_mac(address)
    return obj


def _send_espnow(address, payload):
    if _esp is None:
        raise ValueError("espnow is not initialized")
    mac = parse_mac(address)
    msg = json.dumps(payload, separators=(",", ":")).encode("utf-8")
    if mac not in _known_peers:
        _esp.add_peer(mac)
        _known_peers.add(mac)
    _esp.send(mac, msg)
    return mac, len(msg)


async def _broadcast_ws(obj):
    text = json.dumps(obj)
    dead = []
    for client in list(_ws_clients):
        try:
            await client.send(text)
        except Exception:
            dead.append(client)
    for client in dead:
        _ws_clients.discard(client)


async def _espnow_receive_loop():
    while True:
        host, msg = _esp.recv(0)
        if not host:
            await asyncio.sleep(0.01)
            continue
        await _broadcast_ws(
            {
                "from": format_mac(host),
                "payload": msg.decode("utf-8"),
            }
        )


@app.route("/ws")
@with_websocket
async def ws(request, ws):
    _ws_clients.add(ws)
    while True:
        try:
            raw = await ws.receive()
        except WebSocketError:
            break

        if not raw:
            break

        try:
            parsed = json.loads(raw)
            env = _validate_envelope(parsed)
            sent = []
            for address, payload in env["devices"].items():
                mac, payload_size = _send_espnow(address, payload)
                sent.append(
                    {
                        "address": format_mac(mac),
                        "bytes": payload_size,
                    }
                )
        except (ValueError, TypeError) as e:
            await ws.send(json.dumps({"ok": False, "error": str(e)}))
            continue

        await ws.send(
            json.dumps(
                {
                    "ok": True,
                    "sent": sent,
                }
            )
        )
    _ws_clients.discard(ws)


async def main(port=80):
    _init_espnow()
    asyncio.create_task(_espnow_receive_loop())
    await app.start_server(host="0.0.0.0", port=port)


if __name__ == "__main__":
    asyncio.run(main(port=80))
