""":class:`LibvirtVmSpec` — the libvirt-enriched mapping target (D-G12).

As of D-G12, ``mgf.vm.core.contracts.VmSpec`` is the **canonical**
engine-neutral VM definition (name / cpu / memory_bytes / disks / nics /
devices / provisioning / isolation / metadata / schema_version). It is
the public input to the adapter; the native libvirt format never leaks
above the adapter and no engine-ism leaks into core's ``VmSpec``.

``LibvirtVmSpec`` is therefore **NOT a parallel spec definition** — it is
the rich internal mapping target the XML generator consumes. The only
supported way to build one from public input is
:meth:`LibvirtVmSpec.from_core`, which maps the canonical core
``VmSpec`` into the libvirt-enriched struct. The constructor stays
public for internal use and tests.

The ``libvirt/*`` metadata contract (D-G12)
===========================================

Engine-specific tuning that core's neutral ``VmSpec`` deliberately does
not model is read by :meth:`from_core` from ``spec.metadata`` under a
``libvirt/``-namespaced key convention. When a key is absent the adapter
applies a sensible default. Values are plain strings (metadata is
``Mapping[str, str]``) and are parsed to the target type defensively
(see :func:`_as_bool` / :func:`_as_int`):

==============================  ============  ===========  ==================================
metadata key                    type          default      maps to
==============================  ============  ===========  ==================================
``libvirt/firmware``            str           ``"bios"``   ``LibvirtVmSpec.firmware``
``libvirt/secure_boot``         bool          ``False``    ``LibvirtVmSpec.secure_boot``
``libvirt/cpu_model``           str           ``"host-passthrough"``  ``LibvirtVmSpec.cpu_model``
``libvirt/cpu_pinning``         str | None    ``None``     ``LibvirtVmSpec.cpu_pinning``
``libvirt/vcpus_max``           int | None    ``None``     ``LibvirtVmSpec.vcpus_max``
``libvirt/hugepages``           bool          ``False``    ``LibvirtVmSpec.hugepages``
``libvirt/balloon``             bool          ``True``     ``LibvirtVmSpec.balloon``
``libvirt/display``             str           ``"spice"``  ``LibvirtVmSpec.display``
``libvirt/disk_bus``            str           ``"virtio"`` per-disk ``DiskSpec.bus`` default
``libvirt/disk_cache``          str           ``"none"``   per-disk ``DiskSpec.cache`` default
``libvirt/memory_max_bytes``    int | None    ``None``     ``LibvirtVmSpec.memory_max_bytes``
==============================  ============  ===========  ==================================

Determinism contract (contracts.md §3)
=======================================

:meth:`from_core` is a **pure deterministic function**: the same
``(spec, metadata)`` always yields an equal ``LibvirtVmSpec`` and hence —
through :func:`mgf.vm.libvirt._xml.generate.vmspec_to_xml` — byte-identical
domain XML. This is what makes ``define()`` idempotent (CONF-H-02): no
``uuid4()``, no clock reads, no set-iteration ordering. Disk target
device names (``vda``, ``vdb``, …) are derived deterministically from
disk index, and metadata is read by explicit key (never iterated).

Do NOT modify ``mgf.vm.core`` from here. This module owns the libvirt
enrichment so core stays a stable, minimal contract.

The dataclass is **frozen** (immutable, hashable) so equal specs render
to byte-identical XML and a spec can serve as a cache key.
"""

from __future__ import annotations

from dataclasses import dataclass, field, replace
from typing import TYPE_CHECKING

from mgf.vm.core.contracts import DiskSpec as CoreDiskSpec
from mgf.vm.core.contracts import NicSpec as CoreNicSpec
from mgf.vm.core.domain import IsolationLevel

if TYPE_CHECKING:
    from collections.abc import Mapping

    from mgf.vm.core.contracts import VmSpec

# Field-value vocabularies — lifted from the legacy ``creator/spec.py``
# Literals so the XML generator and any future validator agree on the
# allowed values. Kept as plain tuples (not Literal types) so the bridge
# stays dependency-light; ruff/mypy still see the concrete defaults.
DISK_FORMATS = ("qcow2", "raw")
DISK_BUSES = ("virtio", "sata", "scsi", "ide")
DISK_CACHES = ("none", "writeback", "writethrough", "directsync", "unsafe")
NIC_MODELS = ("virtio", "e1000", "rtl8139")
FIRMWARES = ("bios", "uefi")
DISPLAYS = ("spice", "vnc", "none")

