"""The public client surface — ``VmManagerClient`` + ``AsyncVmManagerClient``.

Two separate classes (the httpx pattern; locked decision), NOT one class with
``a``-prefixed methods:

* :class:`VmManagerClient` — synchronous. Blocking calls; context-manager via
  ``with``; :meth:`~VmManagerClient.close`.
* :class:`AsyncVmManagerClient` — asynchronous. ``await`` calls; async
  context-manager via ``async with``; :meth:`~AsyncVmManagerClient.aclose`.

The two surfaces are **parallel by construction**: same method names, same
parameters, sync returns the domain type / async mirrors with ``async def`` +
``await``. CLI and GUI import only from here (D-H4).

Each client is constructed with a :class:`~mgf.vm.sdk.transport.base.Transport`
(resp. ``AsyncTransport``) — the transport is the only thing that knows whether
work happens in-process or over gRPC. The client owns the two-step collapse:

* **reads** (``get_*`` / ``list_*`` / ``capabilities``) → ``transport.read(query)``,
  return the data directly (``api.md`` §4.2 — reads carry no op handle).
* **mutations** (``create_vm``, ``start_vm``, …) → ``transport.submit_op(...)``
  then ``transport.watch_op(...)`` to a terminal handle; unpack
  ``handle.result`` on ``SUCCEEDED`` or **re-raise** ``handle.error`` (the
  original core exception — no re-wrapping; ``api.md`` §7) on ``FAILED``.

Both surfaces are fully wired (B1-B7): the sync client runs the two-step
collapse against a :class:`~mgf.vm.sdk.transport.base.Transport`; the async
client mirrors it against an :class:`~mgf.vm.sdk.transport.base.AsyncTransport`
(``async def`` + ``await``; the in-process async transport offloads the
blocking core to a thread pool). The async unpack logic reuses
:meth:`VmManagerClient._unpack` verbatim — status handling is identical, so it
is not duplicated.
"""

from __future__ import annotations

import uuid
from typing import TYPE_CHECKING, TypeVar

from mgf.vm.core.contracts import OperationKind, OperationStatus

from mgf.vm.sdk.queries import (
    BackupVmPayload,
    BridgeForDomain,
    Capabilities,
    ClaimDevicePayload,
    CloneVmPayload,
    ConvertFormatPayload,
    CreatePoolPayload,
    CreateVolumePayload,
    DeletePayload,
    DeletePoolPayload,
    DeleteVolumePayload,
    DeleteVolumeSnapshotPayload,
    DetachDevicePayload,
    GetAutostart,
    GetDevice,
    GetGuestInterfaces,
    GetGuestOsInfo,
    GetPool,
    GetVm,
    GetVolume,
    GuestPing,
    ListDevices,
    ListLeases,
    ListNetworks,
    ListOperations,
    ListPools,
    ListVms,
    ListVmSnapshots,
    ListVolumes,
    ListVolumeSnapshots,
    MigrateVmPayload,
    NetworkLeasesQuery,
    PowerPayload,
    RefreshPool,
    ReleaseDevicePayload,
    RenamePayload,
    ResizeVolumePayload,
    RestoreVmPayload,
    RevertVolumeSnapshotPayload,
    SampleMetrics,
    SetAutostartPayload,
    SnapshotVolumePayload,
    TransferLeasePayload,
    VmAttachDevicePayload,
    VmDeleteSnapshotPayload,
    VmRevertSnapshotPayload,
    VmSnapshotPayload,
)
from mgf.vm.sdk.queries import (
    DeviceCapabilities as DeviceCapabilitiesQuery,
)
from mgf.vm.sdk.queries import (
    StorageCapabilities as StorageCapabilitiesQuery,
)

if TYPE_CHECKING:
    from collections.abc import AsyncIterator, Iterator
    from types import TracebackType

    from mgf.vm.core.capabilities import (
        DeviceCapabilities,
        HypervisorCapabilities,
        StorageCapabilities,
    )
    from mgf.vm.core.contracts import (
        BackupManifest,
        DeviceInfo,
        EventGap,
        GuestNic,
        GuestOsInfo,
        LeaseInfo,
        MetricsSample,
        MigrationPlan,
        MigrationTarget,
        NetworkLease,
        NetworkProfile,
        NetworkSpec,
        OperationHandle,
        PoolInfo,
        PoolSpec,
        Vm,
        VmEvent,
        VmSpec,
        VolumeSpec,
        _SnapshotInfoContract,
        _VolumeInfoContract,
    )
    from mgf.vm.core.ids import (
        DeviceId,
        LeaseId,
        OperationId,
        SnapshotId,
        VmId,
        VolumeId,
    )

    from mgf.vm.sdk.transport.base import AsyncTransport, Transport

__all__ = ["AsyncVmManagerClient", "VmManagerClient"]

_T = TypeVar("_T")


def _new_client_op_id() -> str:
    """Generate a fresh dedup key for a mutating op (``api.md`` §4.2; D-G5)."""
    return str(uuid.uuid4())


def _cast(value: object) -> _T:  # type: ignore[type-var]
    """Narrow a type-erased transport return at the call site.

    The transport seam returns ``object`` (``api.md`` §4.2 — reads carry no
    static type across the wire-neutral boundary). Each client method knows the
    concrete return type from the RPC it invoked; this is the single, audited
    place that narrowing happens. It is an identity cast — the backend already
    produced the right runtime type.
    """
    return value  # type: ignore[return-value]


# ===========================================================================
# Synchronous client
# ===========================================================================


