"""Storage-domain handler — ``StorageBackend`` volume + pool orchestration (B4).

The :class:`~mgf.vm.sdk.transport.inprocess.InProcessTransport` keeps
``_dispatch`` a thin router; the volume + pool + volume-snapshot mutation
logic lives here so the transport stays readable (the plan envisioned
per-domain handlers).

Every handler takes the resolved
:class:`~mgf.vm.core.contracts.StorageBackend` and the op payload, calls the
backend, and returns the op-specific success value (a ``VolumeId``, a
``SnapshotId``, or ``None``). The backend's typed errors
(``VolumeNotFound`` / ``VolumeHasSnapshots`` / ``PoolNotEmpty`` / …) propagate
verbatim — the transport's error bridge attaches the ORIGINAL exception to the
``FAILED`` handle (``api.md`` §7). Idempotency + the reference-counted-delete /
GC contract (``contracts.md`` §4) are the backend's responsibility; the SDK
does not second-guess them.

These ops are **resource-scoped**, not VM-scoped: they carry no ``VmId`` and
take no per-VM lock. The backend is responsible for its own thread safety
(``contracts.md`` §8).
"""

from __future__ import annotations

from typing import TYPE_CHECKING, TypeVar

from mgf.vm.core.contracts import OperationKind
from mgf.vm.core.exceptions import TransportError

from mgf.vm.sdk.queries import (
    ConvertFormatPayload,
    CreatePoolPayload,
    CreateVolumePayload,
    DeletePoolPayload,
    DeleteVolumePayload,
    DeleteVolumeSnapshotPayload,
    ResizeVolumePayload,
    RevertVolumeSnapshotPayload,
    SnapshotVolumePayload,
)

if TYPE_CHECKING:
    from mgf.vm.core.cancellation import CancellationToken
    from mgf.vm.core.contracts import StorageBackend

# The storage mutation kinds this handler owns (volume snapshots route here
# too — VOLUME_SNAPSHOT / VOLUME_REVERT — the volume-level half of B3).
STORAGE_KINDS = frozenset(
    {
        OperationKind.VOLUME_CREATE,
        OperationKind.VOLUME_RESIZE,
        OperationKind.VOLUME_DELETE,
        OperationKind.VOLUME_SNAPSHOT,
        OperationKind.VOLUME_REVERT,
        OperationKind.VOLUME_CONVERT,
        OperationKind.POOL_CREATE,
        OperationKind.POOL_DELETE,
    }
)

# Volume-snapshot *delete* has no dedicated OperationKind in the contract — a
# bare ``SnapshotId`` identifies it. The client submits it under VOLUME_DELETE
# and ``dispatch`` disambiguates on the payload type (see below).

_P = TypeVar("_P")


def dispatch(
    storage: StorageBackend | None,
    kind: OperationKind,
    payload: object,
    token: CancellationToken | None,
) -> object:
    """Route a storage ``kind`` → ``StorageBackend`` call; return the result.

    Raises :class:`TransportError` if no storage backend was injected, or if a
    payload does not match its op kind. The backend's typed storage errors
    propagate unchanged.
    """
    if storage is None:
        raise TransportError(
            f"operation {kind.value!r} needs a StorageBackend but none was injected"
        )

    if kind is OperationKind.VOLUME_CREATE:
        return storage.create_volume(_as(payload, CreateVolumePayload).spec)

    if kind is OperationKind.VOLUME_RESIZE:
        rz = _as(payload, ResizeVolumePayload)
        storage.resize_volume(rz.volume_id, rz.new_size_bytes)
        return None

    if kind is OperationKind.VOLUME_DELETE:
        # Two payload shapes ride this kind: delete a volume, or delete a
        # volume snapshot (the contract gives the latter no dedicated kind —
        # the SnapshotId identifies it). Disambiguate by payload type.
        if isinstance(payload, DeleteVolumeSnapshotPayload):
            storage.delete_snapshot(payload.snapshot_id)
            return None
        storage.delete_volume(_as(payload, DeleteVolumePayload).volume_id)
        return None

    if kind is OperationKind.VOLUME_SNAPSHOT:
        sn = _as(payload, SnapshotVolumePayload)
        return storage.snapshot_volume(sn.volume_id, sn.name)

    if kind is OperationKind.VOLUME_REVERT:
        rv = _as(payload, RevertVolumeSnapshotPayload)
        storage.revert_volume_to_snapshot(rv.volume_id, rv.snapshot_id, token=token)
        return None

    if kind is OperationKind.VOLUME_CONVERT:
        cv = _as(payload, ConvertFormatPayload)
        return storage.convert_format(cv.src, cv.dst_format, cv.dst_spec, token=token)

    if kind is OperationKind.POOL_CREATE:
        storage.create_pool(_as(payload, CreatePoolPayload).spec)
        return None

    if kind is OperationKind.POOL_DELETE:
        dp = _as(payload, DeletePoolPayload)
        storage.delete_pool(dp.name, force=dp.force)
        return None

    raise TransportError(f"storage handler does not own {kind.value!r}")


def _as(payload: object, expected: type[_P]) -> _P:
    """Narrow ``payload`` to ``expected`` or raise a typed transport error."""
    if isinstance(payload, expected):
        return payload
    raise TransportError(f"payload {type(payload).__name__!r} is not a {expected.__name__}")
