"""Server-side routing of audio beats to LED drivers (no browser required)."""

from __future__ import annotations

import asyncio
import json
import os
import threading
from typing import Any, Dict, List, Optional, Set

_route_lock = threading.Lock()
_beat_route: Dict[str, Any] = {
    "enabled": False,
    "device_names": [],
    "wire_preset_id": "2",
    "is_manual": False,
    "pattern": "",
    "manual_beat_n": 1,
}
_beat_counter: int = 0
_main_loop: Optional[asyncio.AbstractEventLoop] = None


def set_beat_route_main_loop(loop: asyncio.AbstractEventLoop) -> None:
    global _main_loop
    _main_loop = loop


def update_beat_route(payload: Dict[str, Any]) -> None:
    """Internal: set or clear routing from explicit fields (tests / future APIs)."""
    global _beat_route, _beat_counter
    if not isinstance(payload, dict):
        return
    with _route_lock:
        if payload.get("enabled") is False:
            _beat_route = {**_beat_route, "enabled": False}
            _beat_counter = 0
            return
        names = payload.get("device_names")
        if not isinstance(names, list):
            names = []
        try:
            n_raw = int(payload.get("manual_beat_n", 1))
        except (TypeError, ValueError):
            n_raw = 1
        manual_n = max(1, min(64, n_raw))
        _beat_route = {
            "enabled": bool(payload.get("enabled", False)),
            "device_names": [str(n).strip() for n in names if str(n).strip()],
            "wire_preset_id": str(payload.get("wire_preset_id") or "2"),
            "is_manual": bool(payload.get("is_manual", False)),
            "pattern": str(payload.get("pattern") or "").strip(),
            "manual_beat_n": manual_n,
        }
        _beat_counter = 0


def get_beat_route() -> Dict[str, Any]:
    with _route_lock:
        return dict(_beat_route)


def _coerce_manual_beat_n(body: Any) -> int:
    """Beats between audio-triggered selects (led-controller only); default 1 = every beat."""
    if not isinstance(body, dict):
        return 1
    raw = body.get("manual_beat_n")
    if raw is None:
        return 1
    try:
        n = int(raw)
    except (TypeError, ValueError):
        return 1
    return max(1, min(64, n))


def _coerce_auto_from_body(body: Any) -> bool:
    """Match JS ``coercePresetAuto`` / ``build_preset_dict`` (default: auto-run)."""
    if not isinstance(body, dict):
        return True
    raw = body.get("auto", body.get("a", True))
    if isinstance(raw, bool):
        return raw
    if raw is None:
        return True
    if isinstance(raw, int):
        return raw != 0
    if isinstance(raw, str):
        lowered = raw.strip().lower()
        if lowered in ("false", "0", "no", "off"):
            return False
        if lowered in ("true", "1", "yes", "on"):
            return True
    return True


def sync_beat_route_from_push_sequence(sequence: List[Any]) -> None:
    """
    Update beat routing from a ``/presets/push`` body ``sequence`` (list of v1 dicts).

    When the batch includes a ``select`` and preset bodies, and the selected preset is
    manual (auto off), enables the route; otherwise disables it.
    """
    merged_presets: Dict[str, Any] = {}
    last_select: Optional[Dict[str, Any]] = None
    for item in sequence:
        if isinstance(item, str):
            try:
                item = json.loads(item)
            except (TypeError, ValueError):
                continue
        if not isinstance(item, dict) or item.get("v") != "1":
            continue
        pr = item.get("presets")
        if isinstance(pr, dict):
            merged_presets.update(pr)
        sel = item.get("select")
        if isinstance(sel, dict) and sel:
            last_select = sel
    if not last_select:
        return

    device_names = [str(k).strip() for k in last_select.keys() if str(k).strip()]
    if not device_names:
        update_beat_route({"enabled": False})
        return

    wire_ids: Set[str] = set()
    for name in device_names:
        val = last_select.get(name)
        if isinstance(val, list) and val:
            wire_ids.add(str(val[0]).strip())
        elif val is not None:
            wire_ids.add(str(val).strip())
    if len(wire_ids) != 1:
        update_beat_route({"enabled": False})
        return
    wire_preset_id = wire_ids.pop()
    preset_body = merged_presets.get(wire_preset_id)
    if preset_body is None:
        for k, v in merged_presets.items():
            if str(k).strip() == wire_preset_id:
                preset_body = v
                break
    if not isinstance(preset_body, dict):
        update_beat_route({"enabled": False})
        return

    if _coerce_auto_from_body(preset_body):
        update_beat_route({"enabled": False})
        return

    pattern = str(preset_body.get("pattern") or preset_body.get("p") or "").strip()
    if pattern and not _pattern_supports_manual(pattern):
        update_beat_route({"enabled": False})
        return

    update_beat_route(
        {
            "enabled": True,
            "device_names": device_names,
            "wire_preset_id": wire_preset_id,
            "is_manual": True,
            "pattern": pattern,
            "manual_beat_n": _coerce_manual_beat_n(preset_body),
        }
    )


