"""Cross-source event timeline.

Merges sorted events from network captures, filesystem diffs,
screenshots, and memory dumps into a single chronological list.
v1 reads what's already on disk — no live tail (deferred to FN-A2.3).

The Markdown report (FN-A5) embeds this as a table.
"""

from __future__ import annotations

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

from mgf.vm.forensics.case import ArtifactKind

if TYPE_CHECKING:
    from collections.abc import Sequence

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


__all__ = [
    "TimelineEvent",
    "build_timeline",
]


@dataclass(frozen=True)
class TimelineEvent:
    """One row in the cross-source timeline.

    ``source`` is one of: ``"capture.network.start"``,
    ``"capture.network.stop"``, ``"capture.fs.baseline"``,
    ``"capture.fs.diff.added"`` (one per added file, capped),
    ``"capture.fs.diff.modified"``, ``"capture.fs.diff.deleted"``,
    ``"capture.memory"``, ``"capture.screenshot"``.
    """

    occurred_at: datetime
    source: str
    summary: str
    detail: dict[str, Any]


def build_timeline(
    case: Case,
    *,
    fs_diff_event_cap: int = 100,
) -> Sequence[TimelineEvent]:
    """Build a timeline from the artifacts already attached to ``case``.

    ``fs_diff_event_cap`` caps the per-bucket (added/modified/
    deleted) event count so a 50,000-file diff doesn't fill the
    timeline. The capped buckets surface as a single summary event
    instead.
    """
    events: list[TimelineEvent] = []
    for artifact in case.artifacts:
        events.extend(_events_for_artifact(artifact, cap=fs_diff_event_cap))
    events.sort(key=lambda e: e.occurred_at)
    return tuple(events)


def _events_for_artifact(
    artifact: Artifact,
    *,
    cap: int,
) -> list[TimelineEvent]:
    if artifact.kind is ArtifactKind.NETWORK_PCAP:
        return _network_events(artifact)
    if artifact.kind is ArtifactKind.FS_BASELINE:
        return [
            TimelineEvent(
                occurred_at=artifact.captured_at,
                source="capture.fs.baseline",
                summary=f"filesystem baseline ({artifact.metadata.get('entry_count', '?')} files)",
                detail=dict(artifact.metadata),
            ),
        ]
    if artifact.kind is ArtifactKind.FS_DIFF:
        return _fs_diff_events(artifact, cap=cap)
    if artifact.kind is ArtifactKind.MEMORY_DUMP:
        return [
            TimelineEvent(
                occurred_at=artifact.captured_at,
                source="capture.memory",
                summary=f"memory dump ({artifact.size_bytes:,} bytes)",
                detail=dict(artifact.metadata),
            ),
        ]
    if artifact.kind is ArtifactKind.SCREENSHOT:
        return [
            TimelineEvent(
                occurred_at=artifact.captured_at,
                source="capture.screenshot",
                summary=f"screenshot {artifact.path.name}",
                detail=dict(artifact.metadata),
            ),
        ]
    # Generic fallback for kinds we don't yet specialise on.
    return [
        TimelineEvent(
            occurred_at=artifact.captured_at,
            source=f"capture.{artifact.kind.value}",
            summary=artifact.kind.value,
            detail=dict(artifact.metadata),
        ),
    ]


def _network_events(artifact: Artifact) -> list[TimelineEvent]:
    started_iso = artifact.metadata.get("started_at")
    started_at: datetime | None = None
    if started_iso:
        try:
            started_at = datetime.fromisoformat(str(started_iso))
        except ValueError:
            started_at = None
    bridge = artifact.metadata.get("bridge", "")
    bpf = artifact.metadata.get("bpf", "")

    events: list[TimelineEvent] = []
    if started_at is not None:
        events.append(
            TimelineEvent(
                occurred_at=started_at,
                source="capture.network.start",
                summary=f"network capture started on {bridge}",
                detail={
                    "bridge": bridge,
                    "bpf": bpf,
                    "capture_id": artifact.metadata.get("capture_id", ""),
                },
            ),
        )
    events.append(
        TimelineEvent(
            occurred_at=artifact.captured_at,
            source="capture.network.stop",
            summary=f"network capture stopped ({artifact.size_bytes:,} bytes)",
            detail={
                "bridge": bridge,
                "bpf": bpf,
                "size_bytes": artifact.size_bytes,
                "sha256": artifact.sha256,
            },
        ),
    )
    return events


def _fs_diff_events(artifact: Artifact, *, cap: int) -> list[TimelineEvent]:
    added = int(artifact.metadata.get("added_count", 0))
    modified = int(artifact.metadata.get("modified_count", 0))
    deleted = int(artifact.metadata.get("deleted_count", 0))

    events: list[TimelineEvent] = [
        TimelineEvent(
            occurred_at=artifact.captured_at,
            source="capture.fs.diff",
            summary=(f"filesystem diff: +{added} ~{modified} -{deleted}"),
            detail={
                "vm_state": artifact.metadata.get("vm_state", ""),
                "disk_view": artifact.metadata.get("disk_view", ""),
                "added_count": added,
                "modified_count": modified,
                "deleted_count": deleted,
            },
        ),
    ]
    # Per-file events: cap to avoid flooding the timeline. We read
    # the diff JSON inline (small file).
    if added + modified + deleted > cap:
        # Don't enumerate; the summary above suffices.
        return events
    try:
        raw = json.loads(artifact.path.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError, ValueError):
        return events
    if not isinstance(raw, dict):
        return events
    for entry in raw.get("added", []):
        if not isinstance(entry, dict):
            continue
        events.append(
            TimelineEvent(
                occurred_at=_event_time(entry, default=artifact.captured_at),
                source="capture.fs.diff.added",
                summary=f"added: {entry.get('path', '?')}",
                detail={"path": str(entry.get("path", "")), "sha256": str(entry.get("sha256", ""))},
            ),
        )
    for entry in raw.get("modified", []):
        if not isinstance(entry, dict):
            continue
        events.append(
            TimelineEvent(
                occurred_at=_event_time(entry, default=artifact.captured_at),
                source="capture.fs.diff.modified",
                summary=f"modified: {entry.get('path', '?')}",
                detail={"path": str(entry.get("path", "")), "sha256": str(entry.get("sha256", ""))},
            ),
        )
    for path in raw.get("deleted", []):
        events.append(
            TimelineEvent(
                occurred_at=artifact.captured_at,
                source="capture.fs.diff.deleted",
                summary=f"deleted: {path}",
                detail={"path": str(path)},
            ),
        )
    return events


def _event_time(entry: dict[str, Any], *, default: datetime) -> datetime:
    """Try to extract an mtime as a UTC datetime; fall back to ``default``."""
    mtime_raw = entry.get("mtime")
    if mtime_raw is None:
        return default
    try:
        return datetime.fromtimestamp(float(mtime_raw), tz=UTC)
    except (TypeError, ValueError, OSError):
        return default
