"""``configurator/memory`` — balloon, max memory, hugepage backing (pure planner).

Semantics match :mod:`mgf.vm.core.configurator.cpu`:

* ``current_mb`` is the balloon target (what the guest sees now).
  Growing it inside ``max_mb`` is hotpluggable.
* ``max_mb`` is the ceiling. Changing it always requires a
  shutdown because QEMU allocates the guest address space at
  boot.
* ``hugepages`` toggles the ``<memoryBacking><hugepages/>``
  element. Always requires shutdown.

**Pure core.** :func:`plan` accepts an *already-parsed*
:class:`xml.etree.ElementTree.Element`; the ``apply()`` half stays in
the adapter (``mgf-vm``).
"""

from __future__ import annotations

from typing import TYPE_CHECKING

from mgf.vm.core.configurator.types import ConfigChange
from mgf.vm.core.configurator.xml_helpers import ensure, remove, serialize_domain_xml
from mgf.vm.core.exceptions import SpecValidationError

if TYPE_CHECKING:
    import xml.etree.ElementTree as ET

SUBSYSTEM = "memory"

_MB_TO_KIB = 1024


def plan(
    domain: ET.Element,
    *,
    current_mb: int | None = None,
    max_mb: int | None = None,
    hugepages: bool | None = None,
) -> ConfigChange:
    """Produce a :class:`ConfigChange` for a memory edit.

    ``domain`` is the parsed domain root element (injected by the
    adapter); it is mutated in place to form the new tree.
    """
    if current_mb is not None and current_mb < 64:
        raise SpecValidationError([f"current_mb must be >= 64, got {current_mb}"])
    if max_mb is not None and max_mb < 64:
        raise SpecValidationError([f"max_mb must be >= 64, got {max_mb}"])

    old_xml = serialize_domain_xml(domain)
    max_el = ensure(domain, "memory")
    cur_el = ensure(domain, "currentMemory")

    max_el.set("unit", "KiB")
    cur_el.set("unit", "KiB")

    old_max = int((max_el.text or "0").strip() or "0")
    old_cur = int((cur_el.text or "0").strip() or "0")
    old_max_mb = old_max // _MB_TO_KIB
    old_cur_mb = old_cur // _MB_TO_KIB

    new_max_mb = max_mb if max_mb is not None else old_max_mb
    new_cur_mb = current_mb if current_mb is not None else old_cur_mb
    if new_cur_mb > new_max_mb:
        raise SpecValidationError([f"current_mb ({new_cur_mb}) must be <= max_mb ({new_max_mb})"])

    diffs: list[str] = []
    requires_shutdown = False

    if new_max_mb != old_max_mb:
        max_el.text = str(new_max_mb * _MB_TO_KIB)
        diffs.append(f"max_mb {old_max_mb} -> {new_max_mb}")
        requires_shutdown = True

    if new_cur_mb != old_cur_mb or new_max_mb != old_max_mb:
        cur_el.text = str(new_cur_mb * _MB_TO_KIB)
        if new_cur_mb != old_cur_mb:
            diffs.append(f"current_mb {old_cur_mb} -> {new_cur_mb}")

    if hugepages is not None:
        had_hugepages = domain.find("memoryBacking/hugepages") is not None
        if hugepages and not had_hugepages:
            ensure(domain, "memoryBacking/hugepages")
            diffs.append("hugepages off -> on")
            requires_shutdown = True
        elif not hugepages and had_hugepages:
            remove(domain, "memoryBacking/hugepages")
            # If memoryBacking is now empty, prune it too.
            mb_el = domain.find("memoryBacking")
            if mb_el is not None and len(list(mb_el)) == 0:
                remove(domain, "memoryBacking")
            diffs.append("hugepages on -> off")
            requires_shutdown = True

    summary = ", ".join(diffs) or "no change"
    return ConfigChange(
        subsystem=SUBSYSTEM,
        summary=summary,
        old_xml=old_xml,
        new_xml=serialize_domain_xml(domain),
        requires_shutdown=requires_shutdown,
    )


__all__ = ["SUBSYSTEM", "plan"]