# ── The ``libvirt/*`` metadata-key contract (D-G12). Names are the single
#    source of truth for the namespaced keys ``from_core`` reads. ──────────
_NS = "libvirt/"
KEY_FIRMWARE = _NS + "firmware"
KEY_SECURE_BOOT = _NS + "secure_boot"
KEY_CPU_MODEL = _NS + "cpu_model"
KEY_CPU_PINNING = _NS + "cpu_pinning"
KEY_VCPUS_MAX = _NS + "vcpus_max"
KEY_HUGEPAGES = _NS + "hugepages"
KEY_BALLOON = _NS + "balloon"
KEY_DISPLAY = _NS + "display"
KEY_DISK_BUS = _NS + "disk_bus"
KEY_DISK_CACHE = _NS + "disk_cache"
KEY_MEMORY_MAX_BYTES = _NS + "memory_max_bytes"

# Adapter defaults applied when the corresponding metadata key is absent.
DEFAULT_FIRMWARE = "bios"
DEFAULT_CPU_MODEL = "host-passthrough"
DEFAULT_DISPLAY = "spice"
DEFAULT_DISK_BUS = "virtio"
DEFAULT_DISK_CACHE = "none"
DEFAULT_SECURE_BOOT = False
DEFAULT_HUGEPAGES = False
DEFAULT_BALLOON = True

# Deterministic virtio-style target device names by disk index (vda, vdb, …).
_DISK_TARGET_PREFIX = "vd"


def _as_bool(raw: str | None, default: bool) -> bool:
    """Parse a metadata string to a bool; fall back to ``default``.

    Accepts the usual truthy/falsy spellings case-insensitively. Any
    unrecognised value falls back to ``default`` (never raises) so a
    malformed metadata entry cannot break ``define()``.
    """
    if raw is None:
        return default
    norm = raw.strip().lower()
    if norm in ("1", "true", "yes", "on"):
        return True
    if norm in ("0", "false", "no", "off", ""):
        return False
    return default


def _as_int(raw: str | None, default: int | None) -> int | None:
    """Parse a metadata string to an int; fall back to ``default``.

    A non-numeric value falls back to ``default`` rather than raising, so
    a malformed metadata entry cannot break ``define()``.
    """
    if raw is None:
        return default
    try:
        return int(raw.strip())
    except ValueError:
        return default


def _disk_target(index: int) -> str:
    """Deterministic virtio target dev name for the disk at ``index``.

    ``0 -> vda``, ``1 -> vdb`` … (index-derived, never order-sensitive
    across calls), so equal specs always render byte-identical XML.
    """
    return _DISK_TARGET_PREFIX + chr(ord("a") + index)


@dataclass(frozen=True, slots=True)
class DiskSpec:
    """One libvirt-enriched disk consumed by the XML generator.

    Mirrors core :class:`mgf.vm.core.contracts.DiskSpec`
    (``size_bytes`` / ``format`` / ``source`` / ``readonly``) and adds the
    libvirt-only tuning knobs (``bus`` / ``cache`` / ``target``) that
    :meth:`LibvirtVmSpec.from_core` fills from adapter defaults +
    ``libvirt/*`` metadata.
    """

    size_bytes: int
    format: str = "qcow2"
    bus: str = "virtio"
    cache: str = "none"
    target: str = "vda"
    source_path: str | None = None
    """Host-side image path; ``None`` means the pool allocates it."""
    pool: str | None = None
    readonly: bool = False


@dataclass(frozen=True, slots=True)
class NicSpec:
    """One libvirt-enriched network interface consumed by the generator.

    1:1 with core :class:`mgf.vm.core.contracts.NicSpec`
    (``network`` / ``model`` / ``mac``); kept as a distinct internal type
    so the generator stays decoupled from core's shape.
    """

    network: str = "default"
    model: str = "virtio"
    mac: str | None = None


