"""A scripted long-poll broker for the monitor-family unit pins.

Since 0.1.11 every monitor shape (`monitor` / `amonitor` / `monitor_fd`)
opens its OWN connection and registers a subscription on it, so a
`Client` built over a fake transport can no longer be monitored at all —
the surface needs a socket path, and a peer at the other end that speaks
hello + subscribe + drain_events.

This is that peer. It is deliberately small and dumb: it hands out
scripted batches in order, then empty ones, and records every request so
a pin can assert what the SDK actually asked for (`wait_ms`, the
subscription id, the predicate). Misbehaviour lives in
`test_sdk10_deadlines._FakeBroker` instead — that one models wedged,
crashed and slow brokers; this one models a WORKING broker so the pins
here stay pointed at iterator discipline rather than failure handling.

The hello shape is the real one (Part 12 §3.3 — a fake that does not
volunteer it on accept deadlocks `Client.connect`; SDK-06 recorded the
cost of learning that the hard way).
"""

from __future__ import annotations

import json
import socket
import tempfile
import threading
import time
from contextlib import suppress
from pathlib import Path
from typing import Any


class FakeEventBroker:
    """Speaks hello, answers `subscribe`, serves `batches` in order.

    Each `drain_events` pops the next batch; once they run out, every
    later drain answers `[]` after `idle_delay_s` (a stand-in for the
    broker's long poll — enough to keep a pump loop from spinning at
    full CPU, short enough that no pin waits on it).
    """

    def __init__(
        self,
        batches: list[list[dict[str, Any]]] | None = None,
        *,
        cap_id: int = 1234567890,
        idle_delay_s: float = 0.02,
        shed_first: int = 0,
    ) -> None:
        self._batches = list(batches or [])
        self._idle_delay_s = idle_delay_s
        # M-B B3 — answer the first `shed_first` drains with the wire's
        # own back-pressure code. Models the per-UID rate limiter
        # (`admission.rlAllow`), which sheds ANY op including a drain
        # whose subscription is perfectly healthy — the case that must
        # not be mistaken for a dead stream.
        self._shed_remaining = shed_first
        self._tmpdir = tempfile.mkdtemp(prefix="mon-fake-")
        self.path = str(Path(self._tmpdir) / "broker.sock")
        hello = {"ok": True, "hello": "mgf-hrb", "cap_id": cap_id}
        self._hello = json.dumps(hello).encode() + b"\n"
        self._sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
        self._sock.bind(self.path)
        self._sock.listen(8)
        self._stop = threading.Event()
        self._lock = threading.Lock()
        self._conns: list[socket.socket] = []
        self.requests: list[dict[str, Any]] = []
        self._accept_thread = threading.Thread(target=self._accept_loop, daemon=True)
        self._accept_thread.start()

    # -- server internals ---------------------------------------------------

    def _accept_loop(self) -> None:
        self._sock.settimeout(0.05)
        while not self._stop.is_set():
            try:
                conn, _addr = self._sock.accept()
            except (TimeoutError, OSError):
                continue
            with self._lock:
                self._conns.append(conn)
            threading.Thread(target=self._serve_conn, args=(conn,), daemon=True).start()

    def _next_batch(self) -> list[dict[str, Any]]:
        with self._lock:
            if self._batches:
                return self._batches.pop(0)
        return []

    def _serve_conn(self, conn: socket.socket) -> None:
        with suppress(OSError):
            conn.sendall(self._hello)
        conn.settimeout(0.05)
        buf = b""
        while not self._stop.is_set():
            try:
                chunk = conn.recv(4096)
            except TimeoutError:
                continue
            except OSError:
                return
            if not chunk:
                return
            buf += chunk
            while b"\n" in buf:
                line, buf = buf.split(b"\n", 1)
                try:
                    req = json.loads(line)
                except ValueError:
                    continue
                with self._lock:
                    self.requests.append(req)
                reply = self._reply_for(req)
                with suppress(OSError):
                    conn.sendall(json.dumps(reply).encode() + b"\n")

    def _reply_for(self, req: dict[str, Any]) -> dict[str, Any]:
        op = req.get("op")
        if op == "subscribe":
            return {"ok": True, "subscription_id": req.get("id", "s"), "queue_max": 64}
        if op == "drain_events":
            with self._lock:
                if self._shed_remaining > 0:
                    self._shed_remaining -= 1
                    return {"ok": False, "err": "Overloaded"}
            events = self._next_batch()
            if not events:
                time.sleep(self._idle_delay_s)
            return {"ok": True, "events": events}
        return {"ok": True}

    # -- introspection ------------------------------------------------------

    def requests_for(self, op: str) -> list[dict[str, Any]]:
        with self._lock:
            return [r for r in self.requests if r.get("op") == op]

    # -- lifecycle ----------------------------------------------------------

    def stop(self) -> None:
        self._stop.set()
        self._accept_thread.join(timeout=2.0)
        with suppress(OSError):
            self._sock.close()
        with self._lock:
            conns = list(self._conns)
        for c in conns:
            with suppress(OSError):
                c.close()
        with suppress(OSError):
            Path(self.path).unlink()
