"""Control plane between the dashboard (Python) and the Discord bot (Node).

The bot only ever makes *outbound* HTTP (it already POSTs voice turns), so we
keep that single direction:

* the bot PUSHES its live state here (identity, joinable guilds + voice
  channels, current channel, members, whitelist/blacklist) via
  ``POST /api/bot/report`` → :meth:`report`;
* the bot POLLS ``GET /api/bot/commands`` → :meth:`drain` for pending commands
  (join a channel, leave) that the dashboard UI enqueued via :meth:`enqueue`.

Thread-safe: the HTTP handler threads read/write from several threads.
"""

from __future__ import annotations

import threading
import time
from collections import deque
from typing import Any

# The bot is considered offline if it hasn't reported within this window.
STALE_AFTER_S = 8.0


class BotControl:
    def __init__(self) -> None:
        self._lock = threading.Lock()
        self._state: dict[str, Any] = {"connected": False, "ts": 0.0}
        self._commands: deque[dict[str, Any]] = deque()
        self._cmd_id = 0
        # Per-guild listen filter. Empty whitelist => listen to everyone;
        # blacklist always excludes. Users and roles both supported.
        self._lists: dict[str, dict[str, Any]] = {}

    # -- whitelist / blacklist (per guild) ------------------------------- #
    @staticmethod
    def _empty_lists() -> dict[str, Any]:
        return {"whitelistUsers": [], "blacklistUsers": [],
                "whitelistRoles": [], "blacklistRoles": []}

    def get_lists(self, guild_id: str) -> dict[str, Any]:
        with self._lock:
            return dict(self._lists.get(guild_id) or self._empty_lists())

    def all_lists(self) -> dict[str, Any]:
        with self._lock:
            return {g: dict(v) for g, v in self._lists.items()}

    def set_lists(self, guild_id: str, lists: dict[str, Any]) -> dict[str, Any]:
        clean = self._empty_lists()
        for key in clean:
            items = lists.get(key) or []
            # Normalise to [{id, name}] and drop anything without an id.
            clean[key] = [
                {"id": str(it["id"]), "name": str(it.get("name", it["id"]))}
                for it in items if isinstance(it, dict) and it.get("id")
            ]
        with self._lock:
            self._lists[guild_id] = clean
        return clean

    # -- bot -> dashboard (state push) ----------------------------------- #
    def report(self, state: dict[str, Any]) -> None:
        s = dict(state)
        s["ts"] = time.time()
        s["connected"] = True
        with self._lock:
            self._state = s

    def state(self) -> dict[str, Any]:
        with self._lock:
            s = dict(self._state)
        # A report older than STALE_AFTER_S means the bot stopped polling/pushing.
        if s.get("connected") and (time.time() - s.get("ts", 0.0)) > STALE_AFTER_S:
            s["connected"] = False
        return s

    # -- dashboard -> bot (command queue) -------------------------------- #
    def enqueue(self, cmd: dict[str, Any]) -> int:
        with self._lock:
            self._cmd_id += 1
            cmd = {**cmd, "id": self._cmd_id}
            self._commands.append(cmd)
            return self._cmd_id

    def drain(self) -> list[dict[str, Any]]:
        with self._lock:
            cmds = list(self._commands)
            self._commands.clear()
            return cmds
