"""``FakeGuestAgentBackend`` — in-memory implementation of the OPTIONAL
:class:`~mgf.vm.core.contracts.GuestAgentBackend` (Wave-R6, D-G19).

Modeled on the sibling fakes (``FakeMetricsBackend`` et al., D-F3): a working
in-memory adapter with correct read semantics, not a mock. It knows a set of
domains and, per domain, an "agent reachable" flag plus the
:class:`GuestOsInfo` / :class:`GuestNic` tuple it reports. An unknown domain
raises ``DomainNotFound``; a known-but-unreachable domain makes
``guest_os_info`` / ``guest_interfaces`` raise ``GuestUnreachableError`` and
``guest_ping`` return ``False``.

This backend is OPTIONAL (D-G19): a composition that passes no guest-agent
backend is unaffected. Where one is wanted (the IP-resolution / OS-detect /
``vm agent`` read ports), this fake satisfies ``GuestAgentBackend`` so
downstream suites and the dev loop can exercise the surface without a real
guest.
"""

from __future__ import annotations

import threading
from typing import TYPE_CHECKING

from mgf.vm.core.contracts import GuestNic, GuestOsInfo
from mgf.vm.core.exceptions import DomainNotFound, GuestUnreachableError

if TYPE_CHECKING:
    from collections.abc import Iterable

    from mgf.vm.core.ids import DomainId


class _GuestRecord:
    """Per-domain seeded guest state (mutable; tests poke it via helpers)."""

    __slots__ = ("interfaces", "os_info", "reachable")

    def __init__(self) -> None:
        self.reachable: bool = True
        self.os_info: GuestOsInfo = GuestOsInfo()
        self.interfaces: tuple[GuestNic, ...] = ()


class FakeGuestAgentBackend:
    """In-memory in-guest-agent adapter — D-F3 first-class fake.

    Thread-safe via a single ``RLock`` (consistent with the other fakes'
    threading rules, ``contracts.md`` §8). Constructed with the set of domain
    IDs it "knows about"; with no arguments it knows none (every call raises
    ``DomainNotFound``). Tests register domains and seed their reported state
    via the ``_set_*`` helpers.
    """

    def __init__(self, *, known_domains: Iterable[DomainId] | None = None) -> None:
        self._lock = threading.RLock()
        self._records: dict[DomainId, _GuestRecord] = {
            d: _GuestRecord() for d in (known_domains or ())
        }
        self._closed = False

    @property
    def closed(self) -> bool:
        return self._closed

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

    def close(self) -> None:
        with self._lock:
            self._closed = True

    def __enter__(self) -> FakeGuestAgentBackend:
        return self

    def __exit__(self, *exc: object) -> None:
        self.close()

    # --- the GuestAgentBackend Protocol ----------------------------------

    def guest_ping(self, d: DomainId) -> bool:
        """``True`` iff the (known) domain's agent is reachable; never raises
        for an unreachable agent. Raises ``DomainNotFound`` for an unknown
        domain."""
        with self._lock:
            return self._record(d).reachable

    def guest_os_info(self, d: DomainId) -> GuestOsInfo:
        """Seeded :class:`GuestOsInfo` for a reachable known domain.

        Raises ``GuestUnreachableError`` when the agent is unreachable and
        ``DomainNotFound`` for an unknown domain.
        """
        with self._lock:
            rec = self._record(d)
            if not rec.reachable:
                raise GuestUnreachableError(f"guest agent unreachable on {d}")
            return rec.os_info

    def guest_interfaces(self, d: DomainId) -> tuple[GuestNic, ...]:
        """Seeded guest NICs for a reachable known domain.

        Raises ``GuestUnreachableError`` when the agent is unreachable and
        ``DomainNotFound`` for an unknown domain.
        """
        with self._lock:
            rec = self._record(d)
            if not rec.reachable:
                raise GuestUnreachableError(f"guest agent unreachable on {d}")
            return rec.interfaces

    # --- test helpers (not part of the Protocol) -------------------------

    def _record(self, d: DomainId) -> _GuestRecord:
        rec = self._records.get(d)
        if rec is None:
            raise DomainNotFound(f"no domain {d}")
        return rec

    def _register_domain(self, d: DomainId) -> None:
        """Mark a domain as known (its agent reachable, empty reports)."""
        with self._lock:
            self._records.setdefault(d, _GuestRecord())

    def _set_reachable(self, d: DomainId, reachable: bool) -> None:
        with self._lock:
            self._record(d).reachable = reachable

    def _set_os_info(self, d: DomainId, os_info: GuestOsInfo) -> None:
        with self._lock:
            self._record(d).os_info = os_info

    def _set_interfaces(self, d: DomainId, interfaces: tuple[GuestNic, ...]) -> None:
        with self._lock:
            self._record(d).interfaces = interfaces


__all__ = ["FakeGuestAgentBackend"]
