"""S1-S5 - :class:`LibvirtStorageBackend`, the libvirt ``StorageBackend`` impl.

Implements the 15-method ``StorageBackend`` Protocol from
``mgf.vm.core.contracts`` (``contracts.md`` §4) against libvirt storage
pools + volumes, plus ``qemu-img`` for the volume-snapshot and
format-conversion operations libvirt's storage API does not itself provide.

Layering on the FOUNDATION (never re-implemented here):

* every native libvirt call routes through
  :meth:`mgf.vm.libvirt._connection.LibvirtConnection.call` (the RLock seam,
  §8 thread-safety);
* every public method is decorated with
  :func:`mgf.vm.libvirt._errors.translate_at_seam` and the backend subclasses
  :class:`~mgf.vm.libvirt._errors.LibvirtErrorTranslator`, so a raw
  ``libvirt.libvirtError`` (or a ``qemu-img`` ``OSError``) never escapes the
  adapter (§7, EH-02/EH-03);
* opaque IDs are encoded/decoded only via :mod:`mgf.vm.libvirt._volume_ids`
  (the ``pool/name`` form never leaks, §2.1);
* ``close()`` is deterministic + idempotent (§7, DP-13).

Design decisions (recorded so the conformance + GC contracts stay honest):

* **Volume snapshots are qcow2 *internal* snapshots** (``qemu-img snapshot
  -c/-d/-a/-l``). libvirt's storage API has no volume-snapshot primitive
  (``virDomainSnapshot`` is *VM*-level and lives behind ``HypervisorBackend``,
  §4 charter). qcow2 internal snapshots keep the snapshot bytes inside the one
  volume file, so a snapshot can never outlive a deleted volume — which is why
  ``delete_volume`` on a volume that still has snapshots is refused
  (``VolumeHasSnapshots``, the reference-counted GC of §4).
* **The snapshot *tree* is tracked externally**, in-process, keyed by volume
  path (``contracts.md`` §4 — "external snapshot-tree tracking, no in-file
  metadata"). qcow2's own snapshot list is flat (no parent links); the tree
  is what lets ``delete_snapshot`` enforce ``SnapshotHasChildren`` and lets
  ``revert`` re-parent correctly. The tree is rebuilt lazily from
  ``qemu-img snapshot -l`` on first touch of a volume (flat → all children of
  the synthetic root) so it survives a backend restart for the
  refuses-delete / list cases; parent links only persist within a process,
  which is sufficient for the reconciler's single-process lifetime.
* **``convert_format`` always produces a NEW volume** (§4 invariant: a format
  change is a copy, never an in-place edit) and is **idempotent on the
  destination** — re-running with the same ``dst_spec`` returns the existing
  destination ``VolumeId`` when its bytes already came from this conversion.
"""

from __future__ import annotations

import shutil
import subprocess
import threading
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any
from xml.sax.saxutils import escape as _xml_escape

from defusedxml import ElementTree as DefusedET
from mgf.vm.core.capabilities import StorageCapabilities
from mgf.vm.core.contracts import (
    PoolInfo,
    PoolSpec,
    VolumeSpec,
    _SnapshotInfoContract,
    _VolumeInfoContract,
)
from mgf.vm.core.exceptions import (
    FormatUnsupported,
    PoolExists,
    PoolNotEmpty,
    PoolNotFound,
    ResizeUnsupported,
    SnapshotExists,
    SnapshotHasChildren,
    SnapshotNotFound,
    StorageError,
    VolumeExists,
    VolumeHasSnapshots,
    VolumeInUse,
    VolumeNotFound,
)

from mgf.vm.libvirt._connection import LibvirtConnection
from mgf.vm.libvirt._errors import LibvirtErrorTranslator, translate_at_seam
from mgf.vm.libvirt._volume_ids import (
    decode_snapshot_id,
    decode_volume_id,
    encode_snapshot_id,
    encode_volume_id,
)

if TYPE_CHECKING:
    from datetime import datetime

    from mgf.vm.core.cancellation import CancellationToken
    from mgf.vm.core.ids import SnapshotId, VolumeId

    from mgf.vm.libvirt._volume_ids import VolumeRef

