"""``InProcessTransport`` — the in-process kernel (VManager Local; D-A2, B1).

The concrete :class:`~mgf.vm.sdk.transport.base.Transport` that wires the
:class:`~mgf.vm.sdk.client.VmManagerClient` straight to in-process
``mgf.vm.core`` adapters — no daemons, no network. It is the Phase-0 engine
the CLI and GUI run on when they manage the local host directly.

Anatomy
-------

* **Backends are constructor-injected** via :meth:`from_adapters`: a
  :class:`~mgf.vm.core.contracts.HypervisorBackend` (required) plus optional
  :class:`~mgf.vm.core.contracts.StorageBackend` /
  :class:`~mgf.vm.core.contracts.DeviceBackend`. This slice wires the
  hypervisor (VM lifecycle) path; storage / device routing lands in B3-B5.
* **The op lifecycle lives in the local
  :class:`~mgf.vm.sdk.operations.OperationRegistry`.** :meth:`submit_op`
  registers a ``PENDING`` handle, runs the work synchronously
  (``PENDING`` → ``RUNNING`` → terminal), and resolves the handle. The
  client's two-step collapse (``submit_op`` → ``watch_op``) sees the same
  registry the worker drove.
* **Dispatch is keyed on :class:`~mgf.vm.core.contracts.OperationKind`** to a
  ``mgf.vm.core`` backend call. Each handler also drives the per-VM
  :class:`~mgf.vm.core.state_machine.VmLifecycleStateMachine` so the legal
  transition table is enforced and registered
  :class:`~mgf.vm.core.contracts.LifecycleObserver` instances fire on every
  transition.

Engine
------

**Per-``VmId`` serialization.** A :class:`threading.Lock` per ``VmId`` means
two ops on the *same* VM serialize (the state machine is not thread-safe — one
instance per VM, caller serializes; ``contracts.md`` §6), while ops on
*different* VMs run concurrently. Reads take no per-VM lock.

**Error bridge.** Backends raise typed ``mgf.vm.core.exceptions`` errors. The
worker catches the ``VmManagerError`` tree and attaches the **original**
exception to the ``FAILED`` handle — no re-wrapping (``api.md`` §7). The
client re-raises that exact exception. The state machine's
:class:`~mgf.vm.core.state_machine.InvalidTransition` is bridged to a typed
:class:`~mgf.vm.core.exceptions.DomainStateInvalid` (an illegal lifecycle
request is a domain-state error to the caller).

**Cancellation (D-D7).** A caller-supplied ``token`` is threaded into the
backend calls that accept one (``start`` / ``stop`` / ``reboot`` / …). If the
backend raises :class:`~mgf.vm.core.exceptions.OperationCancelled`, the op is
marked ``CANCELLED``. Success wins over a late cancel: a backend that finishes
before observing the token returns normally and the op is ``SUCCEEDED``.

``VmId`` ⇄ ``DomainId``
-----------------------

Phase-0 on existing core types: the hypervisor backend speaks
:class:`~mgf.vm.core.ids.DomainId`; the SDK surface speaks
:class:`~mgf.vm.core.ids.VmId`. In-process there is exactly one engine, so the
transport uses the engine's ``DomainId`` string as the ``VmId`` — they are the
same opaque value. ``create_vm`` / ``get_vm`` / ``list_vms`` return the
canonical :class:`~mgf.vm.core.contracts.Vm` aggregate (D-G12), whose
:attr:`~mgf.vm.core.contracts.Vm.vm_id` the caller round-trips for subsequent
calls.

Assembling the ``Vm`` aggregate (D-G12)
---------------------------------------

The canonical :class:`~mgf.vm.core.contracts.Vm` pairs the *desired-state*
:class:`~mgf.vm.core.contracts.VmSpec` with the *observed*
:class:`~mgf.vm.core.contracts.VmStatus`. The engine (``HypervisorBackend``) is
the source of truth for **status** (via ``get_domain`` → ``DomainInfo``), but
its Phase-0 ``DomainInfo`` does NOT carry the full submitted spec — only a few
observed fields (``cpu`` / ``memory_bytes`` / ``name``). To return the *exact
submitted spec* in the ``Vm``, the transport remembers it: a ``vm_id → VmSpec``
map (:attr:`InProcessTransport._submitted_specs`) populated at ``define`` /
``redefine`` time. Phase-0 in-process is single-process, so this in-memory map
is sufficient (it does not survive a restart; the registry / agent owns durable
spec persistence in Phase 1).

``get_vm`` / ``list_vms`` are therefore **best-effort on the spec**: they pair
the live observed status from ``get_domain`` with the remembered submitted spec
when known. If the spec is unknown (e.g. a VM defined out-of-band, or after a
transport restart), a *minimal reconstructed* spec is built from the
``DomainInfo`` fields (``name`` / ``cpu`` / ``memory_bytes``) — disks, NICs,
devices, provisioning and metadata are lost. The **status** is always faithful
(the engine is its source of truth); only the spec is freshness-/memory-limited.
"""

from __future__ import annotations

import json
import threading
import uuid
from dataclasses import replace
from datetime import UTC, datetime
from typing import TYPE_CHECKING, TypeVar

from mgf.vm.core.contracts import (
    DiskSpec,
    DomainInfo,
    NetworkSpec,
    OperationKind,
    TransitionCause,
    Vm,
    VmSpec,
    VmState,
    VmStatus,
)
from mgf.vm.core.exceptions import (
    DomainStateInvalid,
    OperationCancelled,
    TransportError,
    VmManagerError,
)
from mgf.vm.core.ids import DomainId, OperationId, VmId
from mgf.vm.core.serde import vmspec_from_mapping, vmspec_to_mapping
from mgf.vm.core.state_machine import InvalidTransition, VmLifecycleStateMachine

from mgf.vm.sdk.operations import OperationRegistry
from mgf.vm.sdk.queries import (
    BackupVmPayload,
    BridgeForDomain,
    Capabilities,
    CloneVmPayload,
    DeletePayload,
    DetachDevicePayload,
    DeviceCapabilities,
    GetAutostart,
    GetDevice,
    GetGuestInterfaces,
    GetGuestOsInfo,
    GetPool,
    GetVm,
    GetVolume,
    GuestPing,
    ListDevices,
    ListLeases,
    ListNetworks,
    ListOperations,
    ListPools,
    ListVms,
    ListVmSnapshots,
    ListVolumes,
    ListVolumeSnapshots,
    MigrateVmPayload,
    NetworkLeasesQuery,
    PowerPayload,
    RefreshPool,
    RenamePayload,
    RestoreVmPayload,
    SampleMetrics,
    SetAutostartPayload,
    StorageCapabilities,
    VmAttachDevicePayload,
    VmDeleteSnapshotPayload,
    VmRevertSnapshotPayload,
    VmSnapshotPayload,
)
from mgf.vm.sdk.transport import _devices, _storage
from mgf.vm.sdk.transport._devices import AttachRegistry