class VmManagerClient:
    """The synchronous VManager client (``api.md`` §4).

    Construct with a :class:`~mgf.vm.sdk.transport.base.Transport`; the surface
    above is transport-agnostic. ``client_id`` (optional) identifies this client
    for audit / dedup-namespacing; it defaults to a fresh UUID.

    Use as a context manager to guarantee :meth:`close`::

        with VmManagerClient(transport) as client:
            vm = client.get_vm(vm_id)
    """

    def __init__(self, transport: Transport, *, client_id: str | None = None) -> None:
        self._transport = transport
        self._client_id = client_id or _new_client_op_id()

    @property
    def client_id(self) -> str:
        """This client's stable identity (for audit / dedup namespacing)."""
        return self._client_id

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

    def close(self) -> None:
        """Close the underlying transport. Idempotent."""
        self._transport.close()

    def __enter__(self) -> VmManagerClient:
        return self

    def __exit__(
        self,
        exc_type: type[BaseException] | None,
        exc: BaseException | None,
        tb: TracebackType | None,
    ) -> None:
        self.close()

    # -- two-step collapse (api.md §4.2) ----------------------------------

    def _run(self, kind: OperationKind, payload: object) -> OperationHandle:
        """Submit a mutating op and drive it to a terminal handle.

        Mints a fresh ``client_op_id`` (D-G5), submits, then watches the op to
        a terminal status. Returns the terminal handle; the per-method caller
        unpacks ``result`` or re-raises ``error`` via :meth:`_unpack`.
        """
        client_op_id = _new_client_op_id()
        handle = self._transport.submit_op(kind, payload, client_op_id=client_op_id)
        if handle.status.is_terminal:
            return handle
        terminal = handle
        for transition in self._transport.watch_op(handle.op_id):
            terminal = transition
        return terminal

    @staticmethod
    def _unpack(handle: OperationHandle) -> object:
        """Return ``handle.result`` on success; re-raise the typed error.

        ``api.md`` §7: the ``FAILED`` handle carries the ORIGINAL core
        exception — re-raised verbatim, no re-wrapping. ``CANCELLED`` surfaces
        as :class:`~mgf.vm.core.exceptions.OperationCancelled`.
        """
        if handle.status is OperationStatus.SUCCEEDED:
            return handle.result
        if handle.status is OperationStatus.FAILED and handle.error is not None:
            raise handle.error
        if handle.status is OperationStatus.CANCELLED:
            from mgf.vm.core.exceptions import OperationCancelled

            raise OperationCancelled(f"operation {handle.op_id} was cancelled")
        # Defensive: a terminal handle is always one of the above.
        from mgf.vm.core.exceptions import TransportError

        raise TransportError(f"operation {handle.op_id} ended in {handle.status.value!r}")

    # ======================================================================
    # Reads — transport.read(query); return data directly (api.md §4.2)
    # ======================================================================

    def get_vm(self, vm_id: VmId) -> Vm:
        """Fetch one VM as the canonical :class:`Vm` aggregate (``GetVm``; D-G12).

        Raises ``DomainNotFound`` if absent. The status is the live engine-
        observed state; the spec is best-effort (the submitted spec when known
        to the transport, else a minimal reconstruction — see
        :class:`~mgf.vm.sdk.transport.inprocess.InProcessTransport`).
        """
        result = self._transport.read(GetVm(vm_id))
        return _cast(result)

    def list_vms(self) -> list[Vm]:
        """List all VMs as :class:`Vm` aggregates (``ListVms``; D-G12)."""
        result = self._transport.read(ListVms())
        return _cast(result)

    def get_volume(self, volume_id: VolumeId) -> _VolumeInfoContract:
        """Fetch one volume. Raises ``VolumeNotFound`` if absent."""
        return _cast(self._transport.read(GetVolume(volume_id)))

    def list_volumes(self, pool: str) -> list[_VolumeInfoContract]:
        """List volumes in ``pool``."""
        return _cast(self._transport.read(ListVolumes(pool)))

    def get_pool(self, name: str) -> PoolInfo:
        """Fetch one storage pool. Raises ``PoolNotFound`` if absent."""
        return _cast(self._transport.read(GetPool(name)))

    def refresh_pool(self, name: str) -> PoolInfo:
        """Rescan a pool's backing store and return its fresh info
        (``RefreshPool``; D-G20). Raises ``PoolNotFound`` if absent."""
        return _cast(self._transport.read(RefreshPool(name)))

    def list_pools(self) -> list[PoolInfo]:
        """List all storage pools."""
        return _cast(self._transport.read(ListPools()))

    def list_snapshots(self, vm_id: VmId) -> list[_SnapshotInfoContract]:
        """List snapshots for a VM (``ListSnapshots``)."""
        return _cast(self._transport.read(ListVmSnapshots(vm_id)))

    def list_volume_snapshots(self, volume_id: VolumeId) -> list[_SnapshotInfoContract]:
        """List snapshots for a volume."""
        return _cast(self._transport.read(ListVolumeSnapshots(volume_id)))

    def get_device(self, device_id: DeviceId) -> DeviceInfo:
        """Fetch one host device. Raises ``DeviceNotFound`` if absent."""
        return _cast(self._transport.read(GetDevice(device_id)))

    def list_devices(self) -> list[DeviceInfo]:
        """List assignable host devices (``ListDevices``)."""
        return _cast(self._transport.read(ListDevices()))

    def list_leases(self) -> list[LeaseInfo]:
        """List currently-held device leases."""
        return _cast(self._transport.read(ListLeases()))

    def get_operation(self, op_id: OperationId) -> OperationHandle:
        """Fetch one operation's handle (``GetOperation``).

        Reads the op registry via the transport; raises a ``TransportError``
        subclass if ``op_id`` is unknown to the target.
        """
        return _cast(self._transport.get_op(op_id))

    def list_operations(self) -> list[OperationHandle]:
        """List known operation handles (``ListOperations``)."""
        return _cast(self._transport.read(ListOperations()))

    def capabilities(self) -> HypervisorCapabilities:
        """Report hypervisor capabilities for this target."""
        return _cast(self._transport.read(Capabilities()))

    def storage_capabilities(self) -> StorageCapabilities:
        """Report storage-backend capabilities."""
        return _cast(self._transport.read(StorageCapabilitiesQuery()))

    def device_capabilities(self) -> DeviceCapabilities:
        """Report device-backend capabilities."""
        return _cast(self._transport.read(DeviceCapabilitiesQuery()))

    # -- Wave-B observation reads (D-G13 / D-G14) -------------------------

    def list_networks(self) -> list[NetworkProfile]:
        """List the host virtual networks the engine knows about (D-G14).

        Read served by the :class:`~mgf.vm.core.contracts.HypervisorBackend`'s
        ``list_networks`` (host-network observation lives on the hypervisor —
        no separate backend). Idempotent.
        """
        return _cast(self._transport.read(ListNetworks()))

    def network_leases(self, network: str) -> list[NetworkLease]:
        """List observed DHCP leases on ``network`` by name (``network_leases``).

        An unknown network has no observed leases (empty list). Idempotent.
        """
        return _cast(self._transport.read(NetworkLeasesQuery(network)))

    def bridge_for_domain(self, vm_id: VmId) -> str | None:
        """Resolve the host bridge ``vm_id``'s interface is on, or ``None``.

        Resolves the VM's ``DomainId`` like the other VM-keyed reads and
        delegates to the hypervisor's ``bridge_for_domain``. ``None`` when the
        VM has no bridged interface or is unknown. Idempotent.
        """
        return _cast(self._transport.read(BridgeForDomain(vm_id)))

    def sample_metrics(self, vm_id: VmId) -> MetricsSample:
        """Sample one per-domain :class:`MetricsSample` for ``vm_id`` (D-G13).

        Routed to the injected :class:`~mgf.vm.core.contracts.MetricsBackend`.
        Raises a :class:`~mgf.vm.core.exceptions.TransportError` when no metrics
        backend is configured on the transport (it does NOT silently return an
        empty sample); raises ``DomainNotFound`` when the VM is absent.
        """
        return _cast(self._transport.read(SampleMetrics(vm_id)))

    def get_autostart(self, vm_id: VmId) -> bool:
        """Return ``vm_id``'s host-boot autostart flag (``GetAutostart``; D-G25).

        Routed to the injected :class:`~mgf.vm.core.contracts.DomainPolicyBackend`.
        Raises ``TransportError`` when no policy backend is configured; raises
        ``DomainNotFound`` when the VM is absent.
        """
        return _cast(self._transport.read(GetAutostart(vm_id)))

    def set_autostart(self, vm_id: VmId, *, enabled: bool) -> None:
        """Set ``vm_id``'s host-boot autostart flag (``VM_SET_AUTOSTART``; D-G25)."""
        self._unpack(self._run(OperationKind.VM_SET_AUTOSTART, SetAutostartPayload(vm_id, enabled)))

    def managed_save(self, vm_id: VmId) -> None:
        """Managed-save a running VM to disk + stop it (``VM_MANAGED_SAVE``; D-G25).

        Routed to the injected ``DomainPolicyBackend``; raises ``OperationError``
        when the VM is not running.
        """
        self._unpack(self._run(OperationKind.VM_MANAGED_SAVE, vm_id))

    def guest_ping(self, vm_id: VmId) -> bool:
        """Probe the in-guest agent's liveness for ``vm_id`` (D-G19).

        Routed to the injected
        :class:`~mgf.vm.core.contracts.GuestAgentBackend`; returns ``False``
        when the agent is unreachable (not an error). Raises a
        :class:`~mgf.vm.core.exceptions.TransportError` when no guest-agent
        backend is configured; ``DomainNotFound`` when the VM is absent.
        """
        return _cast(self._transport.read(GuestPing(vm_id)))

    def guest_os_info(self, vm_id: VmId) -> GuestOsInfo:
        """Read the guest's OS identification for ``vm_id`` (D-G19).

        Routed to the injected ``GuestAgentBackend``. Raises
        ``GuestUnreachableError`` when the agent is unreachable, a
        ``TransportError`` when no guest-agent backend is configured, and
        ``DomainNotFound`` when the VM is absent.
        """
        return _cast(self._transport.read(GetGuestOsInfo(vm_id)))

    def guest_interfaces(self, vm_id: VmId) -> tuple[GuestNic, ...]:
        """Read the guest-reported NICs for ``vm_id`` (D-G19).

        Routed to the injected ``GuestAgentBackend``. Raises
        ``GuestUnreachableError`` when the agent is unreachable, a
        ``TransportError`` when no guest-agent backend is configured, and
        ``DomainNotFound`` when the VM is absent.
        """
        return _cast(self._transport.read(GetGuestInterfaces(vm_id)))

    # ======================================================================
    # Resource watches — transport.watch_stream (server stream; api.md §8.1)
    # ======================================================================

    def watch_vm(self, vm_id: VmId | None = None) -> Iterator[VmEvent | EventGap]:
        """Stream lifecycle events for a VM (or all VMs) until closed (``WatchVm``).

        ``vm_id`` is optional: ``None`` subscribes to every domain. Yields
        :class:`~mgf.vm.core.contracts.VmEvent` in causal per-domain order
        (``api.md`` §8.1). An :class:`~mgf.vm.core.contracts.EventGap` is yielded
        — not swallowed — when the target's retention window elapsed and the
        caller must reconcile (it missed events). The client is a pass-through:
        callers iterate and reconcile on a gap, then continue from the next
        event. Closing the iterator (``close()`` / breaking the loop) releases
        the subscription deterministically (DP-13).
        """
        return self._transport.watch_stream(vm_id)

    # ======================================================================
    # Mutations — submit_op → watch_op → unpack result / re-raise error
    # ======================================================================

    # -- VM lifecycle (HypervisorBackend) ---------------------------------

    def create_vm(self, spec: VmSpec) -> Vm:
        """Create a VM from ``spec``, returning the canonical :class:`Vm` (D-G12).

        ``CreateVm`` / VM_CREATE. Once the op resolves ``SUCCEEDED`` the
        transport assembles the :class:`Vm` — the submitted ``spec`` paired with
        the engine-observed :class:`~mgf.vm.core.contracts.VmStatus` — and the
        client narrows ``handle.result`` to it (D-G11 / D-G12).
        """
        return _cast(self._unpack(self._run(OperationKind.VM_CREATE, spec)))

    def redefine_vm(self, vm_id: VmId, spec: VmSpec) -> Vm:
        """Replace a stopped VM's definition (``UpdateVm``; VM_REDEFINE).

        Returns the canonical :class:`Vm` carrying the new desired-state spec.
        """
        return _cast(self._unpack(self._run(OperationKind.VM_REDEFINE, (vm_id, spec))))

    def delete_vm(self, vm_id: VmId, *, delete_storage: bool = False) -> None:
        """Delete a VM (``DeleteVm``; VM_UNDEFINE). Idempotent."""
        self._unpack(
            self._run(
                OperationKind.VM_UNDEFINE,
                DeletePayload(vm_id, delete_storage=delete_storage),
            )
        )

    def rename_vm(self, vm_id: VmId, new_name: str) -> None:
        """Rename a stopped VM to ``new_name`` (``VM_RENAME``; D-G26).

        The VM's identity (``vm_id``) is stable across the rename. Raises
        ``DomainStateInvalid`` if the VM is running, ``DomainAlreadyExists`` if
        ``new_name`` is taken, ``DomainNotFound`` if absent.
        """
        self._unpack(self._run(OperationKind.VM_RENAME, RenamePayload(vm_id, new_name)))

    def start_vm(self, vm_id: VmId) -> None:
        """Start a VM (``StartVm``; VM_START). Idempotent."""
        self._unpack(self._run(OperationKind.VM_START, vm_id))

    def stop_vm(self, vm_id: VmId, *, force: bool = False) -> None:
        """Stop a VM (``StopVm``; VM_STOP). Graceful unless ``force``."""
        self._unpack(self._run(OperationKind.VM_STOP, PowerPayload(vm_id, force=force)))

    def pause_vm(self, vm_id: VmId) -> None:
        """Pause a VM (``PauseVm``; VM_PAUSE). Idempotent."""
        self._unpack(self._run(OperationKind.VM_PAUSE, vm_id))

    def resume_vm(self, vm_id: VmId) -> None:
        """Resume a paused VM (``ResumeVm``; VM_RESUME). Idempotent."""
        self._unpack(self._run(OperationKind.VM_RESUME, vm_id))

    def reboot_vm(self, vm_id: VmId, *, force: bool = False) -> None:
        """Reboot a VM (``RestartVm``; VM_REBOOT)."""
        self._unpack(self._run(OperationKind.VM_REBOOT, PowerPayload(vm_id, force=force)))

    def migrate_vm(
        self,
        vm_id: VmId,
        dest: MigrationTarget,
        *,
        plan: MigrationPlan,
    ) -> None:
        """Live-migrate a VM to ``dest`` (``MigrateVm``; VM_MIGRATE).

        Executes the CP's pre-computed ``plan`` against the hypervisor's
        ``migrate``. Atomic: on failure the source is left running and the
        original ``MigrationFailed`` / ``MigrationCancelled`` is re-raised
        (``contracts.md`` §3 ``migrate``).
        """
        self._unpack(
            self._run(
                OperationKind.VM_MIGRATE,
                MigrateVmPayload(vm_id, dest, plan),
            )
        )

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

    def backup_vm(self, vm_id: VmId, dest: str) -> BackupManifest:
        """Back up a VM's disks to the archive at ``dest`` (``VM_BACKUP``).

        Archives each disk image via the configured
        :class:`~mgf.vm.core.contracts.HostDiskBackend` and carries the VM's
        serialized :class:`~mgf.vm.core.contracts.VmSpec` in the manifest so a
        later :meth:`restore_vm` is self-contained (D-G18). Returns the
        :class:`~mgf.vm.core.contracts.BackupManifest`. Raises ``TransportError``
        if no host-disk backend is configured; ``DomainNotFound`` if the VM is
        absent; ``BackupError`` on an archive failure.
        """
        return _cast(self._unpack(self._run(OperationKind.VM_BACKUP, BackupVmPayload(vm_id, dest))))

    def clone_vm(
        self,
        vm_id: VmId,
        new_name: str,
        *,
        dest_dir: str,
        linked: bool = False,
    ) -> Vm:
        """Clone a VM into ``new_name`` (``VM_CLONE``); return the new :class:`Vm`.

        Copies (full) or overlays (``linked``) each disk image into ``dest_dir``
        and defines the clone from a spec carrying the new disk sources —
        engine-neutral, no native-format rewrite (D-G18). Raises
        ``TransportError`` if no host-disk backend is configured;
        ``DomainNotFound`` if the source is absent; ``DiskOperationError`` on a
        copy/overlay failure.
        """
        return _cast(
            self._unpack(
                self._run(
                    OperationKind.VM_CLONE,
                    CloneVmPayload(vm_id, new_name, dest_dir, linked=linked),
                )
            )
        )

    def restore_vm(
        self,
        archive: str,
        *,
        dest_dir: str,
        new_name: str | None = None,
    ) -> Vm:
        """Restore a VM from a backup ``archive`` (``VM_RESTORE``); return the :class:`Vm`.

        Extracts + checksum-verifies the archive into ``dest_dir``, reconstructs
        the carried :class:`~mgf.vm.core.contracts.VmSpec`, repoints its disk
        sources at the restored images, and defines the VM under ``new_name``
        (or the backed-up name). Raises ``TransportError`` if no host-disk
        backend is configured or the archive carries no spec; ``BackupError`` on
        a checksum mismatch.
        """
        return _cast(
            self._unpack(
                self._run(
                    OperationKind.VM_RESTORE,
                    RestoreVmPayload(archive, dest_dir, new_name=new_name),
                )
            )
        )

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

    def define_network(self, spec: NetworkSpec) -> NetworkProfile:
        """Define a virtual network from ``spec`` (``NETWORK_DEFINE``).

        Defines but does not start it (follow with :meth:`start_network`).
        Returns the observed :class:`~mgf.vm.core.contracts.NetworkProfile`.
        Raises ``TransportError`` if no network backend is configured;
        ``NetworkExists`` on a conflicting redefine.
        """
        return _cast(self._unpack(self._run(OperationKind.NETWORK_DEFINE, spec)))

    def start_network(self, name: str) -> None:
        """Activate the network ``name`` (``NETWORK_START``). Idempotent.
        Raises ``NetworkNotFound`` if it is not defined."""
        self._unpack(self._run(OperationKind.NETWORK_START, name))

    def stop_network(self, name: str) -> None:
        """Deactivate the network ``name`` (``NETWORK_STOP``). Idempotent.
        Raises ``NetworkNotFound`` if it is not defined."""
        self._unpack(self._run(OperationKind.NETWORK_STOP, name))

    def delete_network(self, name: str) -> None:
        """Undefine the network ``name`` (``NETWORK_DELETE``). Idempotent —
        an already-absent network is a no-op success."""
        self._unpack(self._run(OperationKind.NETWORK_DELETE, name))

    # -- VM snapshots ------------------------------------------------------

    def snapshot_vm(
        self,
        vm_id: VmId,
        name: str,
        *,
        include_memory: bool = False,
    ) -> SnapshotId:
        """Snapshot a VM (``SnapshotVm``; VM_SNAPSHOT)."""
        return _cast(
            self._unpack(
                self._run(
                    OperationKind.VM_SNAPSHOT,
                    VmSnapshotPayload(vm_id, name, include_memory=include_memory),
                )
            )
        )

    def revert_snapshot(self, vm_id: VmId, snapshot_id: SnapshotId) -> None:
        """Revert a VM to a snapshot (``RevertSnapshot``; VM_REVERT_SNAPSHOT)."""
        self._unpack(
            self._run(
                OperationKind.VM_REVERT_SNAPSHOT,
                VmRevertSnapshotPayload(vm_id, snapshot_id),
            )
        )

    def delete_snapshot(self, vm_id: VmId, snapshot_id: SnapshotId) -> None:
        """Delete a VM snapshot (``DeleteSnapshot``; VM_DELETE_SNAPSHOT)."""
        self._unpack(
            self._run(
                OperationKind.VM_DELETE_SNAPSHOT,
                VmDeleteSnapshotPayload(vm_id, snapshot_id),
            )
        )

    # -- device passthrough (hypervisor-side) -----------------------------

    def attach_device(
        self,
        vm_id: VmId,
        device_id: DeviceId,
        *,
        live: bool = True,
    ) -> None:
        """Attach a leased device to a VM (``AttachDevice``; VM_ATTACH_DEVICE).

        Runs the attach transaction (claim → attach → record) with rollback on
        failure (``contracts.md`` §5).
        """
        self._unpack(
            self._run(
                OperationKind.VM_ATTACH_DEVICE,
                VmAttachDevicePayload(vm_id, device_id, live=live),
            )
        )

    def detach_device(
        self,
        vm_id: VmId,
        device_id: DeviceId,
        *,
        live: bool = True,
    ) -> None:
        """Detach a device from a VM (``DetachDevice``; VM_DETACH_DEVICE).

        Mirrors the attach transaction in reverse (hypervisor detach → release).
        A failed hypervisor detach raises ``DeviceDetachFailed`` and does NOT
        release the lease (needs-human; ``contracts.md`` §5).
        """
        self._unpack(
            self._run(
                OperationKind.VM_DETACH_DEVICE,
                DetachDevicePayload(vm_id, device_id, live=live),
            )
        )

    # -- volume + pool lifecycle (StorageBackend) -------------------------

    def create_volume(self, spec: VolumeSpec) -> VolumeId:
        """Create a volume (VOLUME_CREATE)."""
        return _cast(
            self._unpack(self._run(OperationKind.VOLUME_CREATE, CreateVolumePayload(spec)))
        )

    def resize_volume(self, volume_id: VolumeId, new_size_bytes: int) -> None:
        """Resize a volume (VOLUME_RESIZE)."""
        self._unpack(
            self._run(
                OperationKind.VOLUME_RESIZE,
                ResizeVolumePayload(volume_id, new_size_bytes),
            )
        )

    def delete_volume(self, volume_id: VolumeId) -> None:
        """Delete a volume (VOLUME_DELETE). Idempotent."""
        self._unpack(self._run(OperationKind.VOLUME_DELETE, DeleteVolumePayload(volume_id)))

    def snapshot_volume(self, volume_id: VolumeId, name: str) -> SnapshotId:
        """Snapshot a volume (VOLUME_SNAPSHOT)."""
        return _cast(
            self._unpack(
                self._run(
                    OperationKind.VOLUME_SNAPSHOT,
                    SnapshotVolumePayload(volume_id, name),
                )
            )
        )

    def revert_volume_snapshot(
        self,
        volume_id: VolumeId,
        snapshot_id: SnapshotId,
    ) -> None:
        """Revert a volume to a snapshot (VOLUME_REVERT)."""
        self._unpack(
            self._run(
                OperationKind.VOLUME_REVERT,
                RevertVolumeSnapshotPayload(volume_id, snapshot_id),
            )
        )

    def delete_volume_snapshot(self, snapshot_id: SnapshotId) -> None:
        """Delete a volume snapshot.

        Rides ``VOLUME_DELETE`` (the contract gives volume-snapshot delete no
        dedicated kind — the ``SnapshotId`` identifies it); the transport
        disambiguates on the payload type.
        """
        self._unpack(
            self._run(
                OperationKind.VOLUME_DELETE,
                DeleteVolumeSnapshotPayload(snapshot_id),
            )
        )

    def convert_format(
        self,
        src: VolumeId,
        dst_format: str,
        dst_spec: VolumeSpec,
    ) -> VolumeId:
        """Convert a volume to a new format (VOLUME_CONVERT)."""
        return _cast(
            self._unpack(
                self._run(
                    OperationKind.VOLUME_CONVERT,
                    ConvertFormatPayload(src, dst_format, dst_spec),
                )
            )
        )

    def create_pool(self, spec: PoolSpec) -> None:
        """Create a storage pool (POOL_CREATE)."""
        self._unpack(self._run(OperationKind.POOL_CREATE, CreatePoolPayload(spec)))

    def delete_pool(self, name: str, *, force: bool = False) -> None:
        """Delete a storage pool (POOL_DELETE)."""
        self._unpack(self._run(OperationKind.POOL_DELETE, DeletePoolPayload(name, force=force)))

    # -- device leases (DeviceBackend) ------------------------------------

    def claim_device(self, device_id: DeviceId) -> LeaseId:
        """Claim a device lease (DEVICE_CLAIM)."""
        return _cast(
            self._unpack(self._run(OperationKind.DEVICE_CLAIM, ClaimDevicePayload(device_id)))
        )

    def release_device(self, lease_id: LeaseId) -> None:
        """Release a device lease (DEVICE_RELEASE). Idempotent."""
        self._unpack(self._run(OperationKind.DEVICE_RELEASE, ReleaseDevicePayload(lease_id)))

    def transfer_lease(self, lease_id: LeaseId, dest_host: str) -> LeaseId:
        """Transfer a device lease to another host (DEVICE_TRANSFER)."""
        return _cast(
            self._unpack(
                self._run(
                    OperationKind.DEVICE_TRANSFER,
                    TransferLeasePayload(lease_id, dest_host),
                )
            )
        )


