"""Case + Artifact data model for Phase 17 forensics.

A *case* is one investigation: a quarantined VM, an optional sample,
and a directory of captured artifacts (pcaps, FS diffs, memory
dumps, screenshots) that the analyst can later replay analyzers
against.

The data model is deliberately flat — one frozen dataclass per kind
— so it round-trips through YAML cleanly and so a future GUI table
can render every field without reflection.

See [`docs/FORENSICS.md`](../../docs/FORENSICS.md) §4 for the full design.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from datetime import UTC, datetime
from enum import StrEnum
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
    from collections.abc import Mapping
    from pathlib import Path
    from uuid import UUID

    from mgf.vm.core.security.isolation import IsolationReport


__all__ = [
    "CASE_SCHEMA_VERSION",
    "Artifact",
    "ArtifactKind",
    "Case",
    "CaseStatus",
]


CASE_SCHEMA_VERSION = 1
"""Bump on any breaking shape change to :class:`Case`.

Loaders refuse unknown schema versions with an actionable hint
pointing at the regenerate / re-import path.
"""


class CaseStatus(StrEnum):
    """Where a case sits in its lifecycle.

    Transitions are validated at the store layer:

    - PREPARING → ACTIVE (a capture started)
    - ACTIVE → COMPLETED (every capture stopped)
    - COMPLETED → ARCHIVED (`case archive` ran, working dir compacted)

    Skipping a state or going backwards raises :class:`OperationError`.
    """

    PREPARING = "preparing"
    ACTIVE = "active"
    COMPLETED = "completed"
    ARCHIVED = "archived"


class ArtifactKind(StrEnum):
    """The discriminator for :class:`Artifact`.

    String values land in the manifest + case YAML, so adding a new
    kind here is a coordinated change — also update the analysis
    layer's discriminator dispatch.
    """

    NETWORK_PCAP = "network.pcap"
    NETWORK_PCAP_NG = "network.pcapng"
    FS_BASELINE = "filesystem.baseline"
    FS_DIFF = "filesystem.diff"
    MEMORY_DUMP = "memory.dump"
    SCREENSHOT = "screenshot.png"
    GUEST_AGENT_LOG = "guest_agent.log"
    EVENT_LOG = "event.timeline"


@dataclass(frozen=True)
class Artifact:
    """One captured piece of evidence inside a case.

    Hashed at write time so :func:`forensics.manifest.verify_manifest`
    can detect accidental corruption. (Not a defence against an
    attacker with write access to the case dir — see the threat-model
    note at the top of ``forensics/manifest.py``.)
    ``captured_by`` carries the tool name + version so a months-later
    review can reproduce the capture.
    """

    kind: ArtifactKind
    path: Path
    sha256: str
    size_bytes: int
    captured_at: datetime
    captured_by: str = ""
    metadata: Mapping[str, Any] = field(default_factory=dict)


@dataclass(frozen=True)
class Case:
    """One forensic investigation, persisted to ``case.yaml``."""

    name: str
    case_id: UUID
    vm_name: str
    sample_path: Path | None = None
    sample_sha256: str = ""
    description: str = ""
    status: CaseStatus = CaseStatus.PREPARING
    created_at: datetime = field(
        default_factory=lambda: datetime.now(UTC),
    )
    completed_at: datetime | None = None
    vmanager_version: str = ""
    qemu_version: str = ""
    host_kernel: str = ""
    isolation_report: IsolationReport | None = None
    artifacts: tuple[Artifact, ...] = ()
    schema_version: int = field(
        default=CASE_SCHEMA_VERSION,
        kw_only=True,
    )

    def with_artifact(self, artifact: Artifact) -> Case:
        """Return a copy with ``artifact`` appended to :attr:`artifacts`."""
        return self.__class__(
            name=self.name,
            case_id=self.case_id,
            vm_name=self.vm_name,
            sample_path=self.sample_path,
            sample_sha256=self.sample_sha256,
            description=self.description,
            status=self.status,
            created_at=self.created_at,
            completed_at=self.completed_at,
            vmanager_version=self.vmanager_version,
            qemu_version=self.qemu_version,
            host_kernel=self.host_kernel,
            isolation_report=self.isolation_report,
            artifacts=(*self.artifacts, artifact),
            schema_version=self.schema_version,
        )

    def with_status(
        self,
        status: CaseStatus,
        *,
        completed_at: datetime | None = None,
    ) -> Case:
        """Return a copy with :attr:`status` (and optionally
        :attr:`completed_at`) updated.
        """
        return self.__class__(
            name=self.name,
            case_id=self.case_id,
            vm_name=self.vm_name,
            sample_path=self.sample_path,
            sample_sha256=self.sample_sha256,
            description=self.description,
            status=status,
            created_at=self.created_at,
            completed_at=completed_at if completed_at is not None else self.completed_at,
            vmanager_version=self.vmanager_version,
            qemu_version=self.qemu_version,
            host_kernel=self.host_kernel,
            isolation_report=self.isolation_report,
            artifacts=self.artifacts,
            schema_version=self.schema_version,
        )

    def total_size_bytes(self) -> int:
        """Sum :attr:`Artifact.size_bytes` across every artifact."""
        return sum(a.size_bytes for a in self.artifacts)
