"""Pure libvirt domain-XML manipulation helpers for the configurator core.

This module is the single place where an already-parsed libvirt domain
tree is walked, patched, and serialised for the configurator subsystems.
Every ``configurator/<subsystem>.py`` plan() calls into here rather than
growing its own :mod:`xml.etree.ElementTree` logic.

**Pure-core boundary (decided 2026-05-29).** The original
``mgf.vm.configurator.xml_utils`` also owned ``parse_domain_xml`` (built
on ``defusedxml``) and ``apply_config_change`` (which drove a live
libvirt redefine). Neither belongs in the pure core:

- ``parse_domain_xml`` parses an *untrusted* byte stream and is
  ``defusedxml``-backed; ``defusedxml`` is forbidden by the import-linter
  purity contract. Parsing is an adapter concern — the adapter parses and
  injects the resulting :class:`xml.etree.ElementTree.Element`.
- ``apply_config_change`` issues a provider redefine (engine I/O) and is
  adapter-bound.

What remains here is the pure tree-manipulation surface: ``find`` /
``ensure`` / ``set_text`` / ``set_attr`` / ``remove`` / ``iter_children``
(operating on an injected ``Element``), ``serialize_domain_xml`` (stdlib
output only), and ``diff_xml`` (a pure string diff). They are built on
stdlib :mod:`xml.etree.ElementTree` and :mod:`mgf.vm.core.common.xml`.
"""

from __future__ import annotations

import difflib
import xml.etree.ElementTree as ET

from mgf.vm.core.common.xml import find_first, serialize

# ---------------------------------------------------------------------------
# Serialise (pure output — no parse)
# ---------------------------------------------------------------------------


def serialize_domain_xml(root: ET.Element) -> str:
    """Serialise an element tree back to a libvirt XML string.

    Always uses UTF-8 + short-form empty elements so the output lines up
    with what libvirt itself writes — :func:`diff_xml` works on these
    strings directly. Delegates to :func:`mgf.vm.core.common.xml.serialize`
    (stdlib ``ET.tostring``).
    """
    return serialize(root)


# ---------------------------------------------------------------------------
# Find / ensure / set / remove — thin wrappers over an *injected* tree.
# ---------------------------------------------------------------------------


def find(root: ET.Element, path: str) -> ET.Element | None:
    """Like ``root.find(path)`` but explicit about the None case."""
    return find_first(root, path)


def ensure(root: ET.Element, path: str) -> ET.Element:
    """Return the element at ``path``, creating it if missing.

    The path is a slash-separated tag chain; each segment is a bare tag
    name (no attributes, no indexes). The function descends
    segment-by-segment, creating any missing child as a new empty
    ``ET.SubElement``. Returns the final child.
    """
    node = root
    for tag in path.split("/"):
        child = node.find(tag)
        if child is None:
            child = ET.SubElement(node, tag)
        node = child
    return node


def set_text(root: ET.Element, path: str, value: str) -> None:
    """Set the text of the element at ``path`` (creating it if needed)."""
    ensure(root, path).text = value


def set_attr(root: ET.Element, path: str, attr: str, value: str) -> None:
    """Set ``attr=value`` on the element at ``path`` (creating it if needed)."""
    ensure(root, path).set(attr, value)


def remove(root: ET.Element, path: str) -> bool:
    """Remove the element at ``path`` and return whether anything changed."""
    parent_path, _, tag = path.rpartition("/")
    parent = root if not parent_path else root.find(parent_path)
    if parent is None:
        return False
    target = parent.find(tag)
    if target is None:
        return False
    parent.remove(target)
    return True


def iter_children(root: ET.Element, path: str) -> list[ET.Element]:
    """Return every child matching ``path`` (possibly empty)."""
    return list(root.findall(path))


# ---------------------------------------------------------------------------
# Diff (pure string diff)
# ---------------------------------------------------------------------------


def diff_xml(old: str, new: str, *, context_lines: int = 3) -> str:
    """Return a unified diff between two domain XML strings.

    Used by every ``vm config ... --preview`` command to show the user
    what would change before any libvirt call fires. The function splits
    on newlines; callers that pass single-line XML blobs will see a
    single-line diff, which is fine.
    """
    old_lines = old.splitlines(keepends=True)
    new_lines = new.splitlines(keepends=True)
    return "".join(
        difflib.unified_diff(
            old_lines,
            new_lines,
            fromfile="before",
            tofile="after",
            n=context_lines,
        )
    )


__all__ = [
    "diff_xml",
    "ensure",
    "find",
    "iter_children",
    "remove",
    "serialize_domain_xml",
    "set_attr",
    "set_text",
]
