"""F1 — thread-safe libvirt connection wrapper.

The ``libvirt-python`` binding's per-connection object (``virConnect``)
is **not** safe for concurrent use from multiple threads (contracts.md
§8). This module wraps a single shared ``virConnect`` behind an
:class:`threading.RLock` and serializes every native call through it.

Design decisions baked in here (see
``vm-vmanager-docs/component-specs/contracts.md``):

* **RLock-guarded shared connection** (§8). One connection per adapter
  instance; the lock is re-entrant so a guarded method may call another
  guarded method without deadlocking. Callers run every native libvirt
  call inside :meth:`LibvirtConnection.call`.
* **Deterministic idempotent close** (§7, DP-13). :meth:`close` releases
  the connection exactly once, is safe to call twice, and emits **no**
  ``ResourceWarning`` on ``__del__`` once it has been called. A
  connection that is garbage-collected *without* ``close`` DOES emit a
  ``ResourceWarning`` so leaks are detectable under Python 3.13's
  stricter unraisable handling (contracts.md §3 lifecycle invariants).
* **Context manager** — ``with LibvirtConnection.open(uri) as conn:``
  closes deterministically on exit.

Connection-state failures map to the core taxonomy:
``HypervisorConnectionError`` (cannot reach / lost the engine) and
``HypervisorTransientError`` (retryable engine hiccup). The richer
``libvirtError``-code translation lives in :mod:`mgf.vm.libvirt._errors`;
this module only owns the connection *lifecycle* errors, because those
arise before/around the seam-decorated method bodies.

This is **net-new** — the legacy ``LibvirtProvider`` opened a connection
at construction and never closed it (DP-13 gap closed here).
"""

from __future__ import annotations

import threading
import warnings
from typing import TYPE_CHECKING, Any, TypeVar

from mgf.vm.core.exceptions import (
    HypervisorConnectionError,
    HypervisorTransientError,
)

if TYPE_CHECKING:
    from collections.abc import Callable
    from types import TracebackType

T = TypeVar("T")


def _open_libvirt(uri: str) -> Any:
    """Open a libvirt connection, translating open-time failures.

    Imported lazily so this module can be imported on a host without
    the ``libvirt`` binding (unit tests never reach this function;
    they inject a fake connection via :meth:`LibvirtConnection.adopt`).
    """
    try:
        import libvirt
    except ImportError as exc:  # pragma: no cover - env-specific
        raise HypervisorConnectionError(
            "libvirt-python is not installed; install the system binding "
            "(python3-libvirt) matched to the running libvirtd"
        ) from exc

    # D-G17: libvirt's default event-loop impl must be registered BEFORE
    # opening any connection that will deliver domain events. Registering
    # it here (once per process, guarded) makes ``watch_events`` reliable
    # without coupling every caller to the event pump; the pump's loop
    # thread only starts when something actually subscribes.
    from mgf.vm.libvirt._event_pump import ensure_event_impl_registered

    ensure_event_impl_registered()

    try:
        conn = libvirt.open(uri)
    except libvirt.libvirtError as exc:
        raise HypervisorConnectionError(
            f"could not open libvirt connection to {uri!r}: {exc}"
        ) from exc
    if conn is None:  # pragma: no cover - libvirt.open rarely returns None
        raise HypervisorConnectionError(f"libvirt.open({uri!r}) returned None")
    return conn