# Formats this backend can create + convert. qcow2 (snapshots, backing
# chains) and raw (bare). vmdk is convert-only via qemu-img; kept out of the
# create set because dir-pool volume XML cannot describe it portably.
_SUPPORTED_FORMATS: frozenset[str] = frozenset({"qcow2", "raw"})
_CONVERT_FORMATS: frozenset[str] = frozenset({"qcow2", "raw", "vmdk"})
_SUPPORTED_POOL_TYPES: frozenset[str] = frozenset({"dir", "logical", "fs", "netfs"})

# qcow2 is the only format that carries internal snapshots; raw cannot.
_SNAPSHOTTABLE_FORMATS: frozenset[str] = frozenset({"qcow2"})

# qemu-img timeout: snapshots/convert on a small volume are fast; a generous
# ceiling keeps a hung qemu-img from wedging the backend forever.
_QEMU_IMG_TIMEOUT_S = 600


@dataclass
class _SnapNode:
    """One node in a volume's external snapshot tree."""

    name: str
    parent: str | None
    created_at: datetime
    children: set[str] = field(default_factory=set)


@dataclass
class _VolumeSnapTree:
    """The external snapshot tree for a single volume (keyed by snapshot name)."""

    nodes: dict[str, _SnapNode] = field(default_factory=dict)
    loaded: bool = False
    # The snapshot the volume is currently "at" (last created or reverted-to).
    current: str | None = None