if TYPE_CHECKING:
    from collections.abc import Callable, Iterator

    from mgf.vm.core.cancellation import CancellationToken
    from mgf.vm.core.contracts import (
        BackupManifest,
        DeviceBackend,
        DomainPolicyBackend,
        EventGap,
        GuestAgentBackend,
        HostDiskBackend,
        HypervisorBackend,
        LifecycleObserver,
        MetricsBackend,
        NetworkBackend,
        OperationHandle,
        StorageBackend,
        VmEvent,
    )

__all__ = ["InProcessTransport"]

# Lifecycle states where the engine still reports the domain "running" but the
# transport's state machine knows it is paused (Phase-0 ``DomainInfo`` has no
# paused flag — see ``_seed_state_machine``).
_PAUSED_FAMILY = frozenset({VmState.PAUSING, VmState.PAUSED, VmState.RESUMING})

# Host disk-image ops (D-G18) — orchestrated server-side over the OPTIONAL
# HostDiskBackend; routed off the per-VM-lock path (they create new images /
# VMs and only read the source) to a dedicated handler.
_HOSTDISK_KINDS = frozenset(
    {OperationKind.VM_BACKUP, OperationKind.VM_CLONE, OperationKind.VM_RESTORE}
)

# Virtual-network management ops (D-G15) — resource-scoped (no VM lock, no
# state machine); routed to the OPTIONAL NetworkBackend.
_NETWORK_KINDS = frozenset(
    {
        OperationKind.NETWORK_DEFINE,
        OperationKind.NETWORK_START,
        OperationKind.NETWORK_STOP,
        OperationKind.NETWORK_DELETE,
    }
)

#: Metadata key under which a backup manifest carries the serialized source
#: ``VmSpec`` (D-G18) — the self-contained-restore enabler.
_VMSPEC_METADATA_KEY = "mgf.vm.spec"

_P = TypeVar("_P")


def _utc_now() -> datetime:
    """Default clock — UTC-aware always (DP-12 / D-G6). Injectable for tests."""
    return datetime.now(UTC)


