"""Disk-isolation checks (MALWARE_ANALYSIS.md §5.2, 4 checks).

- :func:`disk_in_quarantine_pool` — every ``<disk>`` source path
  is under the canonical quarantine pool.
- :func:`disk_no_shared_backing` — no disk uses a backing chain
  reaching outside the quarantine pool (manual; can't auto-fix).
- :func:`disk_cache_safe` — ``cache='none'`` everywhere (avoids
  host page-cache leaks).
- :func:`disk_discard_off` — ``discard='ignore'`` or absent
  (avoids TRIM passing through to underlying SSD).
"""

from __future__ import annotations

from pathlib import Path
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 xml.etree.ElementTree import Element

    from mgf.vm.core.security.isolation.context import AnalysisContext


__all__ = [
    "disk_cache_safe",
    "disk_discard_off",
    "disk_in_quarantine_pool",
    "disk_no_shared_backing",
]


def _quarantine_pool_path() -> Path:
    """Lazily compute the canonical quarantine-pool absolute path.

    Resolved at call time per CF-04 so monkeypatched ``$HOME`` works
    in tests.
    """
    return (Path("~").expanduser() / ".local/share/vmanager/quarantine").resolve()


def _disk_source_paths(ctx: AnalysisContext) -> list[tuple[Element, str | None]]:
    """Return ``[(<disk> element, source path or None), ...]``.

    Reads ``<source file='...'/>`` for file-backed disks. Skips
    ``<source dev='...'/>`` (block-backed) and ``<source dir='...'/>``
    (directory-backed) intentionally — those are surface-level
    invalid for malware-analysis VMs and surface via the in-pool
    check below.
    """
    out: list[tuple[Element, str | None]] = []
    for disk in ctx.find_devices("disk"):
        source = disk.find("source")
        if source is None:
            out.append((disk, None))
            continue
        out.append((disk, source.get("file") or source.get("dev") or source.get("dir")))
    return out


@isolation_check
def disk_in_quarantine_pool(ctx: AnalysisContext) -> IsolationFinding:
    """Every disk source path lives under the canonical quarantine pool."""
    pool_root = _quarantine_pool_path()
    rows = _disk_source_paths(ctx)
    outside: list[str] = []
    for _, path in rows:
        if path is None:
            outside.append("(no source path)")
            continue
        try:
            resolved = Path(path).resolve()
        except (OSError, RuntimeError):
            outside.append(path)
            continue
        try:
            resolved.relative_to(pool_root)
        except ValueError:
            outside.append(str(resolved))

    return finding(
        fid="disk.in_quarantine_pool",
        title="Every disk under the quarantine pool",
        category=IsolationCategory.BLOCKING,
        fix_kind=FixKind.MANUAL,
        passing=not outside,
        summary_pass=(
            f"every disk source resides under {pool_root} ({len(rows)} disk(s) checked)"
            if rows
            else "no disks declared"
        ),
        summary_fail=(f"{len(outside)} disk(s) outside the quarantine pool: {outside}"),
        details={"pool_root": str(pool_root), "outside": outside},
    )


@isolation_check
def disk_no_shared_backing(ctx: AnalysisContext) -> IsolationFinding:
    """No disk uses a backing chain whose backing file lives outside
    the quarantine pool.

    libvirt records explicit backing chains via ``<backingStore>``
    nested under ``<source>``. A spec-synthesised XML never has
    backing chains, so this check trivially passes for pre-create
    flows; an existing-VM XML with a backing chain pointing at a
    shared base image surfaces here.
    """
    pool_root = _quarantine_pool_path()
    bad: list[str] = []
    for disk in ctx.find_devices("disk"):
        for backing in disk.findall(".//backingStore/source"):
            file_attr = backing.get("file") or backing.get("dev") or backing.get("dir")
            if file_attr is None:
                continue
            try:
                resolved = Path(file_attr).resolve()
            except (OSError, RuntimeError):
                bad.append(file_attr)
                continue
            try:
                resolved.relative_to(pool_root)
            except ValueError:
                bad.append(str(resolved))

    return finding(
        fid="disk.no_shared_backing",
        title="No backing chain reaching outside the quarantine pool",
        category=IsolationCategory.BLOCKING,
        fix_kind=FixKind.MANUAL,
        passing=not bad,
        summary_pass=(f"no backing chain crosses the quarantine boundary (pool: {pool_root})"),
        summary_fail=(f"{len(bad)} backing-chain entr(y/ies) outside the pool: {bad}"),
        details={"outside": bad},
    )


@isolation_check
def disk_cache_safe(ctx: AnalysisContext) -> IsolationFinding:
    """Every disk uses ``cache='none'`` to bypass host page cache."""
    bad: list[dict[str, str]] = []
    total = 0
    for disk in ctx.find_devices("disk"):
        total += 1
        driver = disk.find("driver")
        cache = driver.get("cache", "") if driver is not None else ""
        if cache != "none":
            target = disk.find("target")
            tgt_dev = target.get("dev", "?") if target is not None else "?"
            bad.append({"target": tgt_dev, "cache": cache or "(default)"})

    return finding(
        fid="disk.cache_safe",
        title="Every disk uses cache='none'",
        category=IsolationCategory.ADVISORY,
        fix_kind=FixKind.AUTO_FIX,
        passing=not bad,
        summary_pass=(
            f"all {total} disk(s) use cache='none' (no host page-cache leakage)"
            if total
            else "no disks declared"
        ),
        summary_fail=(f"{len(bad)} disk(s) with cache != 'none': {bad}"),
        details={"misconfigured": bad},
        fix_executor="disk.set_cache_none",
    )


@isolation_check
def disk_discard_off(ctx: AnalysisContext) -> IsolationFinding:
    """No disk passes TRIM through to the underlying storage.

    ``discard='unmap'`` lets the guest tell the host backing storage
    to discard blocks — a side-channel observation path. Either
    explicit ``discard='ignore'`` OR no ``discard`` attribute at all
    (libvirt default = ignore) is acceptable.
    """
    bad: list[dict[str, str]] = []
    for disk in ctx.find_devices("disk"):
        driver = disk.find("driver")
        discard = driver.get("discard", "") if driver is not None else ""
        if discard == "unmap":
            target = disk.find("target")
            tgt_dev = target.get("dev", "?") if target is not None else "?"
            bad.append({"target": tgt_dev, "discard": discard})

    return finding(
        fid="disk.discard_off",
        title="No disk forwards TRIM to underlying storage",
        category=IsolationCategory.ADVISORY,
        fix_kind=FixKind.AUTO_FIX,
        passing=not bad,
        summary_pass="every disk has discard='ignore' or absent (default)",
        summary_fail=(f"{len(bad)} disk(s) with discard='unmap': {bad}"),
        details={"misconfigured": bad},
        fix_executor="disk.set_discard_ignore",
    )
