"""Assemble ``DashboardEntry`` rows for the ``vm list`` surface.

A dashboard entry is a typed triple of :class:`VMState` (always
fresh, event-updated) + optional :class:`VMMetrics` (polled; may
be stale or missing on stopped VMs) + optional
:class:`HostMetrics` (populated once per refresh for the
dashboard header row). Keeping the three pieces separate lets
consumers render each at its own freshness without asking "is
this VM's CPU percent reliable?" for every cell.

The dashboard's only job is to cross-reference the StateStore
with the latest polled metrics; it does **not** fetch metrics
itself. The metrics poller lives in the adapter; until then the
dashboard simply returns entries with ``metrics=None``. Callers
that want real-time numbers can inject a metrics lookup
function via the ``metrics_for`` parameter on :func:`list_vms`.

Purity note: this module is engine-free. :func:`metrics_from_provider`
accepts the narrow structural :class:`MetricsSource` Protocol (one
method, ``domain_metrics``) rather than the adapter-bound
``mgf.vm.providers.base.VMProvider`` — mirroring how
:class:`~mgf.vm.core.monitor.state.StateStore` declares its own
``DomainHandleSource``. Adapters whose backend already exposes
``domain_metrics(handle) -> VMMetrics`` satisfy it structurally with
no import edge into the pure core.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING, Protocol, runtime_checkable

from mgf.vm.core.domain import VMStatus

if TYPE_CHECKING:
    from collections.abc import Callable

    from mgf.vm.core.domain import HostMetrics, VMHandle, VMMetrics, VMState
    from mgf.vm.core.monitor.state import StateStore


@runtime_checkable
class MetricsSource(Protocol):
    """Narrow metrics contract the dashboard needs from a backend.

    Declared structurally so the pure dashboard never imports the
    adapter-bound provider. Any object exposing
    ``domain_metrics(handle) -> VMMetrics`` (libvirt adapter, mock
    provider, test double) satisfies it.
    """

    def domain_metrics(self, handle: VMHandle) -> VMMetrics:
        """Sample the current resource usage for ``handle``."""
        ...


@dataclass(frozen=True)
class DashboardEntry:
    """One row of ``vmanager vm list``.

    ``metrics`` is ``None`` when the VM is stopped or when the
    metrics poller has not yet populated a sample. The CLI
    renders this as ``"-"`` in the CPU / RAM columns.
    """

    state: VMState
    metrics: VMMetrics | None = None
    host_metrics: HostMetrics | None = None


MetricsLookup = "Callable[[VMHandle], VMMetrics | None]"


def list_vms(
    store: StateStore,
    *,
    include_stopped: bool = True,
    metrics_for: Callable[[VMHandle], VMMetrics | None] | None = None,
    host_metrics: HostMetrics | None = None,
) -> list[DashboardEntry]:
    """Return one :class:`DashboardEntry` per tracked VM.

    Args:
        store: the :class:`StateStore` to read from.
        include_stopped: when ``False``, stopped / crashed VMs
            are omitted. Matches the ``vmanager vm list --all``
            flag (which defaults to showing everything).
        metrics_for: optional lookup that returns a
            :class:`VMMetrics` (or ``None``) for a given handle.
            The dashboard calls it once per entry. When omitted
            every entry's ``metrics`` is ``None``.
        host_metrics: populated on every entry so the GUI header
            row can show host CPU / RAM totals without making a
            second pass. ``None`` for text CLI output which only
            uses per-VM rows.

    The returned list is sorted by VM name so repeated calls
    produce stable output.
    """
    entries: list[DashboardEntry] = []

    hidden_states = (
        frozenset()
        if include_stopped
        else frozenset({VMStatus.STOPPED, VMStatus.CRASHED, VMStatus.UNKNOWN})
    )

    for state in sorted(store.list_all(), key=lambda s: s.handle.name):
        if state.status in hidden_states:
            continue
        metrics: VMMetrics | None = None
        if metrics_for is not None:
            metrics = metrics_for(state.handle)
        entries.append(
            DashboardEntry(
                state=state,
                metrics=metrics,
                host_metrics=host_metrics,
            )
        )
    return entries


def entry_for(
    store: StateStore,
    name: str,
    *,
    metrics_for: Callable[[VMHandle], VMMetrics | None] | None = None,
    host_metrics: HostMetrics | None = None,
) -> DashboardEntry | None:
    """Return a single :class:`DashboardEntry` for ``name`` or ``None``.

    Used by ``vmanager vm info <name>`` — the CLI wants exactly
    one row with the same metadata shape as ``vm list``, and
    delegating to the same pathway keeps the two outputs
    consistent.
    """
    state = store.get(name)
    if state is None:
        return None
    metrics: VMMetrics | None = None
    if metrics_for is not None:
        metrics = metrics_for(state.handle)
    return DashboardEntry(
        state=state,
        metrics=metrics,
        host_metrics=host_metrics,
    )


def metrics_from_provider(
    provider: MetricsSource,
) -> Callable[[VMHandle], VMMetrics | None]:
    """Convenience wrapper that turns a metrics source into a lookup.

    Calling the provider for every row is expensive under real
    libvirt (each call goes over the event bus), but cheap fakes
    are fine for tests and the CLI to use this as the default
    lookup. Failures degrade to ``None`` so a transient libvirt
    hiccup does not blow up the whole ``vm list`` output.
    """
    from mgf.vm.core.exceptions import DomainNotFoundError, ProviderError

    def _lookup(handle: VMHandle) -> VMMetrics | None:
        try:
            return provider.domain_metrics(handle)
        except (ProviderError, DomainNotFoundError):
            return None

    return _lookup


__all__ = [
    "DashboardEntry",
    "MetricsLookup",
    "MetricsSource",
    "entry_for",
    "list_vms",
    "metrics_from_provider",
]