class InProcessTransport:
    """In-process :class:`~mgf.vm.sdk.transport.base.Transport` over core adapters.

    Construct via :meth:`from_adapters`. The transport owns an
    :class:`~mgf.vm.sdk.operations.OperationRegistry`, a per-``VmId`` lock map,
    and a per-``VmId`` :class:`~mgf.vm.core.state_machine.VmLifecycleStateMachine`.
    """

    def __init__(
        self,
        *,
        hypervisor: HypervisorBackend,
        storage: StorageBackend | None = None,
        device: DeviceBackend | None = None,
        metrics: MetricsBackend | None = None,
        host_disk: HostDiskBackend | None = None,
        network: NetworkBackend | None = None,
        guest_agent: GuestAgentBackend | None = None,
        policy: DomainPolicyBackend | None = None,
        observers: tuple[LifecycleObserver, ...] = (),
        clock: Callable[[], datetime] = _utc_now,
    ) -> None:
        self._hypervisor = hypervisor
        self._storage = storage
        self._device = device
        # Optional per-domain metrics sampler (Wave-B, D-G13), injected exactly
        # like the device backend. None means no metrics surface is configured;
        # a SampleMetrics read then raises a typed TransportError (not empty).
        self._metrics = metrics
        # Optional host disk-image I/O backend (Wave-R4, D-G18), injected like
        # the others. None means no backup/clone/restore surface is configured;
        # a VM_BACKUP/CLONE/RESTORE op then fails with a typed TransportError.
        self._host_disk = host_disk
        # Optional virtual-network management backend (Wave-R3, D-G15), injected
        # like the others. None means no network-management surface is
        # configured; a NETWORK_* op then fails with a typed TransportError.
        self._network = network
        # Optional in-guest agent observation backend (Wave-R6, D-G19), injected
        # like the others. None means no guest-agent surface is configured; a
        # GuestPing / GetGuestOsInfo / GetGuestInterfaces read then raises a
        # typed TransportError.
        self._guest_agent = guest_agent
        # Optional per-domain policy backend (Wave-R7, D-G25), injected like the
        # others. None means no autostart/managed-save surface is configured; a
        # GetAutostart read or a VM_SET_AUTOSTART / VM_MANAGED_SAVE op then fails
        # with a typed TransportError.
        self._policy = policy
        self._observers = tuple(observers)
        # Injectable UTC clock for the Vm aggregate's timestamps (DP-12 / D-G6).
        self._clock = clock

        self._registry = OperationRegistry()
        # Live (vm, device) → lease bindings for the attach transaction (B5).
        self._attachments = AttachRegistry()

        # vm_id → submitted VmSpec, remembered at define/redefine time so the
        # Vm aggregate can carry the *exact* submitted spec (the engine's
        # DomainInfo is observed-only and lossy). Single-process Phase-0 map;
        # guarded by _vm_guard alongside the other per-VM maps. See the module
        # docstring "Assembling the Vm aggregate".
        self._submitted_specs: dict[VmId, VmSpec] = {}
        # When each VM was first defined (for Vm.created_at). UTC-aware.
        self._created_at: dict[VmId, datetime] = {}

        # Per-VmId serialization + state machines. Guarded by _vm_guard so the
        # lazy create-on-first-use of a VM's lock / SM is itself atomic.
        self._vm_guard = threading.Lock()
        self._vm_locks: dict[VmId, threading.Lock] = {}
        self._state_machines: dict[VmId, VmLifecycleStateMachine] = {}
        self._closed = False

    @classmethod
    def from_adapters(
        cls,
        hypervisor: HypervisorBackend,
        storage: StorageBackend | None = None,
        device: DeviceBackend | None = None,
        *,
        metrics: MetricsBackend | None = None,
        host_disk: HostDiskBackend | None = None,
        network: NetworkBackend | None = None,
        guest_agent: GuestAgentBackend | None = None,
        policy: DomainPolicyBackend | None = None,
        observers: tuple[LifecycleObserver, ...] = (),
        clock: Callable[[], datetime] = _utc_now,
    ) -> InProcessTransport:
        """Build a transport from constructor-injected backends (B1).

        ``hypervisor`` is required; ``storage`` / ``device`` / ``metrics`` /
        ``host_disk`` / ``network`` are optional. ``metrics`` is the OPTIONAL
        Wave-B :class:`~mgf.vm.core.contracts.MetricsBackend` (D-G13);
        ``host_disk`` the OPTIONAL Wave-R4
        :class:`~mgf.vm.core.contracts.HostDiskBackend` (D-G18) backup/clone/
        restore depends on; ``network`` the OPTIONAL Wave-R3
        :class:`~mgf.vm.core.contracts.NetworkBackend` (D-G15) network
        management depends on — all injected exactly like ``device``; when
        ``None`` the corresponding op raises a typed
        :class:`~mgf.vm.core.exceptions.TransportError`. ``observers`` are
        registered on every VM's state machine as it is created, so they see
        lifecycle transitions. ``clock`` is the injectable UTC time source for
        the :class:`Vm` aggregate's ``created_at`` / ``last_transition_at``
        (DP-12 / D-G6).
        """
        return cls(
            hypervisor=hypervisor,
            storage=storage,
            device=device,
            metrics=metrics,
            host_disk=host_disk,
            network=network,
            guest_agent=guest_agent,
            policy=policy,
            observers=observers,
            clock=clock,
        )

    # -- per-VM machinery --------------------------------------------------

    def _lock_for(self, vm_id: VmId) -> threading.Lock:
        with self._vm_guard:
            lock = self._vm_locks.get(vm_id)
            if lock is None:
                lock = threading.Lock()
                self._vm_locks[vm_id] = lock
            return lock

    def _state_machine_for(self, vm_id: VmId) -> VmLifecycleStateMachine:
        with self._vm_guard:
            sm = self._state_machines.get(vm_id)
            if sm is None:
                sm = VmLifecycleStateMachine(vm_id)
                for observer in self._observers:
                    sm.add_observer(observer)
                self._state_machines[vm_id] = sm
            return sm

    # -- Vm-aggregate assembly (D-G12) ------------------------------------

    def _remember_spec(self, vm_id: VmId, spec: VmSpec) -> None:
        """Record the submitted ``spec`` for ``vm_id`` (define / redefine time).

        Stamps ``created_at`` once, on first definition. See the module
        docstring "Assembling the Vm aggregate".
        """
        with self._vm_guard:
            self._submitted_specs[vm_id] = spec
            self._created_at.setdefault(vm_id, self._clock())

    def _build_vm(self, vm_id: VmId, info: DomainInfo) -> Vm:
        """Assemble the canonical :class:`Vm` from observed ``info`` + spec map.

        The :class:`~mgf.vm.core.contracts.VmStatus` carries the ``state``
        derived from the engine's observed ``DomainInfo`` (the source of truth)
        plus the richer observed shape the adapter surfaces — the attached
        ``disks`` / ``nics`` / ``devices_attached`` (contracts §6.3). The spec is
        the remembered submitted spec, or a minimal reconstruction from the
        observed fields when unknown (best-effort — see the module docstring).
        """
        with self._vm_guard:
            spec = self._submitted_specs.get(vm_id)
            created_at = self._created_at.get(vm_id)
        if spec is None:
            # Best-effort: VM unknown to this transport (defined out-of-band or
            # after a restart). Reconstruct the minimal neutral spec from the
            # observed fields; disks/nics/devices/provisioning/metadata are lost.
            spec = VmSpec(name=info.name, cpu=info.cpu, memory_bytes=info.memory_bytes)
        now = self._clock()
        return Vm(
            vm_id=vm_id,
            spec=spec,
            status=VmStatus(
                state=_observed_state(info),
                # vm_id is the aggregate identity (in scope here); the observed
                # lists pass straight through from the adapter's get_domain read.
                vm_id=vm_id,
                disks=info.disks,
                nics=info.nics,
                devices_attached=info.devices_attached,
                # host_id: this in-process transport does no scheduling, so it
                # never observes a placement host — leave the contract default.
                # error / last_transition_at: Phase-0 ``DomainInfo`` carries
                # neither, so don't fabricate them (D-D6; DP-12 forbids a naive
                # stamp). They settle to the contract defaults (None).
            ),
            created_at=created_at if created_at is not None else now,
            last_transition_at=now,
        )

    # -- Transport seam: mutations ----------------------------------------

    def submit_op(
        self,
        kind: OperationKind,
        payload: object,
        *,
        client_op_id: str,
        token: object | None = None,
    ) -> OperationHandle:
        """Register and synchronously drive a mutating op (``api.md`` §4.2).

        Dedup on ``client_op_id`` (D-G5): a repeated submit returns the
        already-issued handle without re-running the work. Otherwise the op is
        registered ``PENDING``, run to a terminal state under the target VM's
        lock, and the (terminal) handle is returned. The client resolves it via
        :meth:`watch_op` (which yields the recorded transitions).
        """
        if self._closed:
            raise TransportError("transport is closed")

        op_id = OperationId(f"op-{uuid.uuid4()}")
        handle = self._registry.submit(op_id, kind, client_op_id=client_op_id)
        # Dedup hit: the registry returned an existing op (different op_id).
        if handle.op_id != op_id:
            return self._registry.get(handle.op_id)

        self._run_op(op_id, kind, payload, _as_token(token))
        return self._registry.get(op_id)

    def get_op(self, op_id: OperationId) -> OperationHandle:
        """Fetch the current handle for ``op_id`` (``GetOperation``)."""
        try:
            return self._registry.get(op_id)
        except KeyError as exc:
            raise TransportError(f"unknown operation {op_id}") from exc

    def watch_op(self, op_id: OperationId) -> Iterator[OperationHandle]:
        """Stream ``op_id``'s transitions to terminal (``WatchOp``).

        In-process, :meth:`submit_op` runs the op synchronously to a terminal
        state before returning, so by the time a watcher subscribes the op is
        usually already terminal. The registry's blocking
        :class:`~mgf.vm.sdk.operations.WatchOpStream` only yields *future*
        transitions and would block forever on a settled op — so we short-
        circuit: if the op is already terminal, yield that handle once and
        stop. Otherwise delegate to the registry stream for the live case
        (concurrent watcher on an in-flight op).
        """
        try:
            current = self._registry.get(op_id)
        except KeyError as exc:
            raise TransportError(f"unknown operation {op_id}") from exc
        if current.status.is_terminal:
            return iter((current,))
        return iter(self._registry.watch(op_id))

    # -- the worker: dispatch + state machine + error/cancel bridge --------

    def _run_op(
        self,
        op_id: OperationId,
        kind: OperationKind,
        payload: object,
        token: CancellationToken | None,
    ) -> None:
        """Run one op synchronously; resolve the handle.

        **VM-scoped** ops (lifecycle, VM snapshots, attach/detach) serialize
        under the target VM's lock and drive the state machine.
        **Resource-scoped** ops (storage volumes/pools, bare device leases)
        carry no ``VmId`` — they take no per-VM lock and route straight to the
        per-domain handler (the backend owns its own thread safety,
        ``contracts.md`` §8).
        """
        if kind in _storage.STORAGE_KINDS or kind in _devices.LEASE_KINDS:
            self._run_resource_op(op_id, kind, payload, token)
            return

        if kind in _HOSTDISK_KINDS:
            self._run_hostdisk_op(op_id, kind, payload, token)
            return

        if kind in _NETWORK_KINDS:
            self._run_network_op(op_id, kind, payload)
            return

        try:
            vm_id = _vm_id_of(kind, payload)
        except VmManagerError as exc:
            # A malformed payload can't be routed to a VM lock; fail the handle.
            self._registry.mark_running(op_id)
            self._registry.mark_failed(op_id, exc)
            return
        lock = self._lock_for(vm_id)
        with lock:
            self._registry.mark_running(op_id)
            try:
                result = self._dispatch(op_id, kind, payload, vm_id, token)
            except OperationCancelled:
                # D-D7: an honest cancel (backend rolled back) → CANCELLED.
                self._registry.mark_cancelled(op_id)
            except InvalidTransition as exc:
                # Illegal lifecycle request → typed domain-state error.
                self._registry.mark_failed(op_id, DomainStateInvalid(str(exc)))
            except VmManagerError as exc:
                # Error bridge: attach the ORIGINAL exception, no re-wrap.
                self._registry.mark_failed(op_id, exc)
            else:
                self._registry.mark_completed(op_id, result)

    def _run_resource_op(
        self,
        op_id: OperationId,
        kind: OperationKind,
        payload: object,
        token: CancellationToken | None,
    ) -> None:
        """Drive a resource-scoped op (storage / bare lease) — no VM lock."""
        self._registry.mark_running(op_id)
        try:
            if kind in _storage.STORAGE_KINDS:
                result = _storage.dispatch(self._storage, kind, payload, token)
            else:
                result = _devices.dispatch_lease(self._device, kind, payload, token)
        except OperationCancelled:
            self._registry.mark_cancelled(op_id)
        except VmManagerError as exc:
            # Error bridge: attach the ORIGINAL exception, no re-wrap.
            self._registry.mark_failed(op_id, exc)
        else:
            self._registry.mark_completed(op_id, result)

    # -- virtual-network management ops (D-G15) ---------------------------

    def _run_network_op(
        self,
        op_id: OperationId,
        kind: OperationKind,
        payload: object,
    ) -> None:
        """Drive a network-management op (define / start / stop / delete) —
        D-G15. Resource-scoped: no per-VM lock or state machine. The
        ``NetworkBackend``'s typed errors (``NetworkExists`` /
        ``NetworkNotFound`` / …) propagate to a ``FAILED`` handle verbatim."""
        self._registry.mark_running(op_id)
        try:
            result = self._dispatch_network(kind, payload)
        except VmManagerError as exc:
            self._registry.mark_failed(op_id, exc)
        else:
            self._registry.mark_completed(op_id, result)

    def _dispatch_network(self, kind: OperationKind, payload: object) -> object:
        """Route a network ``kind`` → ``NetworkBackend`` call; return the result."""
        backend = self._network
        if backend is None:
            raise TransportError(
                f"operation {kind.value!r} needs a NetworkBackend but none was injected"
            )
        if kind is OperationKind.NETWORK_DEFINE:
            return backend.define_network(_as(payload, NetworkSpec))
        if kind is OperationKind.NETWORK_START:
            backend.start_network(_as(payload, str))
            return None
        if kind is OperationKind.NETWORK_STOP:
            backend.stop_network(_as(payload, str))
            return None
        if kind is OperationKind.NETWORK_DELETE:
            backend.delete_network(_as(payload, str))
            return None
        raise TransportError(f"network handler does not own {kind.value!r}")

    # -- host disk-image ops (D-G18): backup / clone / restore -------------

    def _run_hostdisk_op(
        self,
        op_id: OperationId,
        kind: OperationKind,
        payload: object,
        token: CancellationToken | None,
    ) -> None:
        """Drive a host disk-image op (backup / clone / restore) — D-G18.

        These take no per-VM lock: backup only reads the source spec, while
        clone / restore define a *new* VM whose id is not known until
        ``define`` returns (its state machine is created+guarded there). The
        same error/cancel bridge as the other handlers applies — the
        backend's typed errors (``DiskOperationError`` / ``BackupError`` /
        ``DomainNotFound`` / …) propagate to a ``FAILED`` handle verbatim.
        """
        self._registry.mark_running(op_id)
        try:
            result = self._dispatch_hostdisk(op_id, kind, payload, token)
        except OperationCancelled:
            self._registry.mark_cancelled(op_id)
        except VmManagerError as exc:
            self._registry.mark_failed(op_id, exc)
        else:
            self._registry.mark_completed(op_id, result)

    def _dispatch_hostdisk(
        self,
        op_id: OperationId,
        kind: OperationKind,
        payload: object,
        token: CancellationToken | None,
    ) -> object:
        """Route a host-disk ``kind`` → its orchestration; return the result."""
        backend = self._host_disk
        if backend is None:
            raise TransportError(
                f"operation {kind.value!r} needs a HostDiskBackend but none was injected"
            )

        if kind is OperationKind.VM_BACKUP:
            return self._do_backup(_as(payload, BackupVmPayload), backend, token)
        if kind is OperationKind.VM_CLONE:
            return self._do_clone(op_id, _as(payload, CloneVmPayload), backend, token)
        if kind is OperationKind.VM_RESTORE:
            return self._do_restore(op_id, _as(payload, RestoreVmPayload), backend, token)
        raise TransportError(f"host-disk handler does not own {kind.value!r}")

    def _do_backup(
        self,
        bp: BackupVmPayload,
        backend: HostDiskBackend,
        token: CancellationToken | None,
    ) -> BackupManifest:
        """Archive the source VM's disks; carry its serialized spec (D-G18).

        Reads the source spec (with resolved disk sources), archives each disk
        image keyed by its index (the neutral spec has no guest target), and
        stores the serialized ``VmSpec`` in the manifest metadata so a restore
        is self-contained.
        """
        spec = self._effective_spec(bp.vm_id)
        sources = {_disk_key(i): d.source for i, d in enumerate(spec.disks) if d.source is not None}
        metadata = {_VMSPEC_METADATA_KEY: json.dumps(vmspec_to_mapping(spec))}
        return backend.archive(spec.name, sources, bp.dest, metadata=metadata, token=token)

    def _do_clone(
        self,
        op_id: OperationId,
        cp: CloneVmPayload,
        backend: HostDiskBackend,
        token: CancellationToken | None,
    ) -> Vm:
        """Clone the source VM engine-neutrally (D-G18).

        Copies (full) or overlays (``linked``) each source disk into
        ``dest_dir`` and defines a new VM from a spec carrying the new name +
        new disk sources — no native-format rewrite (the neutral ``VmSpec``
        already carries sources). Disks without a source pass through verbatim.
        """
        src = self._effective_spec(cp.vm_id)
        dest_dir = cp.dest_dir.rstrip("/")
        new_disks = []
        for i, disk in enumerate(src.disks):
            if disk.source is None:
                new_disks.append(disk)
                continue
            dest = f"{dest_dir}/{cp.new_name}-{i}.{disk.format}"
            if cp.linked:
                backend.create_overlay(disk.source, dest)
            else:
                backend.copy_image(disk.source, dest, token=token)
            new_disks.append(replace(disk, source=dest))
        new_spec = replace(src, name=cp.new_name, disks=tuple(new_disks))
        return self._define_and_build(op_id, new_spec)

    def _do_restore(
        self,
        op_id: OperationId,
        rp: RestoreVmPayload,
        backend: HostDiskBackend,
        token: CancellationToken | None,
    ) -> Vm:
        """Restore a VM from a backup archive engine-neutrally (D-G18).

        Extracts + checksum-verifies the archive, reconstructs the ``VmSpec``
        carried in the manifest metadata, repoints its disk sources at the
        restored images (matched by the index key the backup wrote), and
        defines the VM under ``new_name`` (or the backed-up name).
        """
        restored = backend.extract(rp.archive, rp.dest_dir, token=token)
        spec_json = restored.metadata.get(_VMSPEC_METADATA_KEY)
        if spec_json is None:
            raise TransportError("backup archive carries no VmSpec metadata; cannot restore")
        src = vmspec_from_mapping(json.loads(spec_json))
        restored_by_key = {d.target: d.path for d in restored.disks}
        new_disks = [
            replace(disk, source=restored_by_key[_disk_key(i)])
            if _disk_key(i) in restored_by_key
            else disk
            for i, disk in enumerate(src.disks)
        ]
        name = rp.new_name or src.name
        new_spec = replace(src, name=name, disks=tuple(new_disks))
        return self._define_and_build(op_id, new_spec)

    def _effective_spec(self, vm_id: VmId) -> VmSpec:
        """The source VM's spec with **resolved disk sources** (D-G18).

        The disk-file operations need each disk's host source path. Two places
        carry it, with different lifetimes:

        * the *remembered submitted spec* (:attr:`_submitted_specs`) — exact,
          but only for a VM defined through **this** transport instance (it
          does not survive a fresh process / a VM defined out-of-band);
        * the engine's *observed* :class:`~mgf.vm.core.contracts.DiskStatus`
          (via ``get_domain``) — always current for any VM the engine knows,
          which is the common case for ``backup`` / ``clone`` (a long-lived VM
          a fresh CLI process operates on).

        So: take the remembered spec when it already carries disks; otherwise
        fall back to the observed disks, mapping each :class:`DiskStatus` to a
        neutral :class:`DiskSpec` (format inferred from the source path's
        extension — the engine doesn't observe it). ``get_domain`` raises the
        backend's ``DomainNotFound`` if the VM is absent (propagated as the
        op's typed failure).
        """
        info = self._hypervisor.get_domain(DomainId(vm_id.value))
        with self._vm_guard:
            base = self._submitted_specs.get(vm_id)
        if base is None:
            base = VmSpec(name=info.name, cpu=info.cpu, memory_bytes=info.memory_bytes)
        if base.disks:
            return replace(base, name=info.name)
        disks = tuple(
            DiskSpec(
                size_bytes=d.size_bytes,
                format=_format_of(d.source),
                source=d.source,
                readonly=d.readonly,
            )
            for d in info.disks
            if d.source is not None
        )
        return replace(base, name=info.name, disks=disks)

    def _define_and_build(self, op_id: OperationId, spec: VmSpec) -> Vm:
        """Define ``spec`` as a new VM and assemble its :class:`Vm` aggregate.

        Shared clone/restore tail — mirrors the ``VM_CREATE`` dispatch: define,
        remember the spec, seed the new VM's state machine to ``DEFINED``, and
        return the canonical aggregate.
        """
        domain_id = self._hypervisor.define(spec)
        new_vm = VmId(domain_id.value)
        self._remember_spec(new_vm, spec)
        sm = self._state_machine_for(new_vm)
        if sm.state is None:
            sm.transition(
                VmState.DEFINED,
                cause=TransitionCause.USER_REQUEST,
                operation_id=op_id,
            )
        return self._build_vm(new_vm, self._hypervisor.get_domain(domain_id))

    def _drive(
        self,
        sm: VmLifecycleStateMachine,
        op_id: OperationId,
        intermediate: VmState,
        terminal: VmState,
        work: Callable[[], None],
    ) -> None:
        """Drive one power op: intermediate → backend work → terminal.

        On backend failure the machine is forced to ``FAILED`` (a legal retry
        origin) before the error propagates. The intermediate transition runs
        first so a wrong-state request (e.g. stop-before-start) fails fast with
        a typed :class:`InvalidTransition` and never touches the backend.
        """
        sm.transition(intermediate, cause=TransitionCause.USER_REQUEST, operation_id=op_id)
        try:
            work()
        except OperationCancelled:
            # An honest cancel leaves the engine as-is; reflect "not converged"
            # by forcing FAILED so a retry has a legal origin, then re-raise.
            sm.force_state(VmState.FAILED, cause=TransitionCause.FAULT)
            raise
        except VmManagerError:
            sm.force_state(VmState.FAILED, cause=TransitionCause.FAULT)
            raise
        sm.transition(terminal, cause=TransitionCause.ENGINE_EVENT, operation_id=op_id)

    def _seed_state_machine(self, vm_id: VmId, domain_id: DomainId) -> VmLifecycleStateMachine:
        """Reconcile a VM's state machine to the backend's observed state.

        The transport does not persist lifecycle state across process restarts,
        and the engine is the source of truth (a VM may have been started out
        of band). Before each lifecycle op we read ``get_domain`` — which also
        raises ``DomainNotFound`` for an unknown VM, so a bad target fails with
        the *engine's* typed error rather than an ``InvalidTransition`` — and
        :meth:`force_state` the machine to the matching settled state
        (``RUNNING`` / ``STOPPED``). Idempotent re-seeds are cheap and keep the
        legal-transition table meaningful.
        """
        info = self._hypervisor.get_domain(domain_id)
        observed = VmState.RUNNING if info.is_running else VmState.STOPPED
        sm = self._state_machine_for(vm_id)
        # Phase-0 ``DomainInfo`` carries only ``is_running`` (no paused flag), so
        # a paused VM still reports running. Trust the SM's own paused-family
        # tracking in that case; otherwise reconcile to the observed settled
        # state (covers out-of-band start/stop + first-use seeding).
        if observed is VmState.RUNNING and sm.state in _PAUSED_FAMILY:
            return sm
        if sm.state is not observed:
            sm.force_state(observed, cause=TransitionCause.RECONCILER)
        return sm

    def _dispatch(
        self,
        op_id: OperationId,
        kind: OperationKind,
        payload: object,
        vm_id: VmId,
        token: CancellationToken | None,
    ) -> object:
        """Route ``kind`` → backend call + drive the state machine.

        Returns the op-specific success value (a :class:`Vm` aggregate for
        create / redefine — D-G12, ``None`` for the power ops). Raises the
        backend's typed error or :class:`InvalidTransition` straight up to
        :meth:`_run_op`.

        For the power ops the machine is seeded from observed state first
        (:meth:`_seed_state_machine`); the requested intermediate transition
        runs; the backend does the work; the terminal transition lands on
        success. If the backend raises, the machine is forced to ``FAILED`` so
        a later retry starts from a legal state (``FAILED`` → ``STOPPING`` /
        ``STOPPED``), then the original error propagates.
        """
        hv = self._hypervisor

        if kind is OperationKind.VM_CREATE:
            spec = _as_spec(payload)
            domain_id = hv.define(spec)
            created_vm = VmId(domain_id.value)
            self._remember_spec(created_vm, spec)
            sm = self._state_machine_for(created_vm)
            if sm.state is None:
                sm.transition(
                    VmState.DEFINED,
                    cause=TransitionCause.USER_REQUEST,
                    operation_id=op_id,
                )
            return self._build_vm(created_vm, hv.get_domain(domain_id))

        if kind is OperationKind.VM_REDEFINE:
            spec, target = _as_spec_and_vm(payload)
            domain_id = DomainId(target.value)
            hv.redefine(domain_id, spec)
            # The redefinition replaces the desired-state spec.
            self._remember_spec(target, spec)
            return self._build_vm(target, hv.get_domain(domain_id))

        domain_id = DomainId(vm_id.value)

        # Domain-policy ops (D-G25) route to the injected DomainPolicyBackend and
        # do NOT drive the lifecycle state machine: set_autostart is a metadata
        # flag (no state change); managed_save's stop is reflected on the next
        # observe (the backend raises OperationError for a non-running domain).
        if kind is OperationKind.VM_SET_AUTOSTART:
            self._require_policy("op 'VM_SET_AUTOSTART'").set_autostart(
                domain_id, enabled=_as(payload, SetAutostartPayload).enabled
            )
            return None
        if kind is OperationKind.VM_MANAGED_SAVE:
            self._require_policy("op 'VM_MANAGED_SAVE'").managed_save(domain_id)
            return None

        # Rename (D-G26): a definition-time op, not a lifecycle transition — no
        # state-machine drive. The DomainId/UUID is stable across the rename, so
        # we keep the cached desired-state spec's name in sync for `get_vm`.
        if kind is OperationKind.VM_RENAME:
            rename_payload = _as(payload, RenamePayload)
            hv.rename(domain_id, rename_payload.new_name)
            cached_spec = self._submitted_specs.get(vm_id)
            if cached_spec is not None:
                self._submitted_specs[vm_id] = replace(cached_spec, name=rename_payload.new_name)
            return None

        sm = self._seed_state_machine(vm_id, domain_id)

        if kind is OperationKind.VM_START:
            self._drive(
                sm,
                op_id,
                VmState.STARTING,
                VmState.RUNNING,
                lambda: hv.start(domain_id, token=token),
            )
            return None

        if kind is OperationKind.VM_STOP:
            force = _force_of(payload)
            self._drive(
                sm,
                op_id,
                VmState.STOPPING,
                VmState.STOPPED,
                lambda: hv.stop(domain_id, force=force, token=token),
            )
            return None

        if kind is OperationKind.VM_PAUSE:
            self._drive(sm, op_id, VmState.PAUSING, VmState.PAUSED, lambda: hv.pause(domain_id))
            return None

        if kind is OperationKind.VM_RESUME:
            self._drive(sm, op_id, VmState.RESUMING, VmState.RUNNING, lambda: hv.resume(domain_id))
            return None

        if kind is OperationKind.VM_REBOOT:
            # Reboot keeps the VM running — no lifecycle-state move.
            force = _force_of(payload)
            hv.reboot(domain_id, force=force, token=token)
            return None

        if kind is OperationKind.VM_UNDEFINE:
            delete_storage = _delete_storage_of(payload)
            hv.undefine(domain_id, delete_storage=delete_storage)
            # Drop the remembered spec — the VM no longer exists (idempotent).
            with self._vm_guard:
                self._submitted_specs.pop(vm_id, None)
                self._created_at.pop(vm_id, None)
            return None

        if kind is OperationKind.VM_MIGRATE:
            mp = _as_migrate(payload)
            # MIGRATING is reachable only from RUNNING (state_machine §legal):
            # a stopped VM fails fast here with InvalidTransition. On backend
            # failure ``_drive`` forces FAILED (a legal retry origin) and the
            # source is left as the engine reports it; on success the local SM
            # settles back to RUNNING (the op completed — the source migrated
            # away). MigrationFailed/Cancelled propagate via the error bridge.
            self._drive(
                sm,
                op_id,
                VmState.MIGRATING,
                VmState.RUNNING,
                lambda: hv.migrate(domain_id, mp.dest, plan=mp.plan, token=token),
            )
            return None

        # -- VM snapshots (B3) — hypervisor-side; no lifecycle-state move -----
        if kind is OperationKind.VM_SNAPSHOT:
            sp = _as_vm_snapshot(payload)
            return hv.snapshot(domain_id, sp.name, include_memory=sp.include_memory)

        if kind is OperationKind.VM_REVERT_SNAPSHOT:
            rp = _as_vm_revert(payload)
            hv.revert_snapshot(domain_id, rp.snapshot_id, token=token)
            return None

        if kind is OperationKind.VM_DELETE_SNAPSHOT:
            dp = _as_vm_delete_snapshot(payload)
            hv.delete_snapshot(domain_id, dp.snapshot_id)
            return None

        # -- device attach/detach (B5) — THE ATTACH TRANSACTION ---------------
        if kind in _devices.ATTACH_KINDS:
            return _devices.dispatch_attach(self._device, hv, self._attachments, kind, payload)

        raise TransportError(f"operation kind {kind.value!r} not wired in-process")

    # -- Transport seam: reads --------------------------------------------

    def read(self, query: object) -> object:
        """Route a read query to the backend (``Get*`` / ``List*`` / caps)."""
        if self._closed:
            raise TransportError("transport is closed")

        if isinstance(query, GetVm):
            vm_id = query.vm_id
            info = self._hypervisor.get_domain(DomainId(vm_id.value))
            return self._build_vm(vm_id, info)
        if isinstance(query, ListVms):
            return [
                self._build_vm(VmId(info.domain_id.value), info)
                for info in self._hypervisor.list_domains()
            ]
        if isinstance(query, ListOperations):
            return self._registry.list()
        if isinstance(query, Capabilities):
            return self._hypervisor.capabilities()
        if isinstance(query, ListVmSnapshots):
            return self._hypervisor.list_snapshots(DomainId(query.vm_id.value))

        # -- host-network observation (Wave-B, D-G14) — on the HypervisorBackend
        # The backend returns engine-neutral tuples; normalize to list to match
        # the client's list[...] read surface (as the other List* reads return).
        if isinstance(query, ListNetworks):
            return list(self._hypervisor.list_networks())
        if isinstance(query, NetworkLeasesQuery):
            return list(self._hypervisor.network_leases(query.network))
        if isinstance(query, BridgeForDomain):
            # Resolve the VM's DomainId exactly as the other VM-keyed reads do.
            return self._hypervisor.bridge_for_domain(DomainId(query.vm_id.value))

        # -- per-domain metrics (Wave-B, D-G13) — injected MetricsBackend ----
        if isinstance(query, SampleMetrics):
            return self._read_metrics(query)

        # -- domain policy (Wave-R7, D-G25) — injected DomainPolicyBackend ---
        if isinstance(query, GetAutostart):
            return self._read_policy(query)

        # -- guest-agent reads (Wave-R6, D-G19) — injected GuestAgentBackend -
        if isinstance(query, GuestPing | GetGuestOsInfo | GetGuestInterfaces):
            return self._read_guest_agent(query)

        # -- storage reads (B3/B4) -------------------------------------------
        if isinstance(
            query,
            GetVolume
            | ListVolumes
            | GetPool
            | RefreshPool
            | ListPools
            | ListVolumeSnapshots
            | StorageCapabilities,
        ):
            return self._read_storage(query)

        # -- device reads (B5) -----------------------------------------------
        if isinstance(query, GetDevice | ListDevices | ListLeases | DeviceCapabilities):
            return self._read_device(query)

        raise TransportError(f"unsupported read query {type(query).__name__!r}")

    def _read_storage(self, query: object) -> object:
        """Route a storage read to the ``StorageBackend`` (B3/B4)."""
        storage = self._storage
        if storage is None:
            raise TransportError(
                f"read {type(query).__name__!r} needs a StorageBackend but none was injected"
            )
        if isinstance(query, GetVolume):
            return storage.get_volume(query.volume_id)
        if isinstance(query, ListVolumes):
            return storage.list_volumes(query.pool)
        if isinstance(query, GetPool):
            return storage.get_pool(query.name)
        if isinstance(query, RefreshPool):
            return storage.refresh_pool(query.name)
        if isinstance(query, ListPools):
            return storage.list_pools()
        if isinstance(query, ListVolumeSnapshots):
            return storage.list_snapshots(query.volume_id)
        # StorageCapabilities
        return storage.capabilities()

    def _read_device(self, query: object) -> object:
        """Route a device read to the ``DeviceBackend`` (B5)."""
        device = self._device
        if device is None:
            raise TransportError(
                f"read {type(query).__name__!r} needs a DeviceBackend but none was injected"
            )
        if isinstance(query, GetDevice):
            return device.get_device(query.device_id)
        if isinstance(query, ListDevices):
            return device.list_assignable()
        if isinstance(query, ListLeases):
            return device.list_leased()
        # DeviceCapabilities
        return device.capabilities()

    def _read_metrics(self, query: SampleMetrics) -> object:
        """Route a metrics read to the injected ``MetricsBackend`` (D-G13).

        Mirrors :meth:`_read_device`: when no metrics backend was injected the
        read raises a typed :class:`~mgf.vm.core.exceptions.TransportError`
        rather than silently returning empty — the caller must configure a
        :class:`~mgf.vm.core.contracts.MetricsBackend` to sample. The VM's
        ``DomainId`` is resolved exactly as the other VM-keyed reads do; the
        backend raises ``DomainNotFound`` for an unknown domain (propagated
        unchanged across the seam).
        """
        metrics = self._metrics
        if metrics is None:
            raise TransportError(
                "read 'SampleMetrics' needs a MetricsBackend but none was injected"
            )
        return metrics.sample_metrics(DomainId(query.vm_id.value))

    def _read_policy(self, query: GetAutostart) -> object:
        """Route a policy read to the injected ``DomainPolicyBackend`` (D-G25).

        Mirrors :meth:`_read_metrics`: when no policy backend was injected the
        read raises a typed :class:`~mgf.vm.core.exceptions.TransportError`. The
        backend raises ``DomainNotFound`` for an unknown domain (propagated
        unchanged across the seam).
        """
        return self._require_policy("read 'GetAutostart'").get_autostart(
            DomainId(query.vm_id.value)
        )

    def _require_policy(self, what: str) -> DomainPolicyBackend:
        """Return the injected ``DomainPolicyBackend`` or raise ``TransportError``."""
        policy = self._policy
        if policy is None:
            raise TransportError(f"{what} needs a DomainPolicyBackend but none was injected")
        return policy

    def _read_guest_agent(self, query: GuestPing | GetGuestOsInfo | GetGuestInterfaces) -> object:
        """Route a guest-agent read to the injected ``GuestAgentBackend`` (D-G19).

        Mirrors :meth:`_read_metrics`: when no guest-agent backend was injected
        the read raises a typed :class:`~mgf.vm.core.exceptions.TransportError`
        rather than degrading silently. The VM's ``DomainId`` is resolved as the
        other VM-keyed reads do; the backend raises ``DomainNotFound`` for an
        unknown domain and ``GuestUnreachableError`` for an unreachable agent
        (both propagated unchanged across the seam).
        """
        guest = self._guest_agent
        if guest is None:
            raise TransportError("read needs a GuestAgentBackend but none was injected")
        d = DomainId(query.vm_id.value)
        if isinstance(query, GuestPing):
            return guest.guest_ping(d)
        if isinstance(query, GetGuestOsInfo):
            return guest.guest_os_info(d)
        return guest.guest_interfaces(d)

    def watch_stream(self, subject: object) -> Iterator[VmEvent | EventGap]:
        """Stream resource events for ``subject`` via the hypervisor backend (B6).

        ``subject`` is an optional :class:`~mgf.vm.core.ids.VmId`; ``None`` (or
        any non-``VmId``) means all domains. The backend's ``watch_events``
        iterator yields :class:`~mgf.vm.core.contracts.VmEvent` in causal
        per-domain order and surfaces a synthetic
        :class:`~mgf.vm.core.contracts.EventGap` when its retention window
        elapsed (``api.md`` §8.1; ``contracts.md`` ``watch_events``). We pass
        the iterator straight through — the gap is not swallowed; the caller
        reconciles. Closing the returned iterator releases the engine
        subscription deterministically (DP-13).
        """
        if self._closed:
            raise TransportError("transport is closed")
        domain = DomainId(subject.value) if isinstance(subject, VmId) else None
        return self._hypervisor.watch_events(domain)

    def close(self) -> None:
        """Release the backends (DP-13). Idempotent."""
        if self._closed:
            return
        self._closed = True
        self._hypervisor.close()
        if self._storage is not None:
            self._storage.close()
        if self._device is not None:
            self._device.close()
        # NOTE: MetricsBackend (D-G13), HostDiskBackend (D-G18) and
        # NetworkBackend (D-G15) declare no close() in their Protocols — they
        # are injected, caller-owned surfaces (the network/metrics adapters
        # typically share the hypervisor's connection) — so the transport does
        # not own their lifecycle.


