"""``FakeMetricsBackend`` — in-memory implementation of the OPTIONAL
:class:`~mgf.vm.core.contracts.MetricsBackend` (Wave-B, D-G13).

Modeled on the sibling fakes (``FakeDeviceBackend`` et al., D-F3): a working
in-memory adapter with correct read semantics, not a mock. It samples a set
of "known" domains and returns a deterministic zero-valued
:class:`~mgf.vm.core.contracts.MetricsSample`, raising ``DomainNotFound`` for
an unknown domain.

This backend is OPTIONAL (D-G13): a composition that passes no metrics
backend is unaffected. Where one is wanted (a metrics read port), this fake
satisfies ``MetricsBackend`` so downstream suites and the dev loop can
exercise the surface without a real engine.
"""

from __future__ import annotations

import threading
from datetime import UTC, datetime
from typing import TYPE_CHECKING

from mgf.vm.core.contracts import MetricsSample
from mgf.vm.core.exceptions import DomainNotFound

if TYPE_CHECKING:
    from collections.abc import Callable, Iterable

    from mgf.vm.core.ids import DomainId


def _utc_now() -> datetime:
    """Default clock — UTC always (DP-12)."""
    return datetime.now(UTC)


class FakeMetricsBackend:
    """In-memory per-domain metrics 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"; :meth:`sample_metrics` returns a
    deterministic zero sample for a known domain and raises
    ``DomainNotFound`` for any other. With no arguments it knows no domains
    (every sample raises) — tests register domains via :meth:`_register_domain`.

    The clock is injectable so tests can drive ``sampled_at`` deterministically.
    """

    def __init__(
        self,
        *,
        clock: Callable[[], datetime] = _utc_now,
        known_domains: Iterable[DomainId] | None = None,
    ) -> None:
        self._clock = clock
        self._lock = threading.RLock()
        self._known: set[DomainId] = set(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) -> FakeMetricsBackend:
        return self

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

    # --- the MetricsBackend Protocol -------------------------------------

    def sample_metrics(self, d: DomainId) -> MetricsSample:
        """Deterministic zero-valued sample for a known domain.

        Honors the read contract: raises ``DomainNotFound`` for an unknown
        domain, returns a fresh :class:`MetricsSample` (all cumulative
        counters zero) timestamped by the injected ``clock`` so tests stay
        deterministic. Real adapters return live engine stats here.
        """
        with self._lock:
            if d not in self._known:
                msg = f"no domain {d}"
                raise DomainNotFound(msg)
            return MetricsSample(
                cpu_time_ns=0,
                memory_used_bytes=0,
                cpu_percent=None,
                memory_available_bytes=None,
                disks=(),
                nics=(),
                sampled_at=self._clock(),
            )

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

    def _register_domain(self, d: DomainId) -> None:
        """Mark a domain as sampleable (tests inject domains)."""
        with self._lock:
            self._known.add(d)


__all__ = ["FakeMetricsBackend"]