# ===========================================================================
# Asynchronous client — parallel surface (same names/params; async def + await)
# ===========================================================================


class AsyncVmManagerClient:
    """The asynchronous VManager client — async mirror of :class:`VmManagerClient`.

    Construct with an :class:`~mgf.vm.sdk.transport.base.AsyncTransport`. Use as
    an async context manager to guarantee :meth:`aclose`::

        async with AsyncVmManagerClient(transport) as client:
            vm = await client.get_vm(vm_id)
    """

    def __init__(
        self,
        transport: AsyncTransport,
        *,
        client_id: str | None = None,
    ) -> None:
        self._transport = transport
        self._client_id = client_id or _new_client_op_id()

    @property
    def client_id(self) -> str:
        """This client's stable identity (for audit / dedup namespacing)."""
        return self._client_id

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

    async def aclose(self) -> None:
        """Close the underlying transport. Idempotent."""
        await self._transport.aclose()

    async def __aenter__(self) -> AsyncVmManagerClient:
        return self

    async def __aexit__(
        self,
        exc_type: type[BaseException] | None,
        exc: BaseException | None,
        tb: TracebackType | None,
    ) -> None:
        await self.aclose()

    # -- two-step collapse (api.md §4.2) — async mirror -------------------

    async def _run(self, kind: OperationKind, payload: object) -> OperationHandle:
        """Submit a mutating op and async-drive it to a terminal handle.

        Mirrors :meth:`VmManagerClient._run`: mint a ``client_op_id`` (D-G5),
        ``await`` the submit, then ``async for`` the op's transitions to a
        terminal status. Returns the terminal handle; the per-method caller
        unpacks via :meth:`VmManagerClient._unpack` (status logic is identical —
        reused, not duplicated).
        """
        client_op_id = _new_client_op_id()
        handle = await self._transport.submit_op(kind, payload, client_op_id=client_op_id)
        if handle.status.is_terminal:
            return handle
        terminal = handle
        async for transition in self._transport.watch_op(handle.op_id):
            terminal = transition
        return terminal

    # ======================================================================
    # Reads
    # ======================================================================

    async def get_vm(self, vm_id: VmId) -> Vm:
        """Fetch one VM as the canonical :class:`Vm` aggregate (``GetVm``; D-G12).

        Raises ``DomainNotFound`` if absent. Async mirror of
        :meth:`VmManagerClient.get_vm` — same best-effort spec semantics.
        """
        return _cast(await self._transport.read(GetVm(vm_id)))

    async def list_vms(self) -> list[Vm]:
        """List all VMs as :class:`Vm` aggregates (``ListVms``; D-G12)."""
        return _cast(await self._transport.read(ListVms()))

    async def get_volume(self, volume_id: VolumeId) -> _VolumeInfoContract:
        """Fetch one volume. Raises ``VolumeNotFound`` if absent."""
        return _cast(await self._transport.read(GetVolume(volume_id)))

    async def list_volumes(self, pool: str) -> list[_VolumeInfoContract]:
        """List volumes in ``pool``."""
        return _cast(await self._transport.read(ListVolumes(pool)))

    async def get_pool(self, name: str) -> PoolInfo:
        """Fetch one storage pool. Raises ``PoolNotFound`` if absent."""
        return _cast(await self._transport.read(GetPool(name)))

    async def refresh_pool(self, name: str) -> PoolInfo:
        """Rescan a pool's backing store and return its fresh info
        (``RefreshPool``; D-G20). Raises ``PoolNotFound`` if absent."""
        return _cast(await self._transport.read(RefreshPool(name)))

    async def list_pools(self) -> list[PoolInfo]:
        """List all storage pools."""
        return _cast(await self._transport.read(ListPools()))

    async def list_snapshots(self, vm_id: VmId) -> list[_SnapshotInfoContract]:
        """List snapshots for a VM (``ListSnapshots``)."""
        return _cast(await self._transport.read(ListVmSnapshots(vm_id)))

    async def list_volume_snapshots(
        self,
        volume_id: VolumeId,
    ) -> list[_SnapshotInfoContract]:
        """List snapshots for a volume."""
        return _cast(await self._transport.read(ListVolumeSnapshots(volume_id)))

    async def get_device(self, device_id: DeviceId) -> DeviceInfo:
        """Fetch one host device. Raises ``DeviceNotFound`` if absent."""
        return _cast(await self._transport.read(GetDevice(device_id)))

    async def list_devices(self) -> list[DeviceInfo]:
        """List assignable host devices (``ListDevices``)."""
        return _cast(await self._transport.read(ListDevices()))

    async def list_leases(self) -> list[LeaseInfo]:
        """List currently-held device leases."""
        return _cast(await self._transport.read(ListLeases()))

    async def get_operation(self, op_id: OperationId) -> OperationHandle:
        """Fetch one operation's handle (``GetOperation``)."""
        return _cast(await self._transport.get_op(op_id))

    async def list_operations(self) -> list[OperationHandle]:
        """List known operation handles (``ListOperations``)."""
        return _cast(await self._transport.read(ListOperations()))

    async def capabilities(self) -> HypervisorCapabilities:
        """Report hypervisor capabilities for this target."""
        return _cast(await self._transport.read(Capabilities()))

    async def storage_capabilities(self) -> StorageCapabilities:
        """Report storage-backend capabilities."""
        return _cast(await self._transport.read(StorageCapabilitiesQuery()))

    async def device_capabilities(self) -> DeviceCapabilities:
        """Report device-backend capabilities."""
        return _cast(await self._transport.read(DeviceCapabilitiesQuery()))

    # -- Wave-B observation reads (D-G13 / D-G14) — async mirror ----------

    async def list_networks(self) -> list[NetworkProfile]:
        """List host virtual networks (D-G14). Async mirror of
        :meth:`VmManagerClient.list_networks`."""
        return _cast(await self._transport.read(ListNetworks()))

    async def network_leases(self, network: str) -> list[NetworkLease]:
        """List observed DHCP leases on ``network`` (``network_leases``).

        Async mirror of :meth:`VmManagerClient.network_leases`.
        """
        return _cast(await self._transport.read(NetworkLeasesQuery(network)))

    async def bridge_for_domain(self, vm_id: VmId) -> str | None:
        """Resolve ``vm_id``'s host bridge, or ``None`` (``bridge_for_domain``).

        Async mirror of :meth:`VmManagerClient.bridge_for_domain`.
        """
        return _cast(await self._transport.read(BridgeForDomain(vm_id)))

    async def sample_metrics(self, vm_id: VmId) -> MetricsSample:
        """Sample one per-domain :class:`MetricsSample` for ``vm_id`` (D-G13).

        Async mirror of :meth:`VmManagerClient.sample_metrics`: routed to the
        injected :class:`~mgf.vm.core.contracts.MetricsBackend`; raises a
        :class:`~mgf.vm.core.exceptions.TransportError` when none is configured.
        """
        return _cast(await self._transport.read(SampleMetrics(vm_id)))

    async def get_autostart(self, vm_id: VmId) -> bool:
        """Return ``vm_id``'s host-boot autostart flag (``GetAutostart``; D-G25).

        Async mirror of :meth:`VmManagerClient.get_autostart`: routed to the
        injected ``DomainPolicyBackend``; raises ``TransportError`` when none is
        configured.
        """
        return _cast(await self._transport.read(GetAutostart(vm_id)))

    async def set_autostart(self, vm_id: VmId, *, enabled: bool) -> None:
        """Set ``vm_id``'s host-boot autostart flag (``VM_SET_AUTOSTART``; D-G25)."""
        VmManagerClient._unpack(
            await self._run(OperationKind.VM_SET_AUTOSTART, SetAutostartPayload(vm_id, enabled))
        )

    async def managed_save(self, vm_id: VmId) -> None:
        """Managed-save a running VM to disk + stop it (``VM_MANAGED_SAVE``; D-G25)."""
        VmManagerClient._unpack(await self._run(OperationKind.VM_MANAGED_SAVE, vm_id))

    async def guest_ping(self, vm_id: VmId) -> bool:
        """Probe the in-guest agent's liveness for ``vm_id`` (D-G19).

        Async mirror of :meth:`VmManagerClient.guest_ping`.
        """
        return _cast(await self._transport.read(GuestPing(vm_id)))

    async def guest_os_info(self, vm_id: VmId) -> GuestOsInfo:
        """Read the guest's OS identification for ``vm_id`` (D-G19).

        Async mirror of :meth:`VmManagerClient.guest_os_info`.
        """
        return _cast(await self._transport.read(GetGuestOsInfo(vm_id)))

    async def guest_interfaces(self, vm_id: VmId) -> tuple[GuestNic, ...]:
        """Read the guest-reported NICs for ``vm_id`` (D-G19).

        Async mirror of :meth:`VmManagerClient.guest_interfaces`.
        """
        return _cast(await self._transport.read(GetGuestInterfaces(vm_id)))

    # ======================================================================
    # Resource watches
    # ======================================================================

    def watch_vm(self, vm_id: VmId | None = None) -> AsyncIterator[VmEvent | EventGap]:
        """Stream lifecycle events for a VM (or all VMs) until closed (``WatchVm``).

        Returns an ``AsyncIterator`` (not a coroutine): ``async for ev in
        client.watch_vm(vm_id): ...``. ``vm_id=None`` subscribes to every
        domain. :class:`~mgf.vm.core.contracts.EventGap` items pass through —
        the caller reconciles. Closing the async iterator releases the
        subscription (DP-13).
        """
        return self._transport.watch_stream(vm_id)

    # ======================================================================
    # Mutations
    # ======================================================================

    async def create_vm(self, spec: VmSpec) -> Vm:
        """Create a VM from ``spec``, returning the canonical :class:`Vm` (D-G12).

        Async mirror of :meth:`VmManagerClient.create_vm`.
        """
        return _cast(VmManagerClient._unpack(await self._run(OperationKind.VM_CREATE, spec)))

    async def redefine_vm(self, vm_id: VmId, spec: VmSpec) -> Vm:
        """Replace a stopped VM's definition (``UpdateVm``; VM_REDEFINE).

        Returns the canonical :class:`Vm` carrying the new desired-state spec.
        """
        return _cast(
            VmManagerClient._unpack(await self._run(OperationKind.VM_REDEFINE, (vm_id, spec)))
        )

    async def delete_vm(self, vm_id: VmId, *, delete_storage: bool = False) -> None:
        """Delete a VM (``DeleteVm``; VM_UNDEFINE). Idempotent."""
        VmManagerClient._unpack(
            await self._run(
                OperationKind.VM_UNDEFINE,
                DeletePayload(vm_id, delete_storage=delete_storage),
            )
        )

    async def rename_vm(self, vm_id: VmId, new_name: str) -> None:
        """Rename a stopped VM to ``new_name`` (``VM_RENAME``; D-G26)."""
        VmManagerClient._unpack(
            await self._run(OperationKind.VM_RENAME, RenamePayload(vm_id, new_name))
        )

    async def start_vm(self, vm_id: VmId) -> None:
        """Start a VM (``StartVm``; VM_START). Idempotent."""
        VmManagerClient._unpack(await self._run(OperationKind.VM_START, vm_id))

    async def stop_vm(self, vm_id: VmId, *, force: bool = False) -> None:
        """Stop a VM (``StopVm``; VM_STOP). Graceful unless ``force``."""
        VmManagerClient._unpack(
            await self._run(OperationKind.VM_STOP, PowerPayload(vm_id, force=force))
        )

    async def pause_vm(self, vm_id: VmId) -> None:
        """Pause a VM (``PauseVm``; VM_PAUSE). Idempotent."""
        VmManagerClient._unpack(await self._run(OperationKind.VM_PAUSE, vm_id))

    async def resume_vm(self, vm_id: VmId) -> None:
        """Resume a paused VM (``ResumeVm``; VM_RESUME). Idempotent."""
        VmManagerClient._unpack(await self._run(OperationKind.VM_RESUME, vm_id))

    async def reboot_vm(self, vm_id: VmId, *, force: bool = False) -> None:
        """Reboot a VM (``RestartVm``; VM_REBOOT)."""
        VmManagerClient._unpack(
            await self._run(OperationKind.VM_REBOOT, PowerPayload(vm_id, force=force))
        )

    async def migrate_vm(
        self,
        vm_id: VmId,
        dest: MigrationTarget,
        *,
        plan: MigrationPlan,
    ) -> None:
        """Live-migrate a VM to ``dest`` (``MigrateVm``; VM_MIGRATE).

        Atomic: on failure the source is left running and the original
        ``MigrationFailed`` / ``MigrationCancelled`` is re-raised.
        """
        VmManagerClient._unpack(
            await self._run(OperationKind.VM_MIGRATE, MigrateVmPayload(vm_id, dest, plan))
        )

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

    async def backup_vm(self, vm_id: VmId, dest: str) -> BackupManifest:
        """Back up a VM's disks to ``dest`` (``VM_BACKUP``); see the sync twin."""
        return _cast(
            VmManagerClient._unpack(
                await self._run(OperationKind.VM_BACKUP, BackupVmPayload(vm_id, dest))
            )
        )

    async def clone_vm(
        self,
        vm_id: VmId,
        new_name: str,
        *,
        dest_dir: str,
        linked: bool = False,
    ) -> Vm:
        """Clone a VM into ``new_name`` (``VM_CLONE``); see the sync twin."""
        return _cast(
            VmManagerClient._unpack(
                await self._run(
                    OperationKind.VM_CLONE,
                    CloneVmPayload(vm_id, new_name, dest_dir, linked=linked),
                )
            )
        )

    async def restore_vm(
        self,
        archive: str,
        *,
        dest_dir: str,
        new_name: str | None = None,
    ) -> Vm:
        """Restore a VM from ``archive`` (``VM_RESTORE``); see the sync twin."""
        return _cast(
            VmManagerClient._unpack(
                await self._run(
                    OperationKind.VM_RESTORE,
                    RestoreVmPayload(archive, dest_dir, new_name=new_name),
                )
            )
        )

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

    async def define_network(self, spec: NetworkSpec) -> NetworkProfile:
        """Define a virtual network (``NETWORK_DEFINE``); see the sync twin."""
        return _cast(VmManagerClient._unpack(await self._run(OperationKind.NETWORK_DEFINE, spec)))

    async def start_network(self, name: str) -> None:
        """Activate the network ``name`` (``NETWORK_START``); see the sync twin."""
        VmManagerClient._unpack(await self._run(OperationKind.NETWORK_START, name))

    async def stop_network(self, name: str) -> None:
        """Deactivate the network ``name`` (``NETWORK_STOP``); see the sync twin."""
        VmManagerClient._unpack(await self._run(OperationKind.NETWORK_STOP, name))

    async def delete_network(self, name: str) -> None:
        """Undefine the network ``name`` (``NETWORK_DELETE``); see the sync twin."""
        VmManagerClient._unpack(await self._run(OperationKind.NETWORK_DELETE, name))

    async def snapshot_vm(
        self,
        vm_id: VmId,
        name: str,
        *,
        include_memory: bool = False,
    ) -> SnapshotId:
        """Snapshot a VM (``SnapshotVm``; VM_SNAPSHOT)."""
        return _cast(
            VmManagerClient._unpack(
                await self._run(
                    OperationKind.VM_SNAPSHOT,
                    VmSnapshotPayload(vm_id, name, include_memory=include_memory),
                )
            )
        )

    async def revert_snapshot(self, vm_id: VmId, snapshot_id: SnapshotId) -> None:
        """Revert a VM to a snapshot (``RevertSnapshot``; VM_REVERT_SNAPSHOT)."""
        VmManagerClient._unpack(
            await self._run(
                OperationKind.VM_REVERT_SNAPSHOT,
                VmRevertSnapshotPayload(vm_id, snapshot_id),
            )
        )

    async def delete_snapshot(self, vm_id: VmId, snapshot_id: SnapshotId) -> None:
        """Delete a VM snapshot (``DeleteSnapshot``; VM_DELETE_SNAPSHOT)."""
        VmManagerClient._unpack(
            await self._run(
                OperationKind.VM_DELETE_SNAPSHOT,
                VmDeleteSnapshotPayload(vm_id, snapshot_id),
            )
        )

    async def attach_device(
        self,
        vm_id: VmId,
        device_id: DeviceId,
        *,
        live: bool = True,
    ) -> None:
        """Attach a leased device to a VM (``AttachDevice``; VM_ATTACH_DEVICE)."""
        VmManagerClient._unpack(
            await self._run(
                OperationKind.VM_ATTACH_DEVICE,
                VmAttachDevicePayload(vm_id, device_id, live=live),
            )
        )

    async def detach_device(
        self,
        vm_id: VmId,
        device_id: DeviceId,
        *,
        live: bool = True,
    ) -> None:
        """Detach a device from a VM (``DetachDevice``; VM_DETACH_DEVICE)."""
        VmManagerClient._unpack(
            await self._run(
                OperationKind.VM_DETACH_DEVICE,
                DetachDevicePayload(vm_id, device_id, live=live),
            )
        )

    async def create_volume(self, spec: VolumeSpec) -> VolumeId:
        """Create a volume (VOLUME_CREATE)."""
        return _cast(
            VmManagerClient._unpack(
                await self._run(OperationKind.VOLUME_CREATE, CreateVolumePayload(spec))
            )
        )

    async def resize_volume(self, volume_id: VolumeId, new_size_bytes: int) -> None:
        """Resize a volume (VOLUME_RESIZE)."""
        VmManagerClient._unpack(
            await self._run(
                OperationKind.VOLUME_RESIZE,
                ResizeVolumePayload(volume_id, new_size_bytes),
            )
        )

    async def delete_volume(self, volume_id: VolumeId) -> None:
        """Delete a volume (VOLUME_DELETE). Idempotent."""
        VmManagerClient._unpack(
            await self._run(OperationKind.VOLUME_DELETE, DeleteVolumePayload(volume_id))
        )

    async def snapshot_volume(self, volume_id: VolumeId, name: str) -> SnapshotId:
        """Snapshot a volume (VOLUME_SNAPSHOT)."""
        return _cast(
            VmManagerClient._unpack(
                await self._run(
                    OperationKind.VOLUME_SNAPSHOT,
                    SnapshotVolumePayload(volume_id, name),
                )
            )
        )

    async def revert_volume_snapshot(
        self,
        volume_id: VolumeId,
        snapshot_id: SnapshotId,
    ) -> None:
        """Revert a volume to a snapshot (VOLUME_REVERT)."""
        VmManagerClient._unpack(
            await self._run(
                OperationKind.VOLUME_REVERT,
                RevertVolumeSnapshotPayload(volume_id, snapshot_id),
            )
        )

    async def delete_volume_snapshot(self, snapshot_id: SnapshotId) -> None:
        """Delete a volume snapshot (rides ``VOLUME_DELETE``)."""
        VmManagerClient._unpack(
            await self._run(
                OperationKind.VOLUME_DELETE,
                DeleteVolumeSnapshotPayload(snapshot_id),
            )
        )

    async def convert_format(
        self,
        src: VolumeId,
        dst_format: str,
        dst_spec: VolumeSpec,
    ) -> VolumeId:
        """Convert a volume to a new format (VOLUME_CONVERT)."""
        return _cast(
            VmManagerClient._unpack(
                await self._run(
                    OperationKind.VOLUME_CONVERT,
                    ConvertFormatPayload(src, dst_format, dst_spec),
                )
            )
        )

    async def create_pool(self, spec: PoolSpec) -> None:
        """Create a storage pool (POOL_CREATE)."""
        VmManagerClient._unpack(await self._run(OperationKind.POOL_CREATE, CreatePoolPayload(spec)))

    async def delete_pool(self, name: str, *, force: bool = False) -> None:
        """Delete a storage pool (POOL_DELETE)."""
        VmManagerClient._unpack(
            await self._run(OperationKind.POOL_DELETE, DeletePoolPayload(name, force=force))
        )

    async def claim_device(self, device_id: DeviceId) -> LeaseId:
        """Claim a device lease (DEVICE_CLAIM)."""
        return _cast(
            VmManagerClient._unpack(
                await self._run(OperationKind.DEVICE_CLAIM, ClaimDevicePayload(device_id))
            )
        )

    async def release_device(self, lease_id: LeaseId) -> None:
        """Release a device lease (DEVICE_RELEASE). Idempotent."""
        VmManagerClient._unpack(
            await self._run(OperationKind.DEVICE_RELEASE, ReleaseDevicePayload(lease_id))
        )

    async def transfer_lease(self, lease_id: LeaseId, dest_host: str) -> LeaseId:
        """Transfer a device lease to another host (DEVICE_TRANSFER)."""
        return _cast(
            VmManagerClient._unpack(
                await self._run(
                    OperationKind.DEVICE_TRANSFER,
                    TransferLeasePayload(lease_id, dest_host),
                )
            )
        )
