"""Host-level isolation checks (MALWARE_ANALYSIS.md §5.10, 3 checks).

All INFO + HOST_LEVEL - surfaced for awareness; the actual
fix lives in ``host doctor`` (Phase 15) for the host-state ones,
or in the user's VMSpec for the VM-config ones.

- :func:`host_ksm_disabled` — runtime-only. Reads
  ``host_facts["host.ksm_run"]``: ``"0"`` means disabled (good).
  KSM merges identical pages across VMs — a side-channel surface.
- :func:`host_cpu_pinning` — static. VM XML has explicit
  ``<cputune><vcpupin/></cputune>`` or ``<vcpu cpuset='X'>``.
  Dedicated cores reduce timing-side-channel exposure.
- :func:`host_swap_disabled` — static. VM XML has
  ``<memoryBacking><locked/></memoryBacking>`` so guest pages
  aren't swapped to host disk.
"""

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__ = [
    "host_cpu_pinning",
    "host_ksm_disabled",
    "host_swap_disabled",
]


@isolation_check(runtime_only=True)
def host_ksm_disabled(ctx: AnalysisContext) -> IsolationFinding:
    """Kernel same-page merging is disabled host-wide.

    Reads ``host.ksm_run`` from ``host_facts`` (analyzer collects
    ``/sys/kernel/mm/ksm/run`` — ``"0"`` = disabled).
    """
    ksm_run = str(ctx.host_facts.get("host.ksm_run", ""))
    return finding(
        fid="host.ksm_disabled",
        title="Kernel same-page merging disabled",
        category=IsolationCategory.INFO,
        fix_kind=FixKind.HOST_LEVEL,
        passing=ksm_run in ("0", ""),  # absent or 0 = disabled
        summary_pass=(
            "KSM is disabled (no cross-VM page merging)"
            if ksm_run == "0"
            else "KSM state could not be determined; treating as disabled"
        ),
        summary_fail=(
            f"KSM is enabled (/sys/kernel/mm/ksm/run = {ksm_run!r}); "
            f"cross-VM page merging is a side-channel surface. "
            f"Disable via `host doctor` or "
            f"`echo 0 | sudo tee /sys/kernel/mm/ksm/run`."
        ),
        details={"ksm_run": ksm_run},
    )


@isolation_check
def host_cpu_pinning(ctx: AnalysisContext) -> IsolationFinding:
    """VM has dedicated CPU pinning.

    Either ``<vcpu cpuset='X-Y'>`` OR ``<cputune><vcpupin
    vcpu='N' cpuset='X'/>`` for every vcpu.
    """
    root = ctx.root()
    vcpu = root.find("vcpu")
    has_cpuset = vcpu is not None and bool(vcpu.get("cpuset"))

    cputune = root.find("cputune")
    has_vcpupin = cputune is not None and bool(cputune.findall("vcpupin"))

    pinned = has_cpuset or has_vcpupin
    if has_cpuset and vcpu is not None:
        summary_pass = f"vcpu cpuset='{vcpu.get('cpuset')}'"
    elif has_vcpupin and cputune is not None:
        summary_pass = f"<cputune> with {len(cputune.findall('vcpupin'))} vcpupin entries"
    else:
        summary_pass = ""
    return finding(
        fid="host.cpu_pinning",
        title="VM has dedicated CPU pinning",
        category=IsolationCategory.INFO,
        fix_kind=FixKind.MANUAL,  # user picks the pinned cores
        passing=pinned,
        summary_pass=summary_pass,
        summary_fail=(
            "no CPU pinning declared - vcpus float across host cores; "
            "timing-side-channel surface. Pin via "
            "`<vcpu cpuset='X-Y'>` or `<cputune><vcpupin/></cputune>`."
        ),
        details={"vcpu_cpuset": vcpu.get("cpuset", "") if vcpu is not None else ""},
    )


@isolation_check
def host_swap_disabled(ctx: AnalysisContext) -> IsolationFinding:
    """Guest memory is not swappable to host disk.

    Detected via ``<memoryBacking><locked/></memoryBacking>``
    — locks guest pages in RAM so they never hit swap.
    """
    backing = ctx.root().find("memoryBacking")
    has_locked = backing is not None and backing.find("locked") is not None
    return finding(
        fid="host.swap_disabled",
        title="Guest memory locked (not swappable)",
        category=IsolationCategory.INFO,
        fix_kind=FixKind.MANUAL,  # user weighs RAM-pressure tradeoff
        passing=has_locked,
        summary_pass="<memoryBacking><locked/></memoryBacking> declared",
        summary_fail=(
            "guest memory is swappable to host disk — under RAM pressure "
            "the host kernel may write guest pages to swap, leaving them "
            "on persistent storage after the VM exits. Add "
            "<memoryBacking><locked/></memoryBacking> if you want to "
            "prevent this (note: requires CAP_IPC_LOCK)."
        ),
        details={"has_memory_backing": backing is not None},
    )
