"""``configurator/display`` — VNC/SPICE graphics element (pure planner).

libvirt does not hotplug graphics devices cleanly — changing the display
type on a running VM leaves the current viewer channel dangling and tends
to crash the guest driver. Every change in this module therefore flags
``requires_shutdown=True``.

**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

import xml.etree.ElementTree as ET

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

SUBSYSTEM = "display"

_VALID_TYPES = {"vnc", "spice", "none"}


def plan(
    domain: ET.Element,
    *,
    display_type: str | None = None,
    listen: str | None = None,
    port: int | None = None,
) -> ConfigChange:
    """Produce a :class:`ConfigChange` for a display edit.

    ``domain`` is the parsed domain root element (injected by the
    adapter); it is mutated in place. Passing ``display_type='none'``
    removes the ``<graphics>`` element entirely. Any other change
    rewrites it.
    """
    if display_type is not None and display_type not in _VALID_TYPES:
        raise SpecValidationError(
            [f"display type must be one of {sorted(_VALID_TYPES)}, got {display_type!r}"]
        )

    old_xml = serialize_domain_xml(domain)
    devices = ensure(domain, "devices")
    graphics = devices.find("graphics")

    old_type = (graphics.get("type") if graphics is not None else "none") or "none"
    old_listen = graphics.get("listen") if graphics is not None else None
    old_port_attr = graphics.get("port") if graphics is not None else None

    target_type: str = display_type or old_type
    target_listen = listen if listen is not None else old_listen
    target_port_attr: str | None
    if port is not None:
        target_port_attr = str(port)
    else:
        target_port_attr = old_port_attr

    diffs: list[str] = []

    if target_type == "none":
        if graphics is not None:
            remove(domain, "devices/graphics")
            diffs.append(f"display {old_type} -> none")
    else:
        # (Re)create <graphics> with the resolved attributes.
        if graphics is not None:
            devices.remove(graphics)
        new_graphics = ET.SubElement(devices, "graphics")
        new_graphics.set("type", target_type)
        if target_listen is not None:
            new_graphics.set("listen", target_listen)
        if target_port_attr is not None:
            new_graphics.set("port", target_port_attr)

        if old_type != target_type:
            diffs.append(f"display {old_type} -> {target_type}")
        if old_listen != target_listen and target_listen is not None:
            diffs.append(f"listen -> {target_listen}")
        if old_port_attr != target_port_attr and port is not None:
            diffs.append(f"port -> {port}")

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


__all__ = ["SUBSYSTEM", "plan"]
