"""Hub WebSocket: koneksi agent (komputer siswa) dan dashboard (guru/admin)."""
from __future__ import annotations

import asyncio
import json
import logging
from dataclasses import dataclass, field

from fastapi import WebSocket

log = logging.getLogger("cms.ws")


@dataclass
class DashClient:
    ws: WebSocket
    role: str
    uid: int
    name: str
    classroom_ids: set[int] | None  # None = semua (admin)
    screen_computer: int | None = None


@dataclass
class Hub:
    loop: asyncio.AbstractEventLoop | None = None
    agents: dict[int, WebSocket] = field(default_factory=dict)
    dashboards: list[DashClient] = field(default_factory=list)

    # ---- agent --------------------------------------------------------------------
    def is_online(self, computer_id: int) -> bool:
        return computer_id in self.agents

    async def send_agent(self, computer_id: int, msg: dict) -> bool:
        ws = self.agents.get(computer_id)
        if ws is None:
            return False
        try:
            await ws.send_text(json.dumps(msg))
            return True
        except Exception as e:  # koneksi putus
            log.warning("send_agent gagal: %s", e)
            self.agents.pop(computer_id, None)
            return False

    def send_agent_threadsafe(self, computer_id: int, msg: dict) -> bool:
        if self.loop is None or not self.is_online(computer_id):
            return False
        fut = asyncio.run_coroutine_threadsafe(self.send_agent(computer_id, msg), self.loop)
        try:
            return fut.result(timeout=5)
        except Exception:
            return False

    # ---- dashboard ----------------------------------------------------------------
    def _visible(self, d: DashClient, classroom_id: int | None) -> bool:
        if d.classroom_ids is None:
            return True
        return classroom_id is not None and classroom_id in d.classroom_ids

    async def broadcast(self, event: dict, classroom_id: int | None = None, only_admin: bool = False) -> None:
        dead = []
        text = json.dumps(event, default=str)
        for d in list(self.dashboards):
            if only_admin and d.role != "admin":
                continue
            if not self._visible(d, classroom_id):
                continue
            try:
                await d.ws.send_text(text)
            except Exception:
                dead.append(d)
        for d in dead:
            if d in self.dashboards:
                self.dashboards.remove(d)

    def emit(self, event: dict, classroom_id: int | None = None, only_admin: bool = False) -> None:
        """Aman dipanggil dari thread sinkron (endpoint FastAPI biasa)."""
        if self.loop is None:
            return
        asyncio.run_coroutine_threadsafe(self.broadcast(event, classroom_id, only_admin), self.loop)

    async def relay_screen(self, computer_id: int, frame: dict) -> None:
        text = json.dumps({"type": "screen_frame", "computer_id": computer_id, **frame})
        for d in list(self.dashboards):
            if d.screen_computer == computer_id:
                try:
                    await d.ws.send_text(text)
                except Exception:
                    pass

    def viewers(self, computer_id: int) -> list[DashClient]:
        return [d for d in self.dashboards if d.screen_computer == computer_id]


hub = Hub()
