"""N — :class:`LibvirtNetworkBackend`, the OPTIONAL ``NetworkBackend`` (D-G15).

Implements ``mgf.vm.core.contracts.NetworkBackend`` (Wave-R3) against libvirt —
virtual-network **management** (define / start / stop / delete), the write-side
counterpart of the D-G14 observation reads on
:class:`mgf.vm.libvirt._hypervisor.LibvirtHypervisorBackend`.

It lives in **its own module** (like :class:`LibvirtMetricsBackend`, D-G13)
precisely because ``NetworkBackend`` is a *separate, injectable* Protocol
(D-G15): a composition that wants network management constructs one of these
over the **same** :class:`mgf.vm.libvirt._connection.LibvirtConnection` the
hypervisor backend holds and injects it; a composition that wants none simply
never builds it. Because both speak to one ``libvirtd``, management's effects
are immediately observable through the hypervisor's ``list_networks`` (which is
what the CONF-N-01 conformance scenario relies on).

Built on the foundation slice, like the other backends:

* every native libvirt call routes through the RLock-guarded connection
  (``with self._conn.lock``, §8 thread-safety);
* the public methods are decorated with
  :func:`mgf.vm.libvirt._errors.translate_at_seam`, so a raw ``libvirtError``
  becomes a typed ``mgf.vm.core.exceptions`` error at the seam (§7);
* the :class:`~mgf.vm.core.contracts.NetworkSpec` → libvirt network-XML
  mapping lives here (the adapter owns the native format; D-G15) and is built
  deterministically with stdlib ElementTree (same convention as the domain XML
  generator), so no engine-ism leaks across the neutral contract.

The umbrella's ``mgf.vm.network.manager.build_network_xml`` is the historic
source for the XML shape; this is its engine-neutral-spec-driven successor.
"""

from __future__ import annotations

from ipaddress import IPv4Network
from typing import TYPE_CHECKING, Any
from xml.etree import ElementTree as ET

from defusedxml import ElementTree as DefusedET

from mgf.vm.libvirt._errors import LibvirtErrorTranslator, translate_at_seam

if TYPE_CHECKING:
    from types import TracebackType

    from mgf.vm.core.contracts import NetworkProfile, NetworkSpec

    from mgf.vm.libvirt._connection import LibvirtConnection


def network_spec_to_xml(spec: NetworkSpec) -> str:
    """Render a neutral :class:`NetworkSpec` to libvirt network XML (D-G15).

    Deterministic (fixed element order, stdlib ElementTree). ``forward_mode``
    ``None`` emits **no** ``<forward>`` (an isolated network); ``"bridge"``
    emits a bridge forward over the named host bridge; ``"nat"`` / ``"route"``
    emit that forward mode plus an engine-managed ``<ip>`` block (gateway +
    optional DHCP range) when a ``cidr`` is set. ``dns_enabled=False`` emits
    ``<dns enable='no'/>`` (the malware-analysis isolation shape).
    """
    net = ET.Element("network")
    ET.SubElement(net, "name").text = spec.name

    if spec.forward_mode == "bridge":
        ET.SubElement(net, "forward", {"mode": "bridge"})
        ET.SubElement(net, "bridge", {"name": spec.bridge or spec.name})
        return ET.tostring(net, encoding="unicode")

    if spec.forward_mode is not None:
        ET.SubElement(net, "forward", {"mode": spec.forward_mode})
    if spec.bridge:
        ET.SubElement(net, "bridge", {"name": spec.bridge})
    if not spec.dns_enabled:
        ET.SubElement(net, "dns", {"enable": "no"})
    if spec.cidr is not None:
        _append_ip(net, spec.cidr, dhcp=spec.dhcp)
    return ET.tostring(net, encoding="unicode")


def _append_ip(net: ET.Element, cidr: str, *, dhcp: bool) -> None:
    ipnet = IPv4Network(cidr, strict=False)
    gateway = str(ipnet.network_address + 1)
    ip_el = ET.SubElement(net, "ip", {"address": gateway, "netmask": str(ipnet.netmask)})
    if dhcp:
        dhcp_el = ET.SubElement(ip_el, "dhcp")
        ET.SubElement(
            dhcp_el,
            "range",
            {
                "start": str(ipnet.network_address + 2),
                "end": str(ipnet.broadcast_address - 1),
            },
        )


