"""``configurator/cpu`` — vCPU count and CPU model edits (pure planner).

Semantics:

* ``vcpus`` is the "current" count (``<vcpu current='N'>M</vcpu>``).
  Growing it from 2 to 4 within a ``vcpus_max`` of 8 is hotpluggable
  (balloon-style vCPU hot-add); shrinking is also safe on every
  libvirt we care about.
* ``vcpus_max`` is the ceiling. Changing it always requires a
  shutdown because the allocated array of vCPU structs inside QEMU
  is fixed at boot.
* ``model`` changes are never hotpluggable — the CPU model is
  baked into QEMU's ``-cpu`` flag at process start.

**Pure core.** :func:`plan` accepts an *already-parsed*
:class:`xml.etree.ElementTree.Element` (the adapter parses untrusted
domain XML with ``defusedxml`` and injects the tree). The ``apply()``
half — which reads the live domain XML from the provider and drives a
redefine — 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, serialize_domain_xml, set_text
from mgf.vm.core.exceptions import SpecValidationError

if TYPE_CHECKING:
    import xml.etree.ElementTree as ET

SUBSYSTEM = "cpu"


def plan(
    domain: ET.Element,
    *,
    vcpus: int | None = None,
    vcpus_max: int | None = None,
    model: str | None = None,
) -> ConfigChange:
    """Produce a :class:`ConfigChange` for a CPU edit.

    ``domain`` is the parsed domain root element (injected by the
    adapter). It is mutated in place to form the new tree.

    * ``vcpus`` — set the current vCPU count (the ``current``
      attribute on ``<vcpu>``). Must be <= the effective maximum.
    * ``vcpus_max`` — set the maximum. Flags the change as
      requires-shutdown.
    * ``model`` — set the ``<cpu><model>`` text. Flags the change
      as requires-shutdown.
    """
    if vcpus is not None and vcpus < 1:
        raise SpecValidationError([f"vcpus must be >= 1, got {vcpus}"])
    if vcpus_max is not None and vcpus_max < 1:
        raise SpecValidationError([f"vcpus_max must be >= 1, got {vcpus_max}"])

    old_xml = serialize_domain_xml(domain)
    vcpu_el = ensure(domain, "vcpu")
    old_max_text = (vcpu_el.text or "").strip()
    old_max = int(old_max_text) if old_max_text else 1
    old_current_attr = vcpu_el.get("current")
    old_current = int(old_current_attr) if old_current_attr else old_max

    new_max = vcpus_max if vcpus_max is not None else old_max
    new_current = vcpus if vcpus is not None else old_current
    if new_current > new_max:
        raise SpecValidationError([f"vcpus ({new_current}) must be <= vcpus_max ({new_max})"])

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

    if new_max != old_max:
        vcpu_el.text = str(new_max)
        diffs.append(f"vcpus_max {old_max} -> {new_max}")
        requires_shutdown = True

    if new_current != old_current or new_max != old_max:
        vcpu_el.set("current", str(new_current))
        if new_current != old_current:
            diffs.append(f"vcpus {old_current} -> {new_current}")

    if model is not None:
        model_el = ensure(domain, "cpu/model")
        old_model = (model_el.text or "").strip() or None
        if old_model != model:
            set_text(domain, "cpu/model", model)
            diffs.append(f"cpu_model {old_model or '(default)'} -> {model}")
            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"]
