"""Runtime-only isolation checks (MALWARE_ANALYSIS.md §5.9, 4 checks).

Every check in this module reads from
:attr:`AnalysisContext.host_facts` — a dict the runtime analyzer
populates BEFORE running checks. Static analysis skips these
(they're registered with ``runtime_only=True``).

The ``host_facts`` keys this module reads:

- ``libvirt.uri``          — the libvirt connection URI string
                             (e.g., ``qemu:///system``)
- ``apparmor.profile_state`` — ``"enforce"`` / ``"complain"`` / ``""``
- ``qemu.open_paths``      — list of host paths the qemu process has
                             open (excluding sanctioned ones —
                             disk, UEFI vars, sockets)
- ``network.active_state`` — list of ``(network_name, is_isolated)``
                             tuples observed on the live libvirt host

Tests inject these directly via the context's ``host_facts`` dict;
the runtime analyzer (MA-A3.3) collects them in production.
"""

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.types import (
    FixKind,
    IsolationCategory,
    IsolationFinding,
    isolation_check,
)

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


__all__ = [
    "runtime_apparmor_enforcing",
    "runtime_libvirt_uri_system",
    "runtime_network_active_isolated",
    "runtime_no_open_host_paths",
]


@isolation_check(runtime_only=True)
def runtime_libvirt_uri_system(ctx: AnalysisContext) -> IsolationFinding:
    """Connection is ``qemu:///system``, not ``qemu:///session``.

    Session mode runs qemu under the unprivileged user account
    without libvirt-qemu AppArmor confinement; the security model
    is fundamentally weaker. Malware analysis MUST use system mode.
    """
    uri = str(ctx.host_facts.get("libvirt.uri", ""))
    return finding(
        fid="runtime.libvirt_uri_system",
        title="libvirt URI is qemu:///system",
        category=IsolationCategory.BLOCKING,
        fix_kind=FixKind.MANUAL,  # user-level config; cannot patch live
        passing=uri == "qemu:///system",
        summary_pass=f"connected via {uri}",
        summary_fail=(
            f"connected via {uri!r} — session mode lacks AppArmor "
            f"confinement and runs qemu unprivileged. Use qemu:///system."
        ),
        details={"uri": uri},
    )


@isolation_check(runtime_only=True)
def runtime_apparmor_enforcing(ctx: AnalysisContext) -> IsolationFinding:
    """qemu process is confined by an AppArmor profile in enforcing mode."""
    state = str(ctx.host_facts.get("apparmor.profile_state", ""))
    profile = str(ctx.host_facts.get("apparmor.profile_name", ""))
    return finding(
        fid="runtime.apparmor_enforcing",
        title="qemu process AppArmor enforcing",
        category=IsolationCategory.BLOCKING,
        # HOST_LEVEL because the fix is "enable AppArmor + libvirt-qemu
        # profile" which lives in `host doctor`, not in this analyzer.
        fix_kind=FixKind.HOST_LEVEL,
        passing=state == "enforce",
        summary_pass=(f"qemu confined by AppArmor profile {profile!r} (enforce)"),
        summary_fail=(
            f"qemu AppArmor state is {state!r} (expected 'enforce'); "
            f"profile name: {profile!r}. Run `vmanager host doctor` to fix."
        ),
        details={"state": state, "profile": profile},
    )


@isolation_check(runtime_only=True)
def runtime_no_open_host_paths(ctx: AnalysisContext) -> IsolationFinding:
    """qemu process has no FDs open against unsanctioned host paths.

    Sanctioned paths are: the VM's disk file(s) under the quarantine
    pool, NVRAM under the same pool, libvirt's runtime sockets in
    ``/var/run/libvirt/``, and pipes / anon-inodes that don't have a
    real path. Anything else (e.g., a stray ``/etc`` open) means
    something escaped containment.
    """
    paths = ctx.host_facts.get("qemu.open_paths", [])
    if not isinstance(paths, list):
        paths = []
    suspect = list(paths)
    return finding(
        fid="runtime.no_open_host_paths",
        title="qemu process has no unsanctioned host paths open",
        category=IsolationCategory.BLOCKING,
        fix_kind=FixKind.MANUAL,
        passing=not suspect,
        summary_pass="every open FD points at a sanctioned path",
        summary_fail=(
            f"qemu has {len(suspect)} open FD(s) against unsanctioned "
            f"host path(s) — investigate immediately: {suspect}"
        ),
        details={"suspect_paths": suspect},
    )


@isolation_check(runtime_only=True)
def runtime_network_active_isolated(ctx: AnalysisContext) -> IsolationFinding:
    """The live libvirt-network state confirms the VM's network is isolated.

    Reads ``network.active_state`` — a list of ``(name, is_isolated)``
    tuples populated by the runtime analyzer from
    ``virsh net-dumpxml``. Static checks already verify the VM
    points at ``vmanager-quarantine``; this check verifies the
    network resource itself hasn't been mutated since (no
    ``<forward>`` element added, no DHCP / DNS enabled).
    """
    state_raw = ctx.host_facts.get("network.active_state", [])
    state: list[tuple[str, bool]] = list(state_raw) if isinstance(state_raw, list) else []
    not_isolated = [name for name, isolated in state if not isolated]
    return finding(
        fid="runtime.network_active_isolated",
        title="Live libvirt network state confirms isolation",
        category=IsolationCategory.BLOCKING,
        fix_kind=FixKind.AUTO_FIX,
        passing=not not_isolated,
        summary_pass=(
            f"live network state OK ({len(state)} attached network(s) confirmed isolated)"
            if state
            else "no attached networks (air-gap mode)"
        ),
        summary_fail=(
            f"{len(not_isolated)} attached network(s) have drifted from "
            f"isolated: {not_isolated}. The vmanager-quarantine network "
            f"may have been mutated; run `vm security apply --repair-network`."
        ),
        details={"state": state, "drifted": not_isolated},
        fix_executor="network.swap_to_quarantine",
    )
