"""Demo manifest contract for truthful replay video generation.

This module defines the manifest schema for replay bundles centered on VM jobs,
with explicit present/missing evidence semantics and optional .gsd context linkage.

Key principles:
- VM job is the primary replay unit, not .gsd artifacts
- .gsd context is optional linkage, not the primary timeline
- Missing evidence is explicit (not implied as continuous motion)
- Source evidence vs overlay vs derived distinctions are enforced
"""

from __future__ import annotations

import json
import re
from datetime import UTC, datetime
from enum import StrEnum
from typing import TYPE_CHECKING

from pydantic import BaseModel, Field

from mcp_vm_blackbox._helpers import discover_repo_root_from_cwd

if TYPE_CHECKING:
    from pathlib import Path


class EvidenceKind(StrEnum):
    """Classification of evidence types in a replay bundle."""

    SOURCE = "source"
    OVERLAY = "overlay"
    DERIVED = "derived"


class MissingReason(StrEnum):
    """Reasons why evidence is not present in the bundle."""

    NOT_CAPTURED = "not_captured"
    PATH_ABSENT = "path_absent"
    OPTIONAL = "optional"
    EXPLICIT_OMISSION = "explicit_omission"


class EvidenceItem(BaseModel):
    """A single evidence item in the replay bundle."""

    kind: EvidenceKind = Field(description="Classification: source, overlay, or derived")
    source_kind: str = Field(description="Specific source type: screenshot, recording, dashboard, job_record")
    path: str | None = Field(default=None, description="Relative path to evidence (None if missing)")
    present: bool = Field(description="Whether the evidence actually exists at the claimed path")
    missing_reason: MissingReason | None = Field(
        default=None, description="Reason why evidence is missing (only if present=False)"
    )
    derived_from: list[str] = Field(
        default_factory=list, description="List of source paths this derived asset was generated from"
    )
    timestamp: str | None = Field(default=None, description="ISO 8601 timestamp of capture (for source evidence)")
    metadata: dict[str, str] = Field(
        default_factory=dict, description="Additional metadata: resolution, format, VM name, etc."
    )


class ContinuousRecording(BaseModel):
    """Metadata for a continuous-motion recording attached to a replay bundle."""

    path: str = Field(description="Repo-relative path to the WebM recording file")
    capture_fps: int = Field(default=5, description="FPS at which the source WebM was captured")


class GsdContextLink(BaseModel):
    """Optional linkage to .gsd artifacts for context/referencing."""

    artifact_type: str = Field(description="Type of .gsd artifact")
    artifact_id: str = Field(description="Artifact identifier (e.g., T01, S01, M001)")
    path: str = Field(description="Relative path to the artifact from repo root")
    narrative_fields: dict[str, str] = Field(
        default_factory=dict, description="Selected narrative fields only - never copy full content blobs"
    )
    linked_to_job: bool = Field(
        default=False, description="Whether this artifact is explicitly linked to the primary job"
    )


class ReplayBundle(BaseModel):
    """A manifest describing a replay bundle for demo video generation."""

    bundle_id: str = Field(description="Unique identifier for this bundle")
    created_at: str = Field(description="ISO 8601 timestamp of manifest creation")
    description: str = Field(description="Human-readable description of what this bundle represents")
    primary_job_id: str = Field(description="Job ID of the primary VM job driving the replay")
    primary_vm_name: str = Field(description="VM name associated with the primary job")
    primary_job_path: str = Field(description="Relative path to the job JSON file")
    evidence: list[EvidenceItem] = Field(default_factory=list, description="All evidence items")
    gsd_context: list[GsdContextLink] = Field(
        default_factory=list, description="Optional .gsd artifacts providing context/referencing"
    )
    has_continuous_motion: bool = Field(
        default=False, description="Whether the bundle has continuous motion from recordings"
    )
    continuous_recording: ContinuousRecording | None = Field(
        default=None, description="Metadata for the continuous recording file when has_continuous_motion is True"
    )
    missing_evidence_summary: dict[str, int] = Field(
        default_factory=dict, description="Summary count of missing evidence by reason"
    )
    source_evidence_count: int = Field(default=0, description="Count of source evidence items present")
    overlay_evidence_count: int = Field(default=0, description="Count of overlay items present")
    render_staging_path: str | None = Field(
        default=None, description="Relative path to job-keyed render staging directory, populated when video is enabled"
    )
    video_output_path: str | None = Field(default=None, description="Absolute path to the rendered MP4 video artifact")


