"""WebSocket endpoint for real-time log streaming.

Clients connect to /ws/logs and receive live log lines from all modules.
"""

from __future__ import annotations

import asyncio
import threading
from collections import deque

from fastapi import APIRouter, WebSocket, WebSocketDisconnect

from Project.Sanad.core.logger import set_ws_push

router = APIRouter()

MAX_WATCHERS = 50

# Ring buffer of recent log lines (shared across connections).
_recent: deque[str] = deque(maxlen=500)
# Each watcher is an (event_loop, queue) pair. We keep the loop so cross-thread
# producers can schedule the enqueue on the consumer's loop (asyncio.Queue is
# NOT thread-safe — calling put_nowait off-loop neither wakes the parked
# `await queue.get()` nor safely mutates the queue's internals).
_watchers: set[tuple[asyncio.AbstractEventLoop, asyncio.Queue]] = set()
_watchers_lock = threading.Lock()


def push_log_line(line: str):
    """Called from the logging system to feed new lines.

    May be called from ANY thread (logging is multi-threaded), so the append
    to _recent and the per-watcher enqueue are done together under the same
    lock that log_ws holds while snapshotting history + registering — that
    closes the history/live overlap window so a connecting client can't see a
    line both in its history replay and again live. The enqueue itself is
    marshalled onto each watcher's loop via call_soon_threadsafe because
    asyncio.Queue.put_nowait is not safe to call from a foreign thread.
    """
    with _watchers_lock:
        _recent.append(line)
        snapshot = list(_watchers)
    for loop, q in snapshot:
        try:
            loop.call_soon_threadsafe(_safe_put, q, line)
        except RuntimeError:
            # Loop already closed — watcher is going away; skip it.
            pass


def _safe_put(q: asyncio.Queue, line: str) -> None:
    """Enqueue on the consumer's own loop thread (so it's safe)."""
    try:
        q.put_nowait(line)
    except asyncio.QueueFull:
        # Drop on overflow rather than block — logs are not critical data
        pass


# Register with the logger so all log records are pushed to WS clients.
# Wrap so a logger registration failure doesn't break Dashboard import.
try:
    set_ws_push(push_log_line)
except Exception:
    pass


@router.websocket("/ws/logs")
async def log_ws(ws: WebSocket):
    await ws.accept()

    loop = asyncio.get_running_loop()
    queue: asyncio.Queue[str] = asyncio.Queue(maxsize=200)
    watcher = (loop, queue)
    with _watchers_lock:
        if len(_watchers) >= MAX_WATCHERS:
            await ws.close(code=1013, reason="Too many log watchers")
            return
        # Register the live queue and snapshot history under the SAME lock that
        # push_log_line holds — so every line is either in this history
        # snapshot or arrives on the queue, never both (no replay duplicates).
        _watchers.add(watcher)
        history = list(_recent)

    try:
        # Send recent history
        for line in history:
            await ws.send_text(line)

        while True:
            line = await queue.get()
            await ws.send_text(line)
    except WebSocketDisconnect:
        pass
    except Exception:
        # Any other error closes the connection cleanly
        try:
            await ws.close()
        except Exception:
            pass
    finally:
        with _watchers_lock:
            _watchers.discard(watcher)