class LibvirtNetworkBackend:
    """``NetworkBackend`` over a single libvirt connection (D-G15).

    Construct with an already-wrapped :class:`LibvirtConnection` — the same one
    the hypervisor backend uses — so a composition injects this alongside the
    lifecycle + observation surface without opening a second connection.
    """

    def __init__(self, connection: LibvirtConnection) -> None:
        self._conn = connection
        self._translator = LibvirtErrorTranslator()

    # -- seam glue ----------------------------------------------------------

    def translate_exception(self, exc: Exception) -> Any:
        """Delegate to the foundation translator (used by ``translate_at_seam``)."""
        return self._translator.translate_exception(exc)

    # -- the NetworkBackend Protocol ----------------------------------------

    @translate_at_seam
    def define_network(self, spec: NetworkSpec) -> NetworkProfile:
        """Define a network from ``spec`` (persistent, inactive); return its
        observed :class:`NetworkProfile`.

        Maps the neutral spec to network XML and calls ``networkDefineXML``
        (which defines persistently, redefining an existing same-named network
        — libvirt's idempotent define semantics). Sets the autostart flag
        best-effort. Does not start the network (the caller follows with
        :meth:`start_network`).
        """
        xml = network_spec_to_xml(spec)
        with self._conn.lock:
            conn = self._conn.raw()
            net = conn.networkDefineXML(xml)
            try:
                net.setAutostart(spec.autostart)
            except Exception:  # noqa: S110 - autostart is best-effort
                pass
            return self._profile(net)

    @translate_at_seam
    def start_network(self, name: str) -> None:
        """Activate ``name`` (idempotent — already-active is success). Raises
        ``NetworkNotFound`` if the network is not defined."""
        with self._conn.lock:
            net = self._lookup(self._conn.raw(), name)
            if not net.isActive():
                net.create()

    @translate_at_seam
    def stop_network(self, name: str) -> None:
        """Deactivate ``name`` (idempotent — already-stopped is success). Raises
        ``NetworkNotFound`` if the network is not defined."""
        with self._conn.lock:
            net = self._lookup(self._conn.raw(), name)
            if net.isActive():
                net.destroy()

    @translate_at_seam
    def delete_network(self, name: str) -> None:
        """Undefine ``name`` (stopping it first if active). Idempotent — an
        already-absent network is a no-op success (D-G15)."""
        with self._conn.lock:
            conn = self._conn.raw()
            try:
                net = conn.networkLookupByName(name)
            except Exception:
                return
            if net.isActive():
                net.destroy()
            net.undefine()

    # -- helpers (under the caller's lock hold) -----------------------------

    @staticmethod
    def _lookup(conn: Any, name: str) -> Any:
        """Resolve a ``virNetwork`` by name, normalising failure to not-found.

        Caller MUST hold the connection lock. An absent network raises
        ``VIR_ERR_NO_NETWORK`` under the bare seam, which is not in the typed
        tree, so we collapse any lookup failure to ``NetworkNotFound`` (an
        ``AppError`` that passes the seam unchanged).
        """
        from mgf.vm.core.exceptions import NetworkNotFound

        try:
            return conn.networkLookupByName(name)
        except Exception as exc:
            raise NetworkNotFound(f"no network {name!r}") from exc

    @staticmethod
    def _profile(net: Any) -> NetworkProfile:
        """Build a :class:`NetworkProfile` from a ``virNetwork`` (mirrors
        :meth:`LibvirtHypervisorBackend._network_profile`)."""
        from mgf.vm.core.contracts import NetworkProfile

        try:
            name = str(net.name())
        except Exception:
            name = ""
        try:
            bridge: str | None = str(net.bridgeName())
        except Exception:
            bridge = None
        try:
            active = bool(net.isActive())
        except Exception:
            active = False
        forward_mode: str | None = None
        try:
            root = DefusedET.fromstring(net.XMLDesc(0))
            forward_el = root.find("forward")
            if forward_el is not None:
                forward_mode = forward_el.get("mode")
        except Exception:
            forward_mode = None
        return NetworkProfile(
            name=name,
            bridge=bridge or None,
            active=active,
            forward_mode=forward_mode,
        )

    # -- lifecycle ----------------------------------------------------------

    def __enter__(self) -> LibvirtNetworkBackend:
        return self

    def __exit__(
        self,
        exc_type: type[BaseException] | None,
        exc: BaseException | None,
        tb: TracebackType | None,
    ) -> None:
        # The connection lifecycle is owned by whoever opened it (shared with
        # the hypervisor backend), so this is a no-op exit.
        return None


__all__ = ["LibvirtNetworkBackend", "network_spec_to_xml"]
