"""Network-isolation checks (MALWARE_ANALYSIS.md §5.1, 6 checks).

Every network-side leakage path the static analyzer knows about:

- :func:`net_no_external_route` — interface attached to a libvirt
  network with no ``<forward>`` element.
- :func:`net_quarantine_network_used` — interface attached to the
  authoritative ``vmanager-quarantine`` network.
- :func:`net_no_macvtap_bridge` — no ``<interface type='direct'>``.
- :func:`net_no_passthrough_nic` — no PCI ``<hostdev>`` of network
  type.
- :func:`net_no_dhcp` — no DHCP block present (verified at
  network-config time; for the per-VM check the ``<source>`` must
  point at the quarantine network where DHCP is removed by §7.1).
- :func:`net_no_dns_forwarding` — no DNS forwarder configured
  (same story — checked indirectly via the quarantine network
  fingerprint).

The DHCP / DNS pair is technically a libvirt-network concern, not
a per-VM XML field. We check them at the per-VM level by verifying
the interface is attached to the canonical quarantine network; the
authoritative DHCP / DNS / forward verification of the network
itself lives in the adapter-bound ``quarantine`` module (MA-A5,
DEFERRED — not ported to the pure core).

These checks are pure: their only adapter coupling in the original
was ``QUARANTINE_NETWORK_NAME``, which is now a pure constant in
:mod:`mgf.vm.core.security.isolation.context`. The element walk uses
``AnalysisContext.find_devices`` (also pure); nothing here touches
``defusedxml`` / ``static_analyzer``.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

from mgf.vm.core.security.isolation.checks._helpers import finding
from mgf.vm.core.security.isolation.context import QUARANTINE_NETWORK_NAME
from mgf.vm.core.security.isolation.types import (
    FixKind,
    IsolationCategory,
    IsolationFinding,
    isolation_check,
)

if TYPE_CHECKING:
    from mgf.vm.core.security.isolation.context import AnalysisContext


__all__ = [
    "net_no_dhcp",
    "net_no_dns_forwarding",
    "net_no_external_route",
    "net_no_macvtap_bridge",
    "net_no_passthrough_nic",
    "net_quarantine_network_used",
]


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _network_interfaces(ctx: AnalysisContext) -> list[tuple[str, str | None]]:
    """Return ``[(iface_type, source_network_or_dev), ...]`` for every NIC.

    For ``type='network'`` interfaces, the second element is the
    network name (from ``<source network='X'/>``). For
    ``type='direct'`` (macvtap), it's the source dev. For
    ``type='bridge'``, the bridge name. ``None`` when the source
    can't be read.
    """
    out: list[tuple[str, str | None]] = []
    for iface in ctx.find_devices("interface"):
        iface_type = iface.get("type", "")
        source = iface.find("source")
        if source is None:
            out.append((iface_type, None))
            continue
        if iface_type == "network":
            out.append((iface_type, source.get("network")))
        elif iface_type == "direct":
            out.append((iface_type, source.get("dev")))
        elif iface_type == "bridge":
            out.append((iface_type, source.get("bridge")))
        else:
            out.append((iface_type, None))
    return out


# ---------------------------------------------------------------------------
# Checks
# ---------------------------------------------------------------------------


@isolation_check
def net_quarantine_network_used(ctx: AnalysisContext) -> IsolationFinding:
    """Every NIC must be attached to ``vmanager-quarantine``."""
    nets = _network_interfaces(ctx)
    network_nics = [(t, n) for t, n in nets if t == "network"]
    bad = [(t, n) for t, n in network_nics if n != QUARANTINE_NETWORK_NAME]

    return finding(
        fid="net.quarantine_network_used",
        title="Every NIC attached to the quarantine network",
        category=IsolationCategory.BLOCKING,
        fix_kind=FixKind.AUTO_FIX,
        passing=not bad and bool(network_nics or not nets),
        summary_pass=(
            f"every <interface type='network'> uses '{QUARANTINE_NETWORK_NAME}'"
            if network_nics
            else "no network interfaces attached"
        ),
        summary_fail=(
            f"{len(bad)} interface(s) on a non-quarantine network: {[n for _, n in bad]}"
        ),
        details={"interfaces": [{"type": t, "source": n} for t, n in nets]},
        fix_executor="network.swap_to_quarantine",
    )


@isolation_check
def net_no_external_route(ctx: AnalysisContext) -> IsolationFinding:
    """No NIC attached to a libvirt network that may have a ``<forward>``.

    From the per-VM XML alone we can't see the ``<forward>`` element
    of a libvirt network — that lives on the network resource itself.
    We approximate by checking that every ``type='network'`` NIC
    points at the canonical ``vmanager-quarantine`` network (which
    is verified to have no forward by the quarantine-network drift
    detector in MA-A5). Anything else is treated as a leakage path.
    """
    nets = _network_interfaces(ctx)
    risky = [(t, n) for t, n in nets if t == "network" and n != QUARANTINE_NETWORK_NAME]
    return finding(
        fid="net.no_external_route",
        title="No NIC on a routable libvirt network",
        category=IsolationCategory.BLOCKING,
        fix_kind=FixKind.AUTO_FIX,
        passing=not risky,
        summary_pass="no NICs on networks that could have a <forward> element",
        summary_fail=(
            f"{len(risky)} NIC(s) on non-quarantine networks "
            f"(e.g., 'default' has NAT forward by libvirt convention): "
            f"{[n for _, n in risky]}"
        ),
        details={"risky_networks": [n for _, n in risky]},
        fix_executor="network.swap_to_quarantine",
    )


@isolation_check
def net_no_macvtap_bridge(ctx: AnalysisContext) -> IsolationFinding:
    """No ``<interface type='direct'>`` (macvtap on a physical NIC)."""
    macvtap = [(t, n) for t, n in _network_interfaces(ctx) if t == "direct"]
    return finding(
        fid="net.no_macvtap_bridge",
        title="No macvtap interface attached to physical NIC",
        category=IsolationCategory.BLOCKING,
        fix_kind=FixKind.AUTO_FIX,
        passing=not macvtap,
        summary_pass="no macvtap interfaces declared",
        summary_fail=(f"{len(macvtap)} macvtap interface(s) — direct path to the physical network"),
        details={"sources": [n for _, n in macvtap]},
        fix_executor="strip.iface.macvtap",
    )


@isolation_check
def net_no_passthrough_nic(ctx: AnalysisContext) -> IsolationFinding:
    """No PCI ``<hostdev>`` of network type."""
    network_hostdevs: list[str] = []
    for hostdev in ctx.find_devices("hostdev"):
        if hostdev.get("mode") != "subsystem":
            continue
        if hostdev.get("type") != "pci":
            continue
        # libvirt doesn't tag a hostdev as "network" — we conservatively
        # treat any PCI hostdev as a candidate (the no_pci check catches
        # them all anyway). Here we surface the subset that has
        # network-class semantics declared via the optional <alias>.
        alias = hostdev.find("alias")
        alias_name = alias.get("name", "") if alias is not None else ""
        if alias_name.startswith("net") or "ethernet" in alias_name.lower():
            network_hostdevs.append(alias_name)

    return finding(
        fid="net.no_passthrough_nic",
        title="No NIC passed through via PCI hostdev",
        category=IsolationCategory.BLOCKING,
        fix_kind=FixKind.AUTO_FIX,
        passing=not network_hostdevs,
        summary_pass="no PCI-passthrough NIC declared",
        summary_fail=(
            f"{len(network_hostdevs)} PCI-passthrough NIC(s) — guest "
            f"has direct hardware access to the network"
        ),
        details={"aliases": network_hostdevs},
        fix_executor="strip.iface.passthrough",
    )


@isolation_check
def net_no_dhcp(ctx: AnalysisContext) -> IsolationFinding:
    """No DHCP path — guest can't auto-configure routable IP.

    The per-VM XML doesn't carry the network's DHCP state. We
    surface this as ADVISORY and reduce the verdict to "depends on
    quarantine network state" — the authoritative check fires from
    the adapter-bound ``quarantine`` module (MA-A5) drift detection.
    """
    nets = _network_interfaces(ctx)
    on_quarantine = all(not (t == "network" and n != QUARANTINE_NETWORK_NAME) for t, n in nets)
    return finding(
        fid="net.no_dhcp",
        title="DHCP not available to guest",
        category=IsolationCategory.ADVISORY,
        fix_kind=FixKind.AUTO_FIX,
        passing=on_quarantine,
        summary_pass=(
            "every network interface points at the quarantine network "
            "(verified to have no DHCP by the network drift detector)"
        ),
        summary_fail=(
            "at least one interface is on a non-quarantine network — "
            "DHCP state must be verified at the libvirt-network level"
        ),
        fix_executor="network.swap_to_quarantine",
    )


@isolation_check
def net_no_dns_forwarding(ctx: AnalysisContext) -> IsolationFinding:
    """No DNS forwarder accessible from the guest.

    Per-VM equivalent to :func:`net_no_dhcp` — the actual DNS
    config lives on the libvirt network. We check the same pointer
    indirectly.
    """
    nets = _network_interfaces(ctx)
    on_quarantine = all(not (t == "network" and n != QUARANTINE_NETWORK_NAME) for t, n in nets)
    return finding(
        fid="net.no_dns_forwarding",
        title="No DNS forwarder reachable from guest",
        category=IsolationCategory.ADVISORY,
        fix_kind=FixKind.AUTO_FIX,
        passing=on_quarantine,
        summary_pass=(
            "every network interface points at the quarantine network "
            "(verified to have <dns enable='no'/> by the drift detector)"
        ),
        summary_fail=(
            "at least one interface is on a non-quarantine network — "
            "DNS forwarding state must be verified at the libvirt-network level"
        ),
        fix_executor="network.swap_to_quarantine",
    )
