"""``StateStore`` — the in-memory registry of every VM VManager knows.

A single instance lives for the lifetime of a CLI invocation or GUI
session. It is seeded cold from a domain-handle source at startup, then
kept live by the adapter-bound event slice (libvirt domain events) which
writes back through :meth:`update`, :meth:`remove`, and :meth:`insert`.

Thread-safety
-------------

``StateStore`` is written by engine event callbacks on a background
thread owned by the adapter and read by the CLI / GUI main thread. Every
public method acquires a single :class:`threading.RLock` and readers
receive either a shallow copy or a frozen snapshot so they can iterate
without holding the lock for the duration of their work. This rule is
enforced by ``tests/unit/monitor/test_state.py``.

GUI consumers do **not** read the store directly — they react to signals
emitted by the controller layer, which batches updates on the main
thread.

This is the pure-domain slice (Wave 1). The event subscription,
metrics, history, dashboard, host and alert slices remain
engine/adapter-bound and are *not* part of the pure core.
"""

from __future__ import annotations

import threading
from dataclasses import replace
from datetime import UTC, datetime
from typing import TYPE_CHECKING, Protocol, runtime_checkable

from mgf.vm.core.domain import VMHandle, VMState, VMStatus

if TYPE_CHECKING:
    from collections.abc import Iterator


@runtime_checkable
class DomainHandleSource(Protocol):
    """Minimal seeding contract: anything that can list resolved handles.

    The legacy umbrella seeded :class:`StateStore` from
    ``mgf.vm.providers.base.VMProvider.list_domains`` (returning
    :class:`VMHandle` objects). In the distributed core the adapter
    contract is :class:`mgf.vm.core.contracts.HypervisorBackend`, whose
    ``list_domains`` returns the leaner :class:`~mgf.vm.core.contracts.DomainInfo`
    (no ``uuid`` / ``VMStatus``). Rather than mistype :meth:`seed`
    against that incompatible shape — or pull the adapter Protocol into
    this pure module — the store declares exactly the narrow structural
    contract it needs. Adapters supply a thin handle source that
    resolves :class:`VMHandle` instances.
    """

    def list_domains(self) -> list[VMHandle]:
        """Return every currently-defined domain as a resolved handle."""
        ...


