"""Serialization for the canonical :class:`~mgf.vm.core.contracts.VmSpec`.

:func:`~mgf.vm.core.contracts.migrate_vmspec` (D-G8) consumes a *serialized*
``VmSpec`` mapping — this module is what produces and re-reads it. Pure
stdlib (json-friendly dicts), no I/O, so it stays inside the core purity
contract. It is the single source of truth for "``VmSpec`` ↔ a plain mapping"
used by anything that must persist or wire-carry a spec without the native
engine format:

* the SDK's backup-archive metadata (D-G18) — a restore reconstructs the VM
  from the spec carried in the manifest, the neutral counterpart of the
  umbrella's stored ``domain_xml``;
* the operation registry / control plane that round-trips specs.

The mapping is the canonical 10-field neutral shape (``api.md`` §6.1) plus
``schema_version``; enums serialize as their string value, nested specs as
nested mappings, and ``metadata`` as a plain dict. :func:`vmspec_from_mapping`
runs the loaded mapping through :func:`migrate_vmspec` before reconstruction,
so an older serialized spec upgrades transparently (D-G8).
"""

from __future__ import annotations

from types import MappingProxyType
from typing import TYPE_CHECKING, Any

from mgf.vm.core.contracts import (
    CURRENT_VMSPEC_SCHEMA_VERSION,
    ClaimMode,
    DeviceSpec,
    DiskSpec,
    NicSpec,
    VmSpec,
    migrate_vmspec,
)
from mgf.vm.core.domain import IsolationLevel
from mgf.vm.core.provisioning.spec import ProvisioningSpec

if TYPE_CHECKING:
    from collections.abc import Mapping

__all__ = ["vmspec_from_mapping", "vmspec_to_mapping"]


# ---------------------------------------------------------------------------
# VmSpec -> mapping
# ---------------------------------------------------------------------------


def vmspec_to_mapping(spec: VmSpec) -> dict[str, Any]:
    """Serialize ``spec`` to a json-friendly mapping (the inverse of
    :func:`vmspec_from_mapping`). Enums become their string value; nested
    specs become nested mappings; ``metadata`` becomes a plain dict.
    """
    return {
        "schema_version": spec.schema_version,
        "name": spec.name,
        "cpu": spec.cpu,
        "memory_bytes": spec.memory_bytes,
        "disks": [_disk_to_mapping(d) for d in spec.disks],
        "nics": [_nic_to_mapping(n) for n in spec.nics],
        "devices": [_device_to_mapping(d) for d in spec.devices],
        "provisioning": _provisioning_to_mapping(spec.provisioning),
        "isolation": spec.isolation.value,
        "metadata": dict(spec.metadata),
    }


def _disk_to_mapping(d: DiskSpec) -> dict[str, Any]:
    return {
        "size_bytes": d.size_bytes,
        "format": d.format,
        "source": d.source,
        "readonly": d.readonly,
    }


def _nic_to_mapping(n: NicSpec) -> dict[str, Any]:
    return {"network": n.network, "model": n.model, "mac": n.mac}


def _device_to_mapping(d: DeviceSpec) -> dict[str, Any]:
    return {"kind": d.kind, "selector": dict(d.selector), "mode": d.mode.value}


def _provisioning_to_mapping(p: ProvisioningSpec | None) -> dict[str, Any] | None:
    if p is None:
        return None
    return {
        "backend": p.backend,
        "hostname": p.hostname,
        "username": p.username,
        "password": p.password,
        "ssh_authorized_keys": list(p.ssh_authorized_keys),
        "packages": list(p.packages),
        "run_commands": list(p.run_commands),
    }


# ---------------------------------------------------------------------------
# mapping -> VmSpec
# ---------------------------------------------------------------------------


def vmspec_from_mapping(raw: Mapping[str, Any]) -> VmSpec:
    """Reconstruct a :class:`VmSpec` from a mapping produced by
    :func:`vmspec_to_mapping` (or an older serialized spec).

    The mapping is first run through :func:`migrate_vmspec` from its recorded
    ``schema_version`` (default: current) so an older spec upgrades to the
    current shape before reconstruction (D-G8). Missing optional keys fall
    back to the :class:`VmSpec` field defaults (additive evolution, D-D6).
    """
    from_version = int(raw.get("schema_version", CURRENT_VMSPEC_SCHEMA_VERSION))
    data = migrate_vmspec(raw, from_version)
    return VmSpec(
        name=str(data["name"]),
        cpu=int(data["cpu"]),
        memory_bytes=int(data["memory_bytes"]),
        disks=tuple(_disk_from_mapping(d) for d in data.get("disks", ())),
        nics=tuple(_nic_from_mapping(n) for n in data.get("nics", ())),
        devices=tuple(_device_from_mapping(d) for d in data.get("devices", ())),
        provisioning=_provisioning_from_mapping(data.get("provisioning")),
        isolation=IsolationLevel(data.get("isolation", IsolationLevel.OFF.value)),
        metadata=MappingProxyType(dict(data.get("metadata", {}))),
        schema_version=CURRENT_VMSPEC_SCHEMA_VERSION,
    )


def _disk_from_mapping(d: Mapping[str, Any]) -> DiskSpec:
    return DiskSpec(
        size_bytes=int(d["size_bytes"]),
        format=str(d.get("format", "qcow2")),
        source=_str_or_none(d.get("source")),
        readonly=bool(d.get("readonly", False)),
    )


def _nic_from_mapping(n: Mapping[str, Any]) -> NicSpec:
    return NicSpec(
        network=str(n.get("network", "default")),
        model=str(n.get("model", "virtio")),
        mac=_str_or_none(n.get("mac")),
    )


def _device_from_mapping(d: Mapping[str, Any]) -> DeviceSpec:
    return DeviceSpec(
        kind=str(d["kind"]),
        selector={str(k): str(v) for k, v in dict(d.get("selector", {})).items()},
        mode=ClaimMode(d.get("mode", ClaimMode.EXCLUSIVE.value)),
    )


def _provisioning_from_mapping(p: Mapping[str, Any] | None) -> ProvisioningSpec | None:
    if p is None:
        return None
    return ProvisioningSpec(
        backend=str(p.get("backend", "none")),
        hostname=_str_or_none(p.get("hostname")),
        username=_str_or_none(p.get("username")),
        password=_str_or_none(p.get("password")),
        ssh_authorized_keys=[str(x) for x in p.get("ssh_authorized_keys", ())],
        packages=[str(x) for x in p.get("packages", ())],
        run_commands=[str(x) for x in p.get("run_commands", ())],
    )


def _str_or_none(value: object) -> str | None:
    return None if value is None else str(value)