def _pattern_supports_manual(pattern_key: str) -> bool:
    if not pattern_key:
        return True
    try:
        here = os.path.dirname(os.path.abspath(__file__))
        root = os.path.abspath(os.path.join(here, "..", ".."))
        path = os.path.join(root, "db", "pattern.json")
        with open(path, "r", encoding="utf-8") as f:
            data = json.load(f)
        meta = data.get(pattern_key)
        if meta is None:
            meta = data.get(pattern_key.lower())
        if not isinstance(meta, dict):
            return True
        return meta.get("supports_manual") is not False
    except OSError:
        return True


def _macs_for_registry_names(device_names: List[str]) -> List[str]:
    from models.device import Device

    want = {str(n).strip() for n in device_names if str(n).strip()}
    if not want:
        return []
    devices = Device()
    macs: List[str] = []
    seen = set()
    for did in devices.list():
        doc = devices.read(did) or {}
        nm = str(doc.get("name") or "").strip()
        if nm not in want:
            continue
        key = str(did).strip().lower().replace(":", "").replace("-", "")
        if len(key) == 12 and key not in seen:
            seen.add(key)
            macs.append(key)
    return macs


async def _deliver_select(device_names: List[str], wire_preset_id: str) -> None:
    from models.device import Device
    from models.transport import get_current_sender
    from util.driver_delivery import deliver_json_messages

    sender = get_current_sender()
    if not sender:
        return
    select = {str(n).strip(): [wire_preset_id] for n in device_names if str(n).strip()}
    if not select:
        return
    msg = json.dumps({"v": "1", "select": select}, separators=(",", ":"))
    macs = _macs_for_registry_names(list(select.keys()))
    if not macs:
        return
    devices = Device()
    try:
        await deliver_json_messages(sender, [msg], macs, devices, delay_s=0.05)
    except Exception as e:
        print(f"[beat-route] deliver failed: {e}")


def notify_beat_detected() -> None:
    """Invoked from the audio thread when a beat is detected."""
    global _beat_counter
    with _route_lock:
        r = dict(_beat_route)
        if not r.get("enabled"):
            return
        if not r.get("is_manual"):
            return
        pattern = r.get("pattern") or ""
        if pattern and not _pattern_supports_manual(pattern):
            return
        names = r.get("device_names") or []
        if not names:
            return
        try:
            n = int(r.get("manual_beat_n") or 1)
        except (TypeError, ValueError):
            n = 1
        n = max(1, min(64, n))
        _beat_counter += 1
        if ((_beat_counter - 1) % n) != 0:
            return
        preset_id = str(r.get("wire_preset_id") or "2")
        names_copy = list(names)
    loop = _main_loop
    if loop is None:
        return
    try:
        asyncio.run_coroutine_threadsafe(_deliver_select(names_copy, preset_id), loop)
    except Exception as e:
        print(f"[beat-route] schedule failed: {e}")