def _classify_screenshot_path(path: Path, vm_name: str) -> dict[str, str]:
    """Extract metadata from screenshot filename."""
    del vm_name
    filename = path.stem
    metadata: dict[str, str] = {}
    match = re.search(r"-(\d{8}-\d{6})-seed(\d+)", filename)
    if match:
        timestamp_str = match.group(1)
        seed = match.group(2)
        try:
            dt = datetime.strptime(timestamp_str, "%Y%m%d-%H%M%S").replace(tzinfo=UTC)
            metadata["timestamp"] = dt.isoformat()
        except ValueError:
            pass
        metadata["seed"] = seed
        metadata["filename"] = filename
    return metadata


def _to_repo_relative(path: Path, repo_root: Path) -> str:
    """Return path relative to repo root when possible, else original string."""
    try:
        return str(path.relative_to(repo_root)) if path.is_absolute() else str(path)
    except ValueError:
        return str(path)


def build_evidence_for_job(
    job_path: Path,
    screenshots_dir: Path,
    recordings_dir: Path | None = None,
    repo_root: Path | None = None,
    terminal_dir: Path | None = None,
    tui_captures_dir: Path | None = None,
) -> list[EvidenceItem]:
    """Build evidence list for a job from repo-shaped evidence."""
    if repo_root is None:
        repo_root = discover_repo_root_from_cwd() or job_path.parent

    evidence: list[EvidenceItem] = []
    job_data = json.loads(job_path.read_text(encoding="utf-8"))
    vm_name = job_data.get("vm_name", "unknown")

    evidence.append(
        EvidenceItem(
            kind=EvidenceKind.SOURCE,
            source_kind="job_record",
            path=_to_repo_relative(job_path, repo_root),
            present=True,
            missing_reason=None,
            timestamp=job_data.get("created_at"),
            metadata={
                "vm_name": vm_name,
                "scenario": job_data.get("scenario_name", "unknown"),
                "status": job_data.get("status", "unknown"),
            },
        )
    )

    if screenshots_dir.exists():
        screenshots = list(screenshots_dir.glob(f"{vm_name}-*.png"))
        for screenshot in sorted(screenshots):
            metadata = _classify_screenshot_path(screenshot, vm_name)
            evidence.append(
                EvidenceItem(
                    kind=EvidenceKind.SOURCE,
                    source_kind="screenshot",
                    path=_to_repo_relative(screenshot, repo_root),
                    present=True,
                    missing_reason=None,
                    timestamp=metadata.get("timestamp"),
                    metadata=metadata,
                )
            )
        if not screenshots:
            evidence.append(
                EvidenceItem(
                    kind=EvidenceKind.SOURCE,
                    source_kind="screenshot",
                    path=None,
                    present=False,
                    missing_reason=MissingReason.NOT_CAPTURED,
                    metadata={"vm_name": vm_name, "note": "no matching screenshots found"},
                )
            )
    else:
        evidence.append(
            EvidenceItem(
                kind=EvidenceKind.SOURCE,
                source_kind="screenshot",
                path=None,
                present=False,
                missing_reason=MissingReason.PATH_ABSENT,
                metadata={"vm_name": vm_name, "note": "screenshots directory does not exist"},
            )
        )

    if recordings_dir and recordings_dir.exists():
        recordings = list(recordings_dir.glob(f"{vm_name}-*.webm"))
        if recordings:
            evidence.extend(
                EvidenceItem(
                    kind=EvidenceKind.SOURCE,
                    source_kind="recording",
                    path=_to_repo_relative(recording, repo_root),
                    present=True,
                    missing_reason=None,
                    metadata={"vm_name": vm_name},
                )
                for recording in sorted(recordings)
            )
        else:
            evidence.append(
                EvidenceItem(
                    kind=EvidenceKind.SOURCE,
                    source_kind="recording",
                    path=None,
                    present=False,
                    missing_reason=MissingReason.NOT_CAPTURED,
                    metadata={"vm_name": vm_name},
                )
            )
    else:
        evidence.append(
            EvidenceItem(
                kind=EvidenceKind.SOURCE,
                source_kind="recording",
                path=None,
                present=False,
                missing_reason=MissingReason.OPTIONAL,
                metadata={"vm_name": vm_name, "note": "recordings not available - video will be frame-by-frame"},
            )
        )

    if terminal_dir is not None and terminal_dir.exists():
        terminal_captures = list(terminal_dir.glob(f"{vm_name}-*.txt"))
        evidence.extend(
            EvidenceItem(
                kind=EvidenceKind.SOURCE,
                source_kind="terminal",
                path=_to_repo_relative(capture, repo_root),
                present=True,
                missing_reason=None,
                metadata={"vm_name": vm_name},
            )
            for capture in sorted(terminal_captures)
        )

    if tui_captures_dir is not None and tui_captures_dir.exists():
        tui_captures = list(tui_captures_dir.glob(f"{vm_name}-*.txt"))
        evidence.extend(
            EvidenceItem(
                kind=EvidenceKind.SOURCE,
                source_kind="tui_capture",
                path=_to_repo_relative(capture, repo_root),
                present=True,
                missing_reason=None,
                metadata={"vm_name": vm_name},
            )
            for capture in sorted(tui_captures)
        )

    return evidence