@dataclass(frozen=True, slots=True)
class LibvirtVmSpec:
    """Libvirt-enriched VM spec — the internal mapping target (D-G12).

    Built from the canonical core :class:`mgf.vm.core.contracts.VmSpec`
    via :meth:`from_core`. The neutral fields (``name`` / ``vcpus`` /
    ``memory_bytes`` / disks / nics / isolation) come straight from the
    core spec; the libvirt-only knobs come from adapter defaults plus
    opt-in ``libvirt/*`` metadata overrides (see the module docstring for
    the full key contract).
    """

    name: str
    vcpus: int
    memory_bytes: int

    # ── CPU (beyond core's plain count) ────────────────────────────────
    cpu_model: str = DEFAULT_CPU_MODEL
    vcpus_max: int | None = None
    cpu_pinning: str | None = None

    # ── Memory (beyond core's plain byte count) ────────────────────────
    memory_max_bytes: int | None = None
    hugepages: bool = DEFAULT_HUGEPAGES
    balloon: bool = DEFAULT_BALLOON

    # ── Storage ────────────────────────────────────────────────────────
    disks: tuple[DiskSpec, ...] = ()

    # ── Networking ──────────────────────────────────────────────────────
    nics: tuple[NicSpec, ...] = field(default_factory=lambda: (NicSpec(),))

    # ── Firmware ────────────────────────────────────────────────────────
    firmware: str = DEFAULT_FIRMWARE
    secure_boot: bool = DEFAULT_SECURE_BOOT

    # ── Display ─────────────────────────────────────────────────────────
    display: str = DEFAULT_DISPLAY
    display_listen: str = "127.0.0.1"
    display_port: int | None = None

    # ── Isolation posture ───────────────────────────────────────────────
    isolation: IsolationLevel = IsolationLevel.OFF

    # ── Identity extras ──────────────────────────────────────────────────
    uuid: str | None = None
    """Optional fixed domain UUID. ``None`` → libvirt assigns one on
    define; for deterministic XML the caller supplies it."""
    description: str = ""

    @property
    def metadata(self) -> Mapping[str, str]:
        """Empty mapping — kept for source compatibility with callers that
        read ``spec.metadata``. The canonical metadata lives on the core
        :class:`VmSpec`; libvirt tuning has already been consumed by
        :meth:`from_core` into the typed fields above."""
        return {}

    # -- canonical mapper ---------------------------------------------------

    @classmethod
    def from_core(cls, spec: VmSpec) -> LibvirtVmSpec:
        """Map the canonical core :class:`VmSpec` into a libvirt spec (D-G12).

        Neutral fields map directly; engine tuning is read from
        ``spec.metadata`` under the ``libvirt/*`` key convention with
        adapter defaults applied when a key is absent (see the module
        docstring for the full contract). This function is **pure and
        deterministic**: the same ``(spec, metadata)`` always yields an
        equal ``LibvirtVmSpec`` — no ``uuid4()``, no clock, no
        set-ordering — which is what makes ``define()`` idempotent
        (CONF-H-02).
        """
        md = spec.metadata

        disk_bus = md.get(KEY_DISK_BUS, DEFAULT_DISK_BUS)
        disk_cache = md.get(KEY_DISK_CACHE, DEFAULT_DISK_CACHE)
        disks = tuple(
            DiskSpec(
                size_bytes=d.size_bytes,
                format=d.format,
                source_path=d.source,
                readonly=d.readonly,
                bus=disk_bus,
                cache=disk_cache,
                target=_disk_target(i),
            )
            for i, d in enumerate(spec.disks)
        )

        nics = tuple(NicSpec(network=n.network, model=n.model, mac=n.mac) for n in spec.nics)

        return cls(
            name=spec.name,
            vcpus=spec.cpu,
            memory_bytes=spec.memory_bytes,
            cpu_model=md.get(KEY_CPU_MODEL, DEFAULT_CPU_MODEL),
            vcpus_max=_as_int(md.get(KEY_VCPUS_MAX), None),
            cpu_pinning=md.get(KEY_CPU_PINNING),
            memory_max_bytes=_as_int(md.get(KEY_MEMORY_MAX_BYTES), None),
            hugepages=_as_bool(md.get(KEY_HUGEPAGES), DEFAULT_HUGEPAGES),
            balloon=_as_bool(md.get(KEY_BALLOON), DEFAULT_BALLOON),
            disks=disks,
            nics=nics,
            firmware=md.get(KEY_FIRMWARE, DEFAULT_FIRMWARE),
            secure_boot=_as_bool(md.get(KEY_SECURE_BOOT), DEFAULT_SECURE_BOOT),
            display=md.get(KEY_DISPLAY, DEFAULT_DISPLAY),
            isolation=spec.isolation,
        )

    # -- internal builder ---------------------------------------------------

    def evolve(self, **overrides: object) -> LibvirtVmSpec:
        """Return a copy with ``overrides`` applied (internal/test use)."""
        return replace(self, **overrides)  # type: ignore[arg-type]


# Re-exported so tests / internal callers can build core disks/nics that
# ``from_core`` consumes without reaching into ``mgf.vm.core`` directly.
__all__ = [
    "DEFAULT_BALLOON",
    "DEFAULT_CPU_MODEL",
    "DEFAULT_DISK_BUS",
    "DEFAULT_DISK_CACHE",
    "DEFAULT_DISPLAY",
    "DEFAULT_FIRMWARE",
    "DEFAULT_HUGEPAGES",
    "DEFAULT_SECURE_BOOT",
    "DISK_BUSES",
    "DISK_CACHES",
    "DISK_FORMATS",
    "DISPLAYS",
    "FIRMWARES",
    "KEY_BALLOON",
    "KEY_CPU_MODEL",
    "KEY_CPU_PINNING",
    "KEY_DISK_BUS",
    "KEY_DISK_CACHE",
    "KEY_DISPLAY",
    "KEY_FIRMWARE",
    "KEY_HUGEPAGES",
    "KEY_MEMORY_MAX_BYTES",
    "KEY_SECURE_BOOT",
    "KEY_VCPUS_MAX",
    "NIC_MODELS",
    "CoreDiskSpec",
    "CoreNicSpec",
    "DiskSpec",
    "LibvirtVmSpec",
    "NicSpec",
]