class LibvirtConnection:
    """An RLock-guarded, deterministically-closable libvirt connection.

    Construct via :meth:`open` (real libvirt) or :meth:`adopt` (tests,
    or callers that already hold a ``virConnect``). Every native call
    goes through :meth:`call`, which holds the lock for the duration of
    the call so concurrent threads never touch the thread-unsafe
    ``virConnect`` simultaneously.
    """

    __slots__ = ("__weakref__", "_closed", "_conn", "_lock", "_uri")

    def __init__(self, conn: Any, *, uri: str) -> None:
        self._conn: Any = conn
        self._uri = uri
        self._lock = threading.RLock()
        self._closed = False

    # -- construction -------------------------------------------------------

    @classmethod
    def open(cls, uri: str) -> LibvirtConnection:
        """Open a real libvirt connection to ``uri``."""
        return cls(_open_libvirt(uri), uri=uri)

    @classmethod
    def adopt(cls, conn: Any, *, uri: str = "test:///adopted") -> LibvirtConnection:
        """Wrap an already-open ``virConnect`` (or a test double)."""
        return cls(conn, uri=uri)

    # -- properties ---------------------------------------------------------

    @property
    def uri(self) -> str:
        """The connection URI (for diagnostics / capabilities)."""
        return self._uri

    @property
    def closed(self) -> bool:
        """Whether :meth:`close` has run."""
        return self._closed

    @property
    def lock(self) -> threading.RLock:
        """The re-entrant lock guarding the connection.

        Exposed so a backend can hold the lock across a multi-call
        sequence (e.g. lookup-then-mutate) without releasing it
        between the two native calls.
        """
        return self._lock

    # -- the guarded-call primitive ----------------------------------------

    def call(self, fn: Callable[..., T], *args: Any, **kwargs: Any) -> T:
        """Run ``fn(*args, **kwargs)`` under the connection lock.

        The single chokepoint every backend method uses for native
        libvirt work. Raises :class:`HypervisorConnectionError` if the
        connection has already been closed — calling into a released
        ``virConnect`` is a programmer error, surfaced as a typed one.
        """
        with self._lock:
            if self._closed:
                raise HypervisorConnectionError(f"libvirt connection to {self._uri!r} is closed")
            return fn(*args, **kwargs)

    def raw(self) -> Any:
        """Return the underlying ``virConnect`` (caller MUST hold :attr:`lock`).

        Escape hatch for the rare native object (a ``virDomain``) that
        must be looked up and reused under the same lock hold. Raises if
        closed.
        """
        if self._closed:
            raise HypervisorConnectionError(f"libvirt connection to {self._uri!r} is closed")
        return self._conn

    # -- deterministic, idempotent close (DP-13) ----------------------------

    def close(self) -> None:
        """Release the connection. Idempotent; safe to call twice.

        The first call closes the underlying ``virConnect`` and marks
        this wrapper closed. Subsequent calls are no-ops. A failure to
        close the native handle is downgraded to a
        :class:`HypervisorTransientError` *after* the wrapper is marked
        closed, so a flaky close never leaves the wrapper in a
        half-open state (and never re-raises on the second call).
        """
        with self._lock:
            if self._closed:
                return
            # Mark closed FIRST so that even if the native close raises,
            # the wrapper is durably closed and ``__del__`` stays quiet.
            self._closed = True
            conn, self._conn = self._conn, None
            if conn is None:
                return
            close_fn = getattr(conn, "close", None)
            if close_fn is None:
                return
            try:
                close_fn()
            except Exception as exc:
                raise HypervisorTransientError(
                    f"error closing libvirt connection to {self._uri!r}: {exc}"
                ) from exc

    # -- context manager ----------------------------------------------------

    def __enter__(self) -> LibvirtConnection:
        return self

    def __exit__(
        self,
        exc_type: type[BaseException] | None,
        exc: BaseException | None,
        tb: TracebackType | None,
    ) -> None:
        self.close()

    # -- leak detection -----------------------------------------------------

    def __del__(self) -> None:
        # DP-13: a connection dropped without close() is a leak. Surface
        # it as a ResourceWarning (Python 3.13 unraisable handling) but
        # never raise from __del__. If close() already ran, stay silent.
        if not self._closed:
            warnings.warn(
                f"LibvirtConnection to {self._uri!r} was not closed; "
                "call close() or use it as a context manager (DP-13)",
                ResourceWarning,
                stacklevel=2,
            )


__all__ = ["LibvirtConnection"]
