"""Secure XML parsing for the OVA interchange (defusedxml).

These two helpers were the only thing the interchange used from the retiring
umbrella ``mgf.vm.common.xml``. Pure ``defusedxml`` (no ``mgf-vm`` deps), but they
cannot live in ``mgf-vm-core`` — core's import-linter purity contract forbids
``defusedxml`` — so the interchange owns them (it parses OVF/domain XML anyway).
Translate defusedxml's typed refusals to ``ET.ParseError`` so callers' existing
``except ET.ParseError`` catches malformed + unsafe XML uniformly.
"""

from __future__ import annotations

import xml.etree.ElementTree as ET  # output API only — parse goes through defusedxml
from typing import TYPE_CHECKING

from defusedxml import ElementTree as defused_ET
from defusedxml.common import DefusedXmlException

if TYPE_CHECKING:
    from pathlib import Path

__all__ = ["DEFAULT_MAX_XML_BYTES", "parse_xml_file", "parse_xml_string"]

#: Bound on input size before parsing (caps the quadratic-blowup surface that
#: survives defusedxml's entity ban). ~100x VManager's realistic inputs.
DEFAULT_MAX_XML_BYTES = 8 * 1024 * 1024  # 8 MiB


def parse_xml_string(
    source: str | bytes,
    *,
    untrusted: bool = False,
    max_bytes: int = DEFAULT_MAX_XML_BYTES,
) -> ET.Element:
    """Parse XML from a string / bytes payload.

    ``untrusted=True`` additionally forbids DTD declarations (use for input from
    outside the local libvirt daemon — OVF, restore manifests). Rejects payloads
    larger than ``max_bytes``. Raises :class:`xml.etree.ElementTree.ParseError`
    on malformed / unsafe XML or oversize input.
    """
    payload_bytes = source.encode("utf-8", errors="ignore") if isinstance(source, str) else source
    if len(payload_bytes) > max_bytes:
        msg = (
            f"XML payload of {len(payload_bytes)} bytes exceeds the "
            f"{max_bytes}-byte cap; refusing to parse"
        )
        raise ET.ParseError(msg)
    try:
        return defused_ET.fromstring(  # type: ignore[no-any-return]
            source,
            forbid_dtd=untrusted,
            forbid_entities=True,
            forbid_external=True,
        )
    except DefusedXmlException as e:
        raise ET.ParseError(f"refused unsafe XML: {e}") from e


def parse_xml_file(
    path: Path | str,
    *,
    untrusted: bool = False,
    max_bytes: int = DEFAULT_MAX_XML_BYTES,
) -> ET.ElementTree:
    """Parse XML from a file path. See :func:`parse_xml_string`."""
    from pathlib import Path as _Path

    resolved = _Path(path)
    if resolved.is_file() and resolved.stat().st_size > max_bytes:
        msg = (
            f"XML file {resolved} ({resolved.stat().st_size} bytes) exceeds the "
            f"{max_bytes}-byte cap; refusing to parse"
        )
        raise ET.ParseError(msg)
    try:
        return defused_ET.parse(  # type: ignore[no-any-return]
            str(resolved),
            forbid_dtd=untrusted,
            forbid_entities=True,
            forbid_external=True,
        )
    except DefusedXmlException as e:
        raise ET.ParseError(f"refused unsafe XML: {e}") from e