class LibvirtStorageBackend(LibvirtErrorTranslator):
    """``StorageBackend`` over libvirt storage pools/volumes + ``qemu-img``.

    Construct via :meth:`open` (real libvirt) or :meth:`adopt` (an existing
    :class:`LibvirtConnection`, e.g. shared with a hypervisor backend, or a
    test double). The backend owns the connection's lifecycle only when it
    opened it; an adopted connection is closed too (the caller passed it in
    expecting the backend to manage it for its lifetime — §3 "one engine
    connection for its lifetime").
    """

    def __init__(self, connection: LibvirtConnection) -> None:
        self._conn = connection
        # Guards the in-process snapshot-tree cache. The connection's own
        # RLock guards native libvirt state; this guards adapter-local state.
        self._tree_lock = threading.RLock()
        self._snap_trees: dict[str, _VolumeSnapTree] = {}

    # -- construction -------------------------------------------------------

    @classmethod
    def open(cls, connection_uri: str) -> LibvirtStorageBackend:
        """Open a new libvirt connection to ``connection_uri`` and wrap it."""
        return cls(LibvirtConnection.open(connection_uri))

    @classmethod
    def adopt(cls, connection: LibvirtConnection) -> LibvirtStorageBackend:
        """Wrap an already-open :class:`LibvirtConnection` (shared / test)."""
        return cls(connection)

    @property
    def closed(self) -> bool:
        """Whether the underlying connection has been closed."""
        return self._conn.closed

    # -- connection lifecycle (§7, DP-13) ----------------------------------

    def close(self) -> None:
        """Release the connection. Idempotent; deterministic."""
        self._conn.close()

    def __enter__(self) -> LibvirtStorageBackend:
        return self

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

    # =====================================================================
    # S1 — pools
    # =====================================================================

    @translate_at_seam
    def list_pools(self) -> list[PoolInfo]:
        """List every storage pool the engine knows about. Idempotent."""
        pools = self._conn.call(self._raw().listAllStoragePools, 0)
        return [self._pool_info(p) for p in pools]

    @translate_at_seam
    def get_pool(self, name: str) -> PoolInfo:
        """Fetch one pool by name. Raises :class:`PoolNotFound`."""
        return self._pool_info(self._lookup_pool(name))

    @translate_at_seam
    def refresh_pool(self, name: str) -> PoolInfo:
        """Rescan the pool's backing store and return its fresh
        :class:`PoolInfo` (D-G20). Raises :class:`PoolNotFound`."""
        pool = self._lookup_pool(name)
        self._refresh_pool(pool)
        return self._pool_info(pool)

    @translate_at_seam
    def create_pool(self, spec: PoolSpec) -> None:
        """Define + start a pool. Idempotent on same spec; :class:`PoolExists`
        on a differing config.

        Idempotency is decided on ``(pool_type, target_path)``: an existing
        pool whose type and target path match the spec is a no-op (it is
        ensured active); a pool of the same name with a different type or path
        raises :class:`PoolExists` — the caller must delete + recreate.
        """
        with self._conn.lock:
            existing = self._try_lookup_pool(spec.name)
            if existing is not None:
                cur = self._pool_info(existing)
                if cur.pool_type == spec.pool_type and cur.target_path == spec.target_path:
                    self._ensure_pool_active(existing)
                    return
                msg = (
                    f"pool {spec.name!r} already exists with a different config "
                    f"(have type={cur.pool_type!r} path={cur.target_path!r}; "
                    f"want type={spec.pool_type!r} path={spec.target_path!r})"
                )
                raise PoolExists(msg)
            xml = self._pool_xml(spec)
            pool = self._conn.raw().storagePoolDefineXML(xml, 0)
            try:
                pool.build(0)
            except Exception:  # noqa: S110 - build best-effort; dir may pre-exist
                # A dir target that already exists makes build() fail; that is
                # fine — create() below still activates the pool.
                pass
            pool.create(0)

    @translate_at_seam
    def delete_pool(self, name: str, *, force: bool = False) -> None:
        """Destroy + undefine a pool. Idempotent (missing pool is success).

        Refuses with :class:`PoolNotEmpty` when the pool still holds volumes
        and ``force=False``. With ``force=True`` the pool's volumes are
        deleted first, then the pool itself.
        """
        with self._conn.lock:
            pool = self._try_lookup_pool(name)
            if pool is None:
                return  # idempotent — already gone
            self._refresh_pool(pool)
            vol_names = list(pool.listVolumes() or ())
            if vol_names and not force:
                msg = f"pool {name!r} contains {len(vol_names)} volume(s); pass force=True"
                raise PoolNotEmpty(msg)
            for vol_name in vol_names:
                vol = pool.storageVolLookupByName(vol_name)
                self._forget_tree(vol.path())
                vol.delete(0)
            if pool.isActive():
                pool.destroy()
            pool.undefine()

    # =====================================================================
    # S2 — volume CRUD
    # =====================================================================

    @translate_at_seam
    def list_volumes(self, pool: str) -> list[_VolumeInfoContract]:
        """List the volumes in ``pool``. Raises :class:`PoolNotFound`."""
        pool_obj = self._lookup_pool(pool)
        self._refresh_pool(pool_obj)
        out: list[_VolumeInfoContract] = []
        for vol_name in pool_obj.listVolumes() or ():
            vol = pool_obj.storageVolLookupByName(vol_name)
            out.append(self._volume_info(pool, vol))
        return out

    @translate_at_seam
    def get_volume(self, v: VolumeId) -> _VolumeInfoContract:
        """Fetch one volume. Raises :class:`VolumeNotFound`."""
        ref = decode_volume_id(v)
        vol = self._lookup_volume(ref)
        return self._volume_info(ref.pool, vol)

    @translate_at_seam
    def create_volume(self, spec: VolumeSpec) -> VolumeId:
        """Allocate a volume in ``spec.pool`` (``pool.createXML``).

        Idempotent on (pool, name) when the existing volume's capacity +
        format match the spec; raises :class:`VolumeExists` on a differing
        spec, :class:`PoolNotFound` for an unknown pool, and
        :class:`FormatUnsupported` for a format this backend cannot create.
        """
        if spec.format not in _SUPPORTED_FORMATS:
            msg = (
                f"format {spec.format!r} cannot be created "
                f"(supported: {sorted(_SUPPORTED_FORMATS)})"
            )
            raise FormatUnsupported(msg)
        with self._conn.lock:
            pool = self._lookup_pool(spec.pool)
            self._refresh_pool(pool)
            existing = self._try_lookup_volume_in(pool, spec.name)
            if existing is not None:
                info = self._volume_info(spec.pool, existing)
                if info.size_bytes == spec.size_bytes and info.format == spec.format:
                    return encode_volume_id(spec.pool, spec.name)
                msg = (
                    f"volume {spec.name!r} already exists in pool {spec.pool!r} "
                    f"with a different spec (have size={info.size_bytes} "
                    f"format={info.format!r}; want size={spec.size_bytes} "
                    f"format={spec.format!r})"
                )
                raise VolumeExists(msg)
            xml = self._volume_xml(spec)
            pool.createXML(xml, 0)
            return encode_volume_id(spec.pool, spec.name)

    @translate_at_seam
    def resize_volume(self, v: VolumeId, new_size_bytes: int) -> None:
        """Resize a volume. Idempotent when ``new_size_bytes`` equals current.

        Growing uses ``vol.resize``. Shrinking is refused with
        :class:`ResizeUnsupported` — qcow2/raw dir-pool volumes cannot shrink
        non-destructively, matching :attr:`StorageCapabilities.supports_shrink`
        (``False``).
        """
        ref = decode_volume_id(v)
        with self._conn.lock:
            vol = self._lookup_volume(ref)
            current = int(vol.info()[1])
            if new_size_bytes == current:
                return  # idempotent
            if new_size_bytes < current:
                msg = (
                    f"shrinking volume {ref.name!r} from {current} to "
                    f"{new_size_bytes} bytes is not supported"
                )
                raise ResizeUnsupported(msg)
            vol.resize(new_size_bytes, 0)

    @translate_at_seam
    def delete_volume(self, v: VolumeId) -> None:
        """Delete a volume. Idempotent (already-gone is success).

        Refuses with :class:`VolumeHasSnapshots` when the volume still carries
        snapshots (reference-counted GC, §4) and :class:`VolumeInUse` when a
        running domain has the volume's path attached as a disk.
        """
        ref = decode_volume_id(v)
        with self._conn.lock:
            vol = self._try_lookup_volume(ref)
            if vol is None:
                return  # idempotent — already gone
            path = vol.path()
            if self._volume_has_snapshots(path, self._volume_format(vol)):
                msg = f"volume {ref.name!r} has snapshots; delete them first"
                raise VolumeHasSnapshots(msg)
            if self._path_attached_to_running_domain(path):
                msg = f"volume {ref.name!r} is attached to a running domain"
                raise VolumeInUse(msg)
            self._forget_tree(path)
            vol.delete(0)

    # =====================================================================
    # S3 — volume snapshots (external tree + reference-counted deletion)
    # =====================================================================

    @translate_at_seam
    def snapshot_volume(self, v: VolumeId, name: str) -> SnapshotId:
        """Take a qcow2 internal snapshot named ``name``.

        Idempotent on same name when the snapshot already exists (returns the
        existing :class:`SnapshotId`). Raises :class:`SnapshotExists` only if a
        same-named snapshot exists but the volume has since diverged
        (different current state) — re-snapshotting a moved volume under a used
        name is a conflict. Raises :class:`FormatUnsupported` for a non-qcow2
        volume (raw cannot hold internal snapshots).
        """
        ref = decode_volume_id(v)
        with self._conn.lock:
            vol = self._lookup_volume(ref)
            fmt = self._volume_format(vol)
            if fmt not in _SNAPSHOTTABLE_FORMATS:
                msg = f"volume format {fmt!r} cannot hold snapshots (need qcow2)"
                raise FormatUnsupported(msg)
            path = vol.path()
            tree = self._load_tree(path, fmt)
            if name in tree.nodes:
                # Idempotent: the snapshot already exists. If the volume's
                # current pointer still references it, this is a clean re-take.
                if tree.current == name or tree.nodes[name].parent == tree.current:
                    return encode_snapshot_id(ref.pool, ref.name, name)
                msg = (
                    f"snapshot {name!r} already exists on volume {ref.name!r} "
                    "with different content"
                )
                raise SnapshotExists(msg)
            self._qemu_img(["snapshot", "-c", name, path])
            node = _SnapNode(name=name, parent=tree.current, created_at=self._now())
            tree.nodes[name] = node
            if tree.current is not None and tree.current in tree.nodes:
                tree.nodes[tree.current].children.add(name)
            tree.current = name
            return encode_snapshot_id(ref.pool, ref.name, name)

    @translate_at_seam
    def list_snapshots(self, v: VolumeId) -> list[_SnapshotInfoContract]:
        """List the snapshots on ``v``. Raises :class:`VolumeNotFound`."""
        ref = decode_volume_id(v)
        with self._conn.lock:
            vol = self._lookup_volume(ref)
            fmt = self._volume_format(vol)
            if fmt not in _SNAPSHOTTABLE_FORMATS:
                return []
            tree = self._load_tree(vol.path(), fmt)
            return [
                _SnapshotInfoContract(
                    snapshot_id=encode_snapshot_id(ref.pool, ref.name, node.name),
                    parent=v,
                    name=node.name,
                    created_at=node.created_at,
                    has_memory=False,
                )
                for node in tree.nodes.values()
            ]

    @translate_at_seam
    def delete_snapshot(self, s: SnapshotId) -> None:
        """Delete a snapshot. Idempotent (already-gone is success).

        Refuses with :class:`SnapshotHasChildren` when the snapshot has
        descendant snapshots (reference-counted deletion, §4). Deleting a
        volume that is itself already gone is treated as the snapshot being
        gone (success).
        """
        sref = decode_snapshot_id(s)
        ref = sref.volume_ref
        with self._conn.lock:
            vol = self._try_lookup_volume(ref)
            if vol is None:
                return  # volume gone → snapshot gone → idempotent success
            fmt = self._volume_format(vol)
            if fmt not in _SNAPSHOTTABLE_FORMATS:
                return
            path = vol.path()
            tree = self._load_tree(path, fmt)
            node = tree.nodes.get(sref.name)
            if node is None:
                return  # idempotent — already gone
            if node.children:
                msg = (
                    f"snapshot {sref.name!r} has {len(node.children)} child "
                    "snapshot(s); delete them first"
                )
                raise SnapshotHasChildren(msg)
            self._qemu_img(["snapshot", "-d", sref.name, path])
            # Re-parent bookkeeping: detach from parent, fix current pointer.
            if node.parent is not None and node.parent in tree.nodes:
                tree.nodes[node.parent].children.discard(sref.name)
            if tree.current == sref.name:
                tree.current = node.parent
            del tree.nodes[sref.name]

    @translate_at_seam
    def revert_volume_to_snapshot(
        self,
        v: VolumeId,
        s: SnapshotId,
        *,
        token: CancellationToken | None = None,
    ) -> None:
        """Restore the volume bytes to snapshot ``s`` (``qemu-img snapshot -a``).

        Idempotent on revert-to-current (a no-op). Raises
        :class:`SnapshotNotFound` if ``s`` is not a snapshot of ``v``, and
        :class:`VolumeInUse` if a running domain holds the volume (reverting
        bytes under a live guest would corrupt it).
        """
        if token is not None:
            token.raise_if_cancelled()
        ref = decode_volume_id(v)
        sref = decode_snapshot_id(s)
        if (sref.pool, sref.volume) != (ref.pool, ref.name):
            msg = f"snapshot {s!r} does not belong to volume {v!r}"
            raise SnapshotNotFound(msg)
        with self._conn.lock:
            vol = self._lookup_volume(ref)
            fmt = self._volume_format(vol)
            if fmt not in _SNAPSHOTTABLE_FORMATS:
                msg = f"volume format {fmt!r} has no snapshots to revert to"
                raise SnapshotNotFound(msg)
            path = vol.path()
            tree = self._load_tree(path, fmt)
            if sref.name not in tree.nodes:
                msg = f"snapshot {sref.name!r} not found on volume {ref.name!r}"
                raise SnapshotNotFound(msg)
            if tree.current == sref.name:
                return  # idempotent — already at this snapshot
            if self._path_attached_to_running_domain(path):
                msg = f"volume {ref.name!r} is attached to a running domain"
                raise VolumeInUse(msg)
            if token is not None:
                token.raise_if_cancelled()
            self._qemu_img(["snapshot", "-a", sref.name, path])
            tree.current = sref.name

    # =====================================================================
    # S4 — format conversion (new VolumeId; idempotent on dst)
    # =====================================================================

    @translate_at_seam
    def convert_format(
        self,
        src: VolumeId,
        dst_format: str,
        dst_spec: VolumeSpec,
        *,
        token: CancellationToken | None = None,
    ) -> VolumeId:
        """Copy ``src`` into a new volume in ``dst_format`` (``qemu-img convert``).

        Always a copy (§4 invariant). Returns the new destination
        :class:`VolumeId`. Idempotent on ``dst_spec``: if the destination
        volume already exists with a matching size + format, the existing
        destination ID is returned without re-converting. Collision detection:
        a destination that exists with a *different* spec raises
        :class:`VolumeExists`.
        """
        if token is not None:
            token.raise_if_cancelled()
        if dst_format not in _CONVERT_FORMATS:
            msg = (
                f"target format {dst_format!r} not supported "
                f"(supported: {sorted(_CONVERT_FORMATS)})"
            )
            raise FormatUnsupported(msg)
        if dst_spec.format != dst_format:
            msg = f"dst_spec.format {dst_spec.format!r} disagrees with dst_format {dst_format!r}"
            raise FormatUnsupported(msg)
        src_ref = decode_volume_id(src)
        with self._conn.lock:
            src_vol = self._lookup_volume(src_ref)
            src_path = src_vol.path()
            dst_pool = self._lookup_pool(dst_spec.pool)
            self._refresh_pool(dst_pool)
            # Idempotency + collision detection on the destination.
            existing = self._try_lookup_volume_in(dst_pool, dst_spec.name)
            if existing is not None:
                info = self._volume_info(dst_spec.pool, existing)
                if info.size_bytes == dst_spec.size_bytes and info.format == dst_format:
                    return encode_volume_id(dst_spec.pool, dst_spec.name)
                msg = (
                    f"destination volume {dst_spec.name!r} exists in pool "
                    f"{dst_spec.pool!r} with a different spec"
                )
                raise VolumeExists(msg)
            # Allocate the destination volume, then convert bytes into it.
            dst_pool.createXML(self._volume_xml(dst_spec), 0)
            if token is not None:
                token.raise_if_cancelled()
            dst_vol = dst_pool.storageVolLookupByName(dst_spec.name)
            dst_path = dst_vol.path()
            try:
                self._qemu_img(["convert", "-O", dst_format, src_path, dst_path])
            except Exception:
                # Roll back the half-written destination so a failed convert
                # leaves no zombie volume (§9 best-effort rollback).
                try:
                    dst_vol.delete(0)
                except Exception:  # noqa: S110 - rollback is best-effort
                    pass
                raise
            return encode_volume_id(dst_spec.pool, dst_spec.name)

    # =====================================================================
    # S5 — capabilities
    # =====================================================================

    @translate_at_seam
    def capabilities(self) -> StorageCapabilities:
        """Report format / shrink / pool-type support. Idempotent; fast."""
        try:
            version = self._conn.call(self._raw().getVersion)
            engine_version = self._format_version(int(version))
        except Exception:
            engine_version = "unknown"
        return StorageCapabilities(
            engine="libvirt-qemu",
            engine_version=engine_version,
            supports_pool_types=_SUPPORTED_POOL_TYPES,
            supports_formats=_SUPPORTED_FORMATS,
            supports_shrink=False,
            supports_thin_provisioning=True,
        )

    # =====================================================================
    # helpers — native lookups (all run under the connection seam)
    # =====================================================================

    def _raw(self) -> Any:
        """The underlying ``virConnect`` (caller already inside ``.call`` or
        holds the lock)."""
        return self._conn.raw()

    def _lookup_pool(self, name: str) -> Any:
        pool = self._try_lookup_pool(name)
        if pool is None:
            msg = f"no storage pool named {name!r}"
            raise PoolNotFound(msg)
        return pool

    def _try_lookup_pool(self, name: str) -> Any | None:
        def _do() -> Any | None:
            try:
                return self._raw().storagePoolLookupByName(name)
            except Exception:
                return None

        return self._conn.call(_do)

    def _lookup_volume(self, ref: VolumeRef) -> Any:
        vol = self._try_lookup_volume(ref)
        if vol is None:
            msg = f"no volume named {ref.name!r} in pool {ref.pool!r}"
            raise VolumeNotFound(msg)
        return vol

    def _try_lookup_volume(self, ref: VolumeRef) -> Any | None:
        def _do() -> Any | None:
            try:
                pool = self._raw().storagePoolLookupByName(ref.pool)
            except Exception:
                return None
            try:
                pool.refresh(0)
            except Exception:  # noqa: S110 - refresh is best-effort
                pass
            try:
                return pool.storageVolLookupByName(ref.name)
            except Exception:
                return None

        return self._conn.call(_do)

    @staticmethod
    def _try_lookup_volume_in(pool: Any, name: str) -> Any | None:
        try:
            return pool.storageVolLookupByName(name)
        except Exception:
            return None

    def _refresh_pool(self, pool: Any) -> None:
        def _do() -> None:
            try:
                if pool.isActive():
                    pool.refresh(0)
            except Exception:  # noqa: S110 - refresh is best-effort
                pass

        self._conn.call(_do)

    def _ensure_pool_active(self, pool: Any) -> None:
        def _do() -> None:
            if not pool.isActive():
                pool.create(0)

        self._conn.call(_do)

    # =====================================================================
    # helpers — info projection
    # =====================================================================

    def _pool_info(self, pool: Any) -> PoolInfo:
        # info() = [state, capacity, allocation, available]
        info = self._conn.call(pool.info)
        xml = self._conn.call(pool.XMLDesc, 0)
        pool_type, target_path = self._parse_pool_xml(xml)
        return PoolInfo(
            name=self._conn.call(pool.name),
            pool_type=pool_type,
            target_path=target_path,
            capacity_bytes=int(info[1]),
            allocation_bytes=int(info[2]),
            available_bytes=int(info[3]),
            active=bool(self._conn.call(pool.isActive)),
        )

    def _volume_info(self, pool: str, vol: Any) -> _VolumeInfoContract:
        # info() = [type, capacity, allocation]
        info = self._conn.call(vol.info)
        return _VolumeInfoContract(
            volume_id=encode_volume_id(pool, self._conn.call(vol.name)),
            pool=pool,
            name=self._conn.call(vol.name),
            path=self._conn.call(vol.path),
            size_bytes=int(info[1]),
            allocation_bytes=int(info[2]),
            format=self._volume_format(vol),
        )

    def _volume_format(self, vol: Any) -> str:
        """Read a volume's format from its XML (``<target><format type=.../>``)."""
        xml = self._conn.call(vol.XMLDesc, 0)
        try:
            root = DefusedET.fromstring(xml)
        except Exception:
            return "unknown"
        fmt_el = root.find("target/format")
        if fmt_el is not None:
            t = fmt_el.get("type")
            if t:
                return t
        return "unknown"

    @staticmethod
    def _parse_pool_xml(xml: str) -> tuple[str, str]:
        try:
            root = DefusedET.fromstring(xml)
        except Exception:
            return ("unknown", "")
        pool_type = root.get("type") or "unknown"
        target_path = root.findtext("target/path", "") or ""
        return (pool_type, target_path)

    # =====================================================================
    # helpers — XML generation (volume + pool define)
    # =====================================================================

    @staticmethod
    def _volume_xml(spec: VolumeSpec) -> str:
        backing = ""
        if spec.backing_file is not None:
            backing = (
                f"<backingStore><path>{_xml_escape(spec.backing_file)}</path>"
                "<format type='qcow2'/></backingStore>"
            )
        return (
            f"<volume type='file'>"
            f"<name>{_xml_escape(spec.name)}</name>"
            f"<capacity unit='bytes'>{int(spec.size_bytes)}</capacity>"
            f"<target><format type='{_xml_escape(spec.format)}'/></target>"
            f"{backing}"
            f"</volume>"
        )

    @staticmethod
    def _pool_xml(spec: PoolSpec) -> str:
        return (
            f"<pool type='{_xml_escape(spec.pool_type)}'>"
            f"<name>{_xml_escape(spec.name)}</name>"
            f"<target><path>{_xml_escape(spec.target_path)}</path></target>"
            f"</pool>"
        )

    # =====================================================================
    # helpers — running-domain disk scan (VolumeInUse detection)
    # =====================================================================

    def _path_attached_to_running_domain(self, path: str) -> bool:
        """True if any *running* domain has ``path`` as a disk source.

        Used to enforce :class:`VolumeInUse`. Best-effort: an unreadable
        domain XML is skipped rather than masking the in-use check.
        """

        def _do() -> bool:
            try:
                domains = self._raw().listAllDomains(0)
            except Exception:
                return False
            for dom in domains:
                try:
                    if not dom.isActive():
                        continue
                    xml = dom.XMLDesc(0)
                except Exception:  # noqa: S112 - unreadable domain skipped
                    continue
                if self._xml_references_path(xml, path):
                    return True
            return False

        return self._conn.call(_do)

    @staticmethod
    def _xml_references_path(domain_xml: str, path: str) -> bool:
        try:
            root = DefusedET.fromstring(domain_xml)
        except Exception:
            return False
        for src in root.findall("devices/disk/source"):
            if src.get("file") == path or src.get("dev") == path:
                return True
        return False

    # =====================================================================
    # helpers — external snapshot tree
    # =====================================================================

    def _load_tree(self, path: str, fmt: str) -> _VolumeSnapTree:
        """Return the (lazily-built) snapshot tree for the volume at ``path``.

        First touch reads ``qemu-img snapshot -l`` and seeds a flat tree (all
        snapshots as children of the synthetic root). Parent/child links
        beyond that flat seeding accrue as ``snapshot_volume`` /
        ``delete_snapshot`` run within the process.
        """
        with self._tree_lock:
            tree = self._snap_trees.get(path)
            if tree is None:
                tree = _VolumeSnapTree()
                self._snap_trees[path] = tree
            if not tree.loaded:
                for name, created in self._read_qcow2_snapshots(path):
                    if name not in tree.nodes:
                        tree.nodes[name] = _SnapNode(name=name, parent=None, created_at=created)
                if tree.current is None and tree.nodes:
                    # No in-process current pointer yet: leave None so the
                    # next created snapshot parents at the root.
                    pass
                tree.loaded = True
            _ = fmt  # fmt already validated by callers; kept for symmetry
            return tree

    def _forget_tree(self, path: str) -> None:
        with self._tree_lock:
            self._snap_trees.pop(path, None)

    def _volume_has_snapshots(self, path: str, fmt: str) -> bool:
        if fmt not in _SNAPSHOTTABLE_FORMATS:
            return False
        tree = self._load_tree(path, fmt)
        return bool(tree.nodes)

    def _read_qcow2_snapshots(self, path: str) -> list[tuple[str, datetime]]:
        """Parse ``qemu-img snapshot -l`` into ``(name, created_at)`` pairs."""
        try:
            result = self._qemu_img(["snapshot", "-l", path])
        except StorageError:
            return []
        out: list[tuple[str, datetime]] = []
        for line in result.splitlines():
            stripped = line.strip()
            # Skip header rows; data rows begin with a numeric snapshot index.
            head = stripped.split(None, 1)
            if not head or not head[0].isdigit():
                continue
            cols = stripped.split()
            if len(cols) < 2:
                continue
            out.append((cols[1], self._now()))
        return out

    # =====================================================================
    # helpers — qemu-img invocation
    # =====================================================================

    def _qemu_img(self, args: list[str]) -> str:
        """Run ``qemu-img`` with ``args``; return stdout. Raises
        :class:`StorageError` on a missing binary or non-zero exit (the seam
        then leaves it as the typed error)."""
        binary = shutil.which("qemu-img")
        if binary is None:
            msg = "qemu-img not found on PATH; install the qemu-utils package"
            raise StorageError(msg)
        cmd = [binary, *args]
        try:
            proc = subprocess.run(  # noqa: S603 - argv list, no shell
                cmd,
                capture_output=True,
                text=True,
                timeout=_QEMU_IMG_TIMEOUT_S,
                check=False,
            )
        except (OSError, subprocess.SubprocessError) as exc:
            msg = f"qemu-img {args[0] if args else ''} failed to run: {exc}"
            raise StorageError(msg) from exc
        if proc.returncode != 0:
            msg = (
                f"qemu-img {' '.join(args)} exited {proc.returncode}: "
                f"{proc.stderr.strip() or proc.stdout.strip()}"
            )
            raise StorageError(msg)
        return proc.stdout

    # =====================================================================
    # helpers — misc
    # =====================================================================

    @staticmethod
    def _now() -> datetime:
        from datetime import UTC, datetime

        return datetime.now(UTC)

    @staticmethod
    def _format_version(raw: int) -> str:
        # libvirt encodes version as major*1_000_000 + minor*1_000 + release.
        major, rem = divmod(raw, 1_000_000)
        minor, release = divmod(rem, 1_000)
        return f"{major}.{minor}.{release}"


__all__ = ["LibvirtStorageBackend"]