# ---------------------------------------------------------------------------
# Payload / token helpers — keep _dispatch readable + type-narrowed.
# ---------------------------------------------------------------------------


def _as_token(token: object | None) -> CancellationToken | None:
    """Narrow a type-erased ``token`` to a :class:`CancellationToken`.

    The seam threads ``token: object | None``; in-process we accept any object
    that structurally satisfies the (``runtime_checkable``) protocol and pass
    it to backends, else ``None``.
    """
    from mgf.vm.core.cancellation import CancellationToken

    if token is not None and isinstance(token, CancellationToken):
        return token
    return None


def _as(payload: object, expected: type[_P]) -> _P:
    """Narrow ``payload`` to ``expected`` or raise a typed transport error.

    Generic counterpart of the per-kind ``_as_*`` helpers, used by the
    host-disk handler (D-G18).
    """
    if isinstance(payload, expected):
        return payload
    raise TransportError(f"payload {type(payload).__name__!r} is not a {expected.__name__}")


def _disk_key(index: int) -> str:
    """Stable archive/restore key for the disk at ``index`` (D-G18).

    The canonical ``VmSpec`` has no guest target (``vda``/…) — disks are
    index-identified — so backup keys each disk by position and restore maps
    the restored images back the same way.
    """
    return f"disk{index}"


