"""S0 — opaque ``VolumeId`` / ``SnapshotId`` codecs for the libvirt storage backend.

A libvirt storage volume is addressed by the pair ``(pool-name, volume-name)``
— there is no single global handle the way a domain has a UUID. The
:class:`~mgf.vm.core.ids.VolumeId` the contract hands callers must stay
**opaque** (``contracts.md`` §2.1): callers never parse it, and the libvirt
``pool/name`` form never leaks above the adapter as a structured value.

This module is the *only* place that knows the internal encoding. It packs
the pair into the opaque string and unpacks it back, so every other method in
:mod:`mgf.vm.libvirt._storage` works in terms of the typed wrapper plus a
small decoded record and never does ad-hoc string surgery on an ID.

Encoding choice — ``lvpool:<pool>/<name>`` with percent-escaping of the
delimiters inside each component so a pool or volume name containing ``/`` or
``:`` round-trips losslessly. The ``lvpool:`` prefix is a self-describing tag:
if an ID issued by a *different* backend (a raw-path adapter, Firecracker)
is fed in by mistake, :func:`decode_volume_id` rejects it with a typed
:class:`~mgf.vm.core.exceptions.VolumeNotFound` rather than mis-parsing it
(the cross-engine isolation rule, ``contracts.md`` §2.1).

Snapshots are volume-scoped: a :class:`~mgf.vm.core.ids.SnapshotId` carries
the owning volume's ``(pool, name)`` *plus* the snapshot name, so the backend
can resolve the parent volume from the snapshot ID alone (needed by
``delete_snapshot(s)``, which takes only the snapshot).
"""

from __future__ import annotations

from dataclasses import dataclass
from urllib.parse import quote, unquote

from mgf.vm.core.exceptions import SnapshotNotFound, VolumeNotFound
from mgf.vm.core.ids import SnapshotId, VolumeId

_VOLUME_TAG = "lvpool"
_SNAPSHOT_TAG = "lvsnap"
# Characters that are structural in the encoding; everything else passes
# through quote() untouched so the common case (plain names) stays readable.
_SAFE = ""  # quote() escapes "/" and ":" because they are not in _SAFE


@dataclass(frozen=True, slots=True)
class VolumeRef:
    """Decoded form of a :class:`VolumeId`: the libvirt ``(pool, name)`` pair."""

    pool: str
    name: str


@dataclass(frozen=True, slots=True)
class SnapshotRef:
    """Decoded form of a :class:`SnapshotId`: owning volume + snapshot name."""

    pool: str
    volume: str
    name: str

    @property
    def volume_ref(self) -> VolumeRef:
        """The owning volume's :class:`VolumeRef`."""
        return VolumeRef(pool=self.pool, name=self.volume)


def encode_volume_id(pool: str, name: str) -> VolumeId:
    """Pack ``(pool, name)`` into an opaque :class:`VolumeId`."""
    return VolumeId(f"{_VOLUME_TAG}:{quote(pool, safe=_SAFE)}/{quote(name, safe=_SAFE)}")


def decode_volume_id(v: VolumeId) -> VolumeRef:
    """Unpack a :class:`VolumeId` back to ``(pool, name)``.

    Raises :class:`VolumeNotFound` if ``v`` was not issued by this backend
    (wrong tag / malformed) — an ID this backend cannot interpret refers to
    no volume *here*, which is exactly what ``VolumeNotFound`` means.
    """
    raw = v.value
    prefix = f"{_VOLUME_TAG}:"
    if not raw.startswith(prefix):
        msg = f"{v!r} is not a libvirt volume id"
        raise VolumeNotFound(msg)
    body = raw[len(prefix) :]
    pool, sep, name = body.partition("/")
    if not sep:
        msg = f"{v!r} is a malformed libvirt volume id (no pool/name separator)"
        raise VolumeNotFound(msg)
    return VolumeRef(pool=unquote(pool), name=unquote(name))


def encode_snapshot_id(pool: str, volume: str, name: str) -> SnapshotId:
    """Pack a volume-scoped snapshot coordinate into an opaque :class:`SnapshotId`."""
    return SnapshotId(
        f"{_SNAPSHOT_TAG}:{quote(pool, safe=_SAFE)}/"
        f"{quote(volume, safe=_SAFE)}/{quote(name, safe=_SAFE)}"
    )


def decode_snapshot_id(s: SnapshotId) -> SnapshotRef:
    """Unpack a :class:`SnapshotId` back to ``(pool, volume, name)``.

    Raises :class:`SnapshotNotFound` for an ID not issued by this backend.
    """
    raw = s.value
    prefix = f"{_SNAPSHOT_TAG}:"
    if not raw.startswith(prefix):
        msg = f"{s!r} is not a libvirt snapshot id"
        raise SnapshotNotFound(msg)
    body = raw[len(prefix) :]
    parts = body.split("/")
    if len(parts) != 3:
        msg = f"{s!r} is a malformed libvirt snapshot id"
        raise SnapshotNotFound(msg)
    pool, volume, name = (unquote(p) for p in parts)
    return SnapshotRef(pool=pool, volume=volume, name=name)


__all__ = [
    "SnapshotRef",
    "VolumeRef",
    "decode_snapshot_id",
    "decode_volume_id",
    "encode_snapshot_id",
    "encode_volume_id",
]
