"""H5b — the process-singleton libvirt event pump (D-G17).

``watch_events`` (contracts.md §3) returns a *live* iterator. libvirt
delivers domain lifecycle events through its own event loop, and that
loop has two hard, process-global constraints:

* ``virEventRegisterDefaultImpl()`` may be registered **once per
  process**, and must run **before** opening any connection that will
  deliver events;
* exactly **one** thread drives ``virEventRunDefaultImpl()`` in a loop.

So a per-:class:`~mgf.vm.libvirt._hypervisor._EventStream` event loop is
impossible. Instead this module owns a single
:class:`LibvirtEventPump` (D-G17): one default-impl loop thread, and
**one** host-wide ``domainEventRegisterAny(None, …LIFECYCLE, …)``
callback **per connection**, fanning every lifecycle event out to all
subscribed streams — each of which applies its own domain filter and
feeds its own pure :class:`~mgf.vm.libvirt._events.EventSequencer`.

The result is the ratified D-G17 shape: **one libvirt callback, N
concurrent consumers** — the monitor ``EventSubscriber`` (host-wide) and
any number of per-VM ``watch_vm`` callers coexist off the single source.

This module is the *only* libvirt-event-loop owner; everything causal
stays in the pure sequencer (``_events.py``). ``libvirt`` is imported
lazily so a host without the binding can still import the module (unit
tests inject a fake connection).
"""

from __future__ import annotations

import threading
import time
from typing import TYPE_CHECKING, Any, Protocol

if TYPE_CHECKING:
    from collections.abc import Callable

    from mgf.vm.core.ids import DomainId

    from mgf.vm.libvirt._connection import LibvirtConnection

    _PushFn = Callable[[DomainId, dict[str, Any]], None]


class EventPump(Protocol):
    """The fan-out surface an ``_EventStream`` depends on (DP-02 seam).

    The production :class:`LibvirtEventPump` satisfies it; unit tests
    inject a fake so they drive ``push`` directly without a real libvirt
    event loop.
    """

    def subscribe(
        self,
        conn: LibvirtConnection,
        domain_filter: DomainId | None,
        push: _PushFn,
    ) -> int: ...

    def unsubscribe(self, sub_id: int) -> None: ...


# --- process-global event-loop impl registration ---------------------------

_impl_lock = threading.Lock()
_impl_registered = False


def ensure_event_impl_registered() -> None:
    """Register libvirt's default event-loop impl, once per process.

    libvirt requires this to run **before** any connection that will
    deliver events is opened, so :func:`mgf.vm.libvirt._connection._open_libvirt`
    calls it before ``libvirt.open``. Idempotent and thread-safe; calling
    it a second time is a no-op (libvirt forbids re-registration).
    """
    global _impl_registered
    with _impl_lock:
        if _impl_registered:
            return
        import libvirt

        libvirt.virEventRegisterDefaultImpl()
        _impl_registered = True


def _run_loop() -> None:
    """The single daemon thread: drive libvirt's default event loop forever.

    ``virEventRunDefaultImpl`` blocks until there is activity, then
    returns; we call it in a loop. A raised error must never kill the
    pump (that would silently stop every stream), so we swallow and back
    off briefly.
    """
    import libvirt

    while True:
        try:
            libvirt.virEventRunDefaultImpl()
        except Exception:
            time.sleep(0.1)


# --- subscriber + per-connection bookkeeping --------------------------------


class _Subscriber:
    """One live stream's fan-out target: its domain filter + push hook."""

    __slots__ = ("domain_filter", "push")

    def __init__(self, domain_filter: DomainId | None, push: _PushFn) -> None:
        self.domain_filter = domain_filter
        self.push = push


class _ConnReg:
    """The single lifecycle callback registered on one connection."""

    __slots__ = ("callback_id", "conn", "sub_ids")

    def __init__(self, conn: LibvirtConnection, callback_id: int) -> None:
        self.conn = conn
        self.callback_id = callback_id
        self.sub_ids: set[int] = set()