def _format_of(source: str | None) -> str:
    """Infer a neutral disk format from a source path's extension (D-G18).

    The engine's observed :class:`~mgf.vm.core.contracts.DiskStatus` carries no
    format, so when resolving disks from observed state we infer it from the
    image filename (``qcow2`` is the safe default for the common case).
    """
    if source is not None:
        lowered = source.lower()
        if lowered.endswith(".raw") or lowered.endswith(".img"):
            return "raw"
        if lowered.endswith(".vmdk"):
            return "vmdk"
    return "qcow2"


def _vm_id_of(kind: OperationKind, payload: object) -> VmId:
    """Extract the VM the op serializes on, from the op's payload.

    ``VM_CREATE`` has no VM yet — it serializes on a per-spec-name synthetic
    id so two creates of the same name serialize but distinct names run
    concurrently. Every other lifecycle op carries a ``VmId`` (directly or in a
    payload wrapper).
    """
    if kind is OperationKind.VM_CREATE:
        spec = _as_spec(payload)
        return VmId(f"__create__:{spec.name}")
    if isinstance(payload, VmId):
        return payload
    if isinstance(
        payload,
        PowerPayload
        | SetAutostartPayload
        | RenamePayload
        | DeletePayload
        | MigrateVmPayload
        | VmSnapshotPayload
        | VmRevertSnapshotPayload
        | VmDeleteSnapshotPayload
        | VmAttachDevicePayload
        | DetachDevicePayload,
    ):
        return payload.vm_id
    if isinstance(payload, tuple) and payload and isinstance(payload[0], VmId):
        # (vm_id, spec) for VM_REDEFINE.
        return payload[0]
    raise TransportError(f"cannot determine VmId for {kind.value!r} payload")


