"""Memory-dump capture over an injected ``dump_fn`` (D-G22).

Thin wrapper that drives an injected memory-dump callable, hashes the
resulting file, and packages it as an :class:`Artifact`. The callable is
engine-specific (the app binds it to the libvirt acquisition escape hatch);
this module stays provider-free. Volatility/Rekall integration is deferred
to Phase 17-C; for v1 we just persist the raw dump.
"""

from __future__ import annotations

import hashlib
from datetime import UTC, datetime
from typing import TYPE_CHECKING

from mgf.vm.forensics.case import Artifact, ArtifactKind

if TYPE_CHECKING:
    from collections.abc import Callable
    from pathlib import Path

    # ``(dest, live) -> None`` — write the RAM dump to ``dest``.
    DumpMemoryFn = Callable[[Path, bool], None]


__all__ = ["capture_memory"]


def capture_memory(
    dump_fn: DumpMemoryFn,
    vm_name: str,
    *,
    case_dir: Path,
    live: bool = True,
    captured_at: datetime | None = None,
    name: str | None = None,
) -> Artifact:
    """Dump RAM to ``<case_dir>/memory/<name>``; return an :class:`Artifact`.

    ``dump_fn(dest, live)`` performs the engine-specific dump. ``name``
    defaults to a timestamped slug (``memory-YYYYmmddTHHMMSS.dump``).
    ``live=True`` keeps the VM running across the dump; ``live=False``
    powers off after.
    """
    when = captured_at or datetime.now(UTC)
    filename = name or f"memory-{when.strftime('%Y%m%dT%H%M%S')}.dump"

    memory_dir = case_dir / "memory"
    memory_dir.mkdir(parents=True, exist_ok=True)
    dest = memory_dir / filename

    dump_fn(dest, live)

    return Artifact(
        kind=ArtifactKind.MEMORY_DUMP,
        path=dest,
        sha256=_hash_file(dest),
        size_bytes=dest.stat().st_size,
        captured_at=when,
        captured_by="mgf.vm.forensics.capture.memory",
        metadata={"live": live, "vm_name": vm_name},
    )


def _hash_file(path: Path, *, chunk_size: int = 1 << 16) -> str:
    h = hashlib.sha256()
    with path.open("rb") as f:
        while True:
            chunk = f.read(chunk_size)
            if not chunk:
                break
            h.update(chunk)
    return h.hexdigest()
