"""XML tree utilities for the pure core — **construct, manipulate, and
serialize only; never parse untrusted input here.**

The original ``mgf.vm.common.xml`` was a *parser* built on ``defusedxml``
(XXE / billion-laughs / quadratic-blowup defence). It was deliberately
**not** carried into ``mgf-vm-core``: parsing untrusted XML is an I/O
boundary concern, and ``defusedxml`` is forbidden in the pure core by the
import-linter purity contract (it would also drag a third-party dep into
the domain layer).

The boundary convention (decided 2026-05-29):

- **Adapters parse.** Untrusted XML (libvirt domain XML from a restored
  backup, a user ``xml --edit``, an imported OVA) is parsed in the
  adapter (mgf-vm-libvirt / -interchange / agent) using ``defusedxml``,
  which yields a standard :class:`xml.etree.ElementTree.Element`.
- **Core operates on the parsed tree.** The adapter injects that
  ``Element`` into core. Core builds, queries, edits, and serializes the
  tree with the stdlib :mod:`xml.etree.ElementTree` API — none of which
  touches an untrusted byte stream, so no XXE surface exists here.

That is why this module imports stdlib ``ElementTree`` (the *output /
manipulation* API) but exposes **no parse function**. If you find
yourself wanting to parse a string in core, the parse belongs in the
adapter; pass the resulting ``Element`` in instead.
"""

from __future__ import annotations

import xml.etree.ElementTree as ET

#: The core's XML currency. Core functions accept and return this type;
#: they never accept a raw XML string from an untrusted source.
XmlElement = ET.Element

__all__ = ["XmlElement", "find_first", "require_child", "serialize"]


def serialize(element: ET.Element, *, encoding: str = "unicode") -> str:
    """Serialize an in-memory ``Element`` back to an XML string.

    Pure output — no parsing. ``encoding="unicode"`` returns ``str``
    (the default); pass a real codec name only if you need ``bytes``
    semantics from :func:`xml.etree.ElementTree.tostring`.
    """
    return ET.tostring(element, encoding=encoding)  # type: ignore[no-any-return]


def find_first(element: ET.Element, path: str) -> ET.Element | None:
    """Return the first descendant matching ``path``, or ``None``.

    A thin, typed wrapper over :meth:`Element.find` so call sites read
    intent-first and mypy sees the optional result.
    """
    return element.find(path)


def require_child(element: ET.Element, path: str) -> ET.Element:
    """Like :func:`find_first` but raise ``KeyError`` if absent.

    For the common case where a malformed-but-already-parsed tree is a
    programming/contract error, not untrusted input.
    """
    found = element.find(path)
    if found is None:
        raise KeyError(f"required XML element {path!r} not found")
    return found