def _as_spec(payload: object) -> VmSpec:
    from mgf.vm.core.contracts import VmSpec

    if isinstance(payload, VmSpec):
        return payload
    raise TransportError("VM_CREATE payload must be a VmSpec")


def _as_spec_and_vm(payload: object) -> tuple[VmSpec, VmId]:
    from mgf.vm.core.contracts import VmSpec

    if (
        isinstance(payload, tuple)
        and len(payload) == 2
        and isinstance(payload[0], VmId)
        and isinstance(payload[1], VmSpec)
    ):
        return payload[1], payload[0]
    raise TransportError("VM_REDEFINE payload must be (VmId, VmSpec)")


def _as_migrate(payload: object) -> MigrateVmPayload:
    if isinstance(payload, MigrateVmPayload):
        return payload
    raise TransportError("VM_MIGRATE payload must be a MigrateVmPayload")


def _as_vm_snapshot(payload: object) -> VmSnapshotPayload:
    if isinstance(payload, VmSnapshotPayload):
        return payload
    raise TransportError("VM_SNAPSHOT payload must be a VmSnapshotPayload")


def _as_vm_revert(payload: object) -> VmRevertSnapshotPayload:
    if isinstance(payload, VmRevertSnapshotPayload):
        return payload
    raise TransportError("VM_REVERT_SNAPSHOT payload must be a VmRevertSnapshotPayload")


def _as_vm_delete_snapshot(payload: object) -> VmDeleteSnapshotPayload:
    if isinstance(payload, VmDeleteSnapshotPayload):
        return payload
    raise TransportError("VM_DELETE_SNAPSHOT payload must be a VmDeleteSnapshotPayload")


def _force_of(payload: object) -> bool:
    return payload.force if isinstance(payload, PowerPayload) else False


def _delete_storage_of(payload: object) -> bool:
    return payload.delete_storage if isinstance(payload, DeletePayload) else False


def _observed_state(info: DomainInfo) -> VmState:
    """Map the engine's observed ``DomainInfo`` onto a neutral :class:`VmState`.

    Phase-0 ``DomainInfo`` carries only ``is_running`` (no paused flag — see
    ``_seed_state_machine``), so the observed status collapses to the two
    settled states the engine can report: ``RUNNING`` / ``STOPPED``. The richer
    lifecycle states (paused, transitional) are tracked by the per-VM state
    machine, not derivable from ``DomainInfo`` alone.
    """
    return VmState.RUNNING if info.is_running else VmState.STOPPED