class LibvirtEventPump:
    """Process-singleton libvirt lifecycle-event fan-out (D-G17).

    Access via :meth:`instance`. :meth:`subscribe` registers a stream's
    push hook (and, on first use of a connection, the one host-wide
    libvirt callback for it); :meth:`unsubscribe` drops it (and
    deregisters the connection callback when its last subscriber
    leaves). The loop thread starts lazily on the first subscription.
    """

    _instance: LibvirtEventPump | None = None
    _instance_lock = threading.Lock()

    @classmethod
    def instance(cls) -> LibvirtEventPump:
        with cls._instance_lock:
            if cls._instance is None:
                cls._instance = cls()
            return cls._instance

    def __init__(self) -> None:
        self._lock = threading.Lock()
        self._subs: dict[int, _Subscriber] = {}
        self._conns: dict[int, _ConnReg] = {}
        self._next_id = 0
        self._thread: threading.Thread | None = None

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

    def _ensure_loop_thread(self) -> None:
        """Start the single default-impl loop thread (idempotent).

        Caller holds :attr:`_lock`.
        """
        if self._thread is not None:
            return
        ensure_event_impl_registered()
        thread = threading.Thread(target=_run_loop, name="libvirt-event-pump", daemon=True)
        thread.start()
        self._thread = thread

    def subscribe(
        self,
        conn: LibvirtConnection,
        domain_filter: DomainId | None,
        push: _PushFn,
    ) -> int:
        """Register a stream; return an opaque id for :meth:`unsubscribe`.

        On the first subscriber for ``conn`` this registers the one
        host-wide ``domainEventRegisterAny`` lifecycle callback; later
        subscribers on the same connection share it.
        """
        with self._lock:
            self._ensure_loop_thread()
            sub_id = self._next_id
            self._next_id += 1
            self._subs[sub_id] = _Subscriber(domain_filter, push)

            key = id(conn)
            reg = self._conns.get(key)
            if reg is None:
                reg = _ConnReg(conn, self._register_callback(conn, key))
                self._conns[key] = reg
            reg.sub_ids.add(sub_id)
            return sub_id

    def unsubscribe(self, sub_id: int) -> None:
        """Drop a stream; deregister the connection callback if it was last."""
        reg_to_drop: _ConnReg | None = None
        with self._lock:
            self._subs.pop(sub_id, None)
            for key, reg in list(self._conns.items()):
                if sub_id in reg.sub_ids:
                    reg.sub_ids.discard(sub_id)
                    if not reg.sub_ids:
                        reg_to_drop = reg
                        del self._conns[key]
                    break
        if reg_to_drop is not None:
            self._deregister_callback(reg_to_drop)

    # -- libvirt wiring -----------------------------------------------------

    def _register_callback(self, conn: LibvirtConnection, key: int) -> int:
        """Register the one host-wide LIFECYCLE callback on ``conn``.

        Caller holds :attr:`_lock`.
        """
        import libvirt

        def _cb(
            _conn: Any,
            dom: Any,
            event: int,
            detail: int,
            _opaque: Any,
            _key: int = key,
        ) -> None:
            self._dispatch(_key, dom, event, detail)

        with conn.lock:
            raw = conn.raw()
            return int(
                raw.domainEventRegisterAny(None, libvirt.VIR_DOMAIN_EVENT_ID_LIFECYCLE, _cb, None)
            )

    def _deregister_callback(self, reg: _ConnReg) -> None:
        """Best-effort deregistration; a closed/gone connection is fine."""
        try:
            with reg.conn.lock:
                if not reg.conn.closed:
                    reg.conn.raw().domainEventDeregisterAny(reg.callback_id)
        except Exception:  # noqa: S110 - teardown is best-effort (DP-13)
            pass

    def _dispatch(self, key: int, dom: Any, event: int, detail: int) -> None:
        """libvirt callback body: fan one event out to matching subscribers.

        Runs on the event-loop thread. Snapshots the subscriber set under
        :attr:`_lock`, then pushes **outside** the lock (a subscriber's
        ``push`` takes the stream lock — holding :attr:`_lock` across it
        would invert lock order against :meth:`subscribe`). Never raises
        into libvirt's C loop.
        """
        try:
            from mgf.vm.core.ids import DomainId

            uuid = dom.UUIDString()
            domain_id = DomainId(uuid)
            payload: dict[str, Any] = {
                "event": "lifecycle",
                "lifecycle_event": int(event),
                "detail": int(detail),
            }
            with self._lock:
                reg = self._conns.get(key)
                if reg is None:
                    return
                targets = [self._subs[s] for s in reg.sub_ids if s in self._subs]
            for sub in targets:
                if sub.domain_filter is None or sub.domain_filter.value == uuid:
                    sub.push(domain_id, payload)
        except Exception:
            return


__all__ = ["EventPump", "LibvirtEventPump", "ensure_event_impl_registered"]