def build_manifest(
    job_id: str,
    vm_name: str,
    job_path: Path,
    screenshots_dir: Path,
    recordings_dir: Path | None = None,
    gsd_context: list[GsdContextLink] | None = None,
    description: str | None = None,
    repo_root: Path | None = None,
    terminal_dir: Path | None = None,
    tui_captures_dir: Path | None = None,
) -> ReplayBundle:
    """Build a complete manifest for a replay bundle."""
    evidence = build_evidence_for_job(
        job_path, screenshots_dir, recordings_dir, repo_root, terminal_dir, tui_captures_dir
    )

    source_count = sum(1 for item in evidence if item.kind == EvidenceKind.SOURCE and item.present)
    overlay_count = sum(1 for item in evidence if item.kind == EvidenceKind.OVERLAY and item.present)

    missing_summary: dict[str, int] = {}
    for item in evidence:
        if not item.present and item.missing_reason:
            reason = item.missing_reason.value
            missing_summary[reason] = missing_summary.get(reason, 0) + 1

    has_recordings = any(item.source_kind == "recording" and item.present for item in evidence)

    # Build continuous_recording metadata from the first present recording evidence item
    continuous_rec: ContinuousRecording | None = None
    if has_recordings:
        recording_item = next((item for item in evidence if item.source_kind == "recording" and item.present), None)
        if recording_item and recording_item.path:
            continuous_rec = ContinuousRecording(path=recording_item.path)

    if description is None:
        status = "with" if source_count > 0 else "without"
        motion = "continuous motion" if has_recordings else "frame-by-frame"
        description = f"Replay bundle for {vm_name} job {job_id}: {status} source evidence, {motion}"

    effective_repo_root = repo_root or discover_repo_root_from_cwd() or job_path.parent

    return ReplayBundle(
        bundle_id=job_id,
        created_at=datetime.now(UTC).isoformat(),
        description=description,
        primary_job_id=job_id,
        primary_vm_name=vm_name,
        primary_job_path=_to_repo_relative(job_path, effective_repo_root),
        evidence=evidence,
        gsd_context=gsd_context or [],
        has_continuous_motion=has_recordings,
        continuous_recording=continuous_rec,
        missing_evidence_summary=missing_summary,
        source_evidence_count=source_count,
        overlay_evidence_count=overlay_count,
    )


def load_manifest(path: Path) -> ReplayBundle:
    """Load a manifest from disk."""
    data = json.loads(path.read_text(encoding="utf-8"))
    return ReplayBundle.model_validate(data)


def save_manifest(manifest: ReplayBundle, path: Path) -> None:
    """Save a manifest to disk."""
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(manifest.model_dump_json(indent=2), encoding="utf-8")
