"""Filesystem-diff renderer + on-demand file extraction.

The capture-side module (`mgf.vm.forensics.capture.filesystem`)
produces baseline.json + diff.json. This module renders them for
human/JSON consumption AND supports `analyze filesystem --extract
PATH` (FN-A3.2) to write one file from the post-launch state into
the case dir for analyst inspection.
"""

from __future__ import annotations

import json
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any

from mgf.vm.core.exceptions import OperationError

from mgf.vm.forensics.capture.filesystem import (
    DIFF_SCHEMA_VERSION,
    FileEntry,
    FilesystemDiff,
)

if TYPE_CHECKING:
    from collections.abc import Sequence
    from datetime import datetime
    from pathlib import Path


__all__ = [
    "DiffRenderRow",
    "DiffSummary",
    "load_diff",
    "render_diff_table",
    "summarize_diff",
]


@dataclass(frozen=True)
class DiffRenderRow:
    """One row in the human-readable diff table."""

    change: str  # "added" | "modified" | "deleted"
    path: str
    size_bytes: int  # 0 for deleted
    sha256: str  # "" for deleted


@dataclass(frozen=True)
class DiffSummary:
    """Coarse counts across a :class:`FilesystemDiff`."""

    added: int
    modified: int
    deleted: int
    captured_at: datetime
    baseline_captured_at: datetime
    vm_state: str
    disk_view: str

    @property
    def total_changes(self) -> int:
        return self.added + self.modified + self.deleted


# ---------------------------------------------------------------------------
# Loaders
# ---------------------------------------------------------------------------


def load_diff(path: Path) -> FilesystemDiff:
    """Load a diff JSON file. Raises :class:`OperationError` on schema mismatch."""
    try:
        raw = json.loads(path.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError) as e:
        msg = f"diff at {path} is unreadable: {e}"
        raise OperationError(msg) from e
    version = raw.get("schema_version")
    if version != DIFF_SCHEMA_VERSION:
        msg = (
            f"diff at {path} has schema_version {version!r}; "
            f"this build expects {DIFF_SCHEMA_VERSION}."
        )
        raise OperationError(msg)
    try:
        from datetime import datetime as _dt

        return FilesystemDiff(
            captured_at=_dt.fromisoformat(str(raw["captured_at"])),
            baseline_captured_at=_dt.fromisoformat(
                str(raw["baseline_captured_at"]),
            ),
            vm_state=str(raw.get("vm_state", "stopped")),
            disk_view=str(raw.get("disk_view", "consistent")),
            added=tuple(_entry_from_dict(e) for e in raw.get("added", [])),
            modified=tuple(_entry_from_dict(e) for e in raw.get("modified", [])),
            deleted=tuple(raw.get("deleted", [])),
            schema_version=version,
        )
    except (KeyError, TypeError, ValueError) as e:
        msg = f"diff at {path} is malformed: {e}"
        raise OperationError(msg) from e


def _entry_from_dict(d: dict[str, Any]) -> FileEntry:
    return FileEntry(
        path=str(d["path"]),
        size_bytes=int(d["size_bytes"]),
        mtime=float(d["mtime"]),
        sha256=str(d.get("sha256", "")),
    )


# ---------------------------------------------------------------------------
# Rendering
# ---------------------------------------------------------------------------


def summarize_diff(diff: FilesystemDiff) -> DiffSummary:
    return DiffSummary(
        added=len(diff.added),
        modified=len(diff.modified),
        deleted=len(diff.deleted),
        captured_at=diff.captured_at,
        baseline_captured_at=diff.baseline_captured_at,
        vm_state=diff.vm_state,
        disk_view=diff.disk_view,
    )


def render_diff_table(diff: FilesystemDiff) -> Sequence[DiffRenderRow]:
    """Flatten a diff into a sorted list of rows for tabular display.

    Order: added rows first, then modified, then deleted; within
    each group sorted by path.
    """
    rows: list[DiffRenderRow] = []
    for e in diff.added:
        rows.append(
            DiffRenderRow(
                change="added",
                path=e.path,
                size_bytes=e.size_bytes,
                sha256=e.sha256,
            ),
        )
    for e in diff.modified:
        rows.append(
            DiffRenderRow(
                change="modified",
                path=e.path,
                size_bytes=e.size_bytes,
                sha256=e.sha256,
            ),
        )
    for path in diff.deleted:
        rows.append(DiffRenderRow(change="deleted", path=path, size_bytes=0, sha256=""))
    return rows