class StateStore:
    """In-memory ``{vm_name: VMState}`` registry with cold-start seeding.

    The store owns the canonical answer to "which VMs exist right now
    and what is their state?". Callers that need to flip a power state
    themselves use :meth:`update`; the normal path is for the
    adapter-bound event slice to call it after an engine event fires.
    """

    def __init__(self) -> None:
        self._lock = threading.RLock()
        self._entries: dict[str, VMState] = {}
        self._seeded_at: datetime | None = None

    # ------------------------------------------------------------------
    # Seeding
    # ------------------------------------------------------------------

    def seed(self, source: DomainHandleSource) -> int:
        """Populate the store from ``source.list_domains()``.

        Called once at startup. Subsequent calls are allowed (they
        refresh the entire set) but cost a full round-trip, so callers
        typically only do it when the event subscription has to be
        reattached after an engine disconnect.

        Returns the number of domains loaded.
        """
        handles = source.list_domains()
        now = datetime.now(UTC)
        with self._lock:
            self._entries = {
                handle.name: VMState(
                    handle=handle,
                    status=handle.state,
                    last_state_change=now,
                )
                for handle in handles
            }
            self._seeded_at = now
        return len(handles)

    @property
    def seeded_at(self) -> datetime | None:
        """When :meth:`seed` last completed, or ``None`` before first seed.

        Exposed so ``vmanager vm list`` can warn ("VM list has not been
        seeded yet — run `vmanager vm list --refresh`") on a store that
        never received its cold-start data.
        """
        return self._seeded_at

    # ------------------------------------------------------------------
    # Reads
    # ------------------------------------------------------------------

    def get(self, name: str) -> VMState | None:
        """Return the :class:`VMState` for ``name`` or ``None``."""
        with self._lock:
            state = self._entries.get(name)
            return _freeze(state) if state is not None else None

    def list_all(self) -> list[VMState]:
        """Return every tracked :class:`VMState` as a list of frozen copies.

        The returned list is a snapshot at call time. Callers can iterate
        as long as they like without holding the lock; subsequent mutations
        on the store do not affect the returned instances.
        """
        with self._lock:
            return [_freeze(state) for state in self._entries.values()]

    def contains(self, name: str) -> bool:
        """Fast membership test — avoids copying the state on the hot path."""
        with self._lock:
            return name in self._entries

    def __len__(self) -> int:
        with self._lock:
            return len(self._entries)

    def __iter__(self) -> Iterator[VMState]:
        """Iterate over a frozen snapshot."""
        return iter(self.list_all())

    # ------------------------------------------------------------------
    # Writes — called by the event slice and directly by lifecycle ops
    # ------------------------------------------------------------------

    def insert(self, handle: VMHandle) -> VMState:
        """Add a new entry for a freshly-defined domain.

        Fires when the engine emits a "domain defined" event for a VM
        the store did not already know about. A duplicate insert is
        treated as an update — the new handle's state wins.
        """
        now = datetime.now(UTC)
        with self._lock:
            existing = self._entries.get(handle.name)
            if existing is not None:
                previous_status = existing.status
            else:
                previous_status = None
            entry = VMState(
                handle=handle,
                status=handle.state,
                ip=existing.ip if existing is not None else None,
                uptime_seconds=None,
                last_state_change=now,
                previous_status=previous_status,
            )
            self._entries[handle.name] = entry
            return _freeze(entry)

    def update(
        self,
        name: str,
        *,
        status: VMStatus | None = None,
        ip: str | None = None,
        uptime_seconds: int | None = None,
        handle: VMHandle | None = None,
    ) -> VMState | None:
        """Mutate a tracked entry in place.

        Only fields the caller supplies are modified. Returns the new
        frozen :class:`VMState` or ``None`` when ``name`` is not tracked
        (the caller can fall back to :meth:`insert`).
        """
        now = datetime.now(UTC)
        with self._lock:
            existing = self._entries.get(name)
            if existing is None:
                return None

            new_status = status if status is not None else existing.status
            new_handle = handle if handle is not None else existing.handle
            state_changed = new_status is not existing.status

            entry = replace(
                existing,
                handle=new_handle,
                status=new_status,
                ip=ip if ip is not None else existing.ip,
                uptime_seconds=(
                    uptime_seconds if uptime_seconds is not None else existing.uptime_seconds
                ),
                last_state_change=(now if state_changed else existing.last_state_change),
                previous_status=(existing.status if state_changed else existing.previous_status),
            )
            self._entries[name] = entry
            return _freeze(entry)

    def remove(self, name: str) -> bool:
        """Drop an entry. Returns ``True`` if something was actually removed."""
        with self._lock:
            return self._entries.pop(name, None) is not None

    def rename(self, old_name: str, new_name: str) -> VMState | None:
        """Re-key an entry after a domain rename.

        Returns the frozen :class:`VMState` under the new name, or
        ``None`` when ``old_name`` is not tracked.
        """
        with self._lock:
            existing = self._entries.pop(old_name, None)
            if existing is None:
                return None
            new_handle = VMHandle(
                name=new_name,
                uuid=existing.handle.uuid,
                state=existing.handle.state,
            )
            entry = replace(existing, handle=new_handle)
            self._entries[new_name] = entry
            return _freeze(entry)

    def clear(self) -> None:
        """Empty the store. Tests and reconnect flows use this."""
        with self._lock:
            self._entries.clear()
            self._seeded_at = None


# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------


def _freeze(state: VMState) -> VMState:
    """Return a shallow copy of ``state`` so callers can iterate safely.

    :class:`VMState` is a mutable dataclass, but after this copy the
    returned object shares no references with the store's internal
    dict — a caller cannot accidentally mutate the store by writing to
    the result.
    """
    return replace(state)


__all__ = ["DomainHandleSource", "StateStore"]
