from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
from typing import Any, Literal

SessionStatus = Literal["ready", "interrupted", "failed"]
SessionModality = Literal["point-cloud", "trajectory", "video"]


class SessionStoreError(RuntimeError):
    """Base error for the host-owned observation session store."""


class SessionNotFoundError(SessionStoreError):
    """The requested opaque session identifier is not registered."""


class SessionNotReplayableError(SessionStoreError):
    """The session has no reviewed replay source."""


class SessionIntegrityError(SessionStoreError):
    """A stored artifact no longer satisfies its confinement/integrity boundary."""


class LayoutConflictError(SessionStoreError):
    """A workspace layout revision changed since the caller loaded it."""


@dataclass(frozen=True, slots=True)
class LabSessionBinding:
    """Immutable provenance for one derived laboratory replay."""

    session_id: str
    source_session_id: str
    lab_id: str
    result_kind: str
    result_id: str
    source_result_id: str | None
    config_sha256: str | None
    run_created_at_utc: str
    published_at_utc: str
    provenance: dict[str, Any]

    def as_dict(self) -> dict[str, Any]:
        return {
            "lab_id": self.lab_id,
            "source_session_id": self.source_session_id,
            "result_kind": self.result_kind,
            "result_id": self.result_id,
            "source_result_id": self.source_result_id,
            "config_sha256": self.config_sha256,
            "run_created_at_utc": self.run_created_at_utc,
            "published_at_utc": self.published_at_utc,
            "provenance": self.provenance,
        }


@dataclass(frozen=True, slots=True)
class SessionSource:
    source_id: str
    semantic_channel_id: str
    modality: SessionModality
    status: str
    seekable: bool
    artifact_id: str

    def as_dict(self) -> dict[str, Any]:
        return {
            "source_id": self.source_id,
            "semantic_channel_id": self.semantic_channel_id,
            "modality": self.modality,
            "status": self.status,
            "seekable": self.seekable,
            "artifact_id": self.artifact_id,
        }


@dataclass(frozen=True, slots=True)
class SessionArtifact:
    artifact_id: str
    kind: str
    media_type: str
    byte_length: int
    sha256: str | None
    integrity_status: str

    def as_dict(self) -> dict[str, Any]:
        return {
            "artifact_id": self.artifact_id,
            "kind": self.kind,
            "media_type": self.media_type,
            "byte_length": self.byte_length,
            "sha256": self.sha256,
            "integrity_status": self.integrity_status,
        }


@dataclass(frozen=True, slots=True)
class SessionSummary:
    session_id: str
    display_name: str
    status: SessionStatus
    started_at_utc: str | None
    completed_at_utc: str | None
    duration_seconds: float | None
    modalities: tuple[SessionModality, ...]
    source_count: int
    total_bytes: int
    replayable: bool
    origin: str
    lab: LabSessionBinding | None = None

    def as_dict(self) -> dict[str, Any]:
        document: dict[str, Any] = {
            "schema_version": "missioncore.observation-session-summary/v1",
            "session_id": self.session_id,
            "display_name": self.display_name,
            "status": self.status,
            "started_at_utc": self.started_at_utc,
            "completed_at_utc": self.completed_at_utc,
            "duration_seconds": self.duration_seconds,
            "modalities": list(self.modalities),
            "source_count": self.source_count,
            "total_bytes": self.total_bytes,
            "replayable": self.replayable,
            "origin": self.origin,
        }
        if self.lab is not None:
            document["lab"] = self.lab.as_dict()
        return document


@dataclass(frozen=True, slots=True)
class SessionDetail:
    summary: SessionSummary
    sources: tuple[SessionSource, ...]
    artifacts: tuple[SessionArtifact, ...]

    def as_dict(self) -> dict[str, Any]:
        duration = self.summary.duration_seconds
        return {
            "schema_version": "missioncore.observation-session/v1",
            **{
                key: value
                for key, value in self.summary.as_dict().items()
                if key != "schema_version"
            },
            "timeline": {
                "mode": "recorded" if self.summary.replayable else "unavailable",
                "seekable": self.summary.replayable,
                "start_seconds": 0.0 if self.summary.replayable else None,
                "end_seconds": duration if self.summary.replayable else None,
                "synchronization": "host-arrival-best-effort",
            },
            "sources": [source.as_dict() for source in self.sources],
            "artifacts": [artifact.as_dict() for artifact in self.artifacts],
        }


@dataclass(frozen=True, slots=True)
class SessionPage:
    items: tuple[SessionSummary, ...]
    next_cursor: str | None

    def as_dict(self) -> dict[str, Any]:
        return {
            "schema_version": "missioncore.observation-session-list/v1",
            "items": [item.as_dict() for item in self.items],
            "next_cursor": self.next_cursor,
        }


@dataclass(frozen=True, slots=True)
class WorkspaceLayout:
    workspace_id: str
    schema_version: int
    revision: int
    name: str
    layout: dict[str, Any]
    updated_at_utc: str

    def as_dict(self) -> dict[str, Any]:
        return {
            "schema_version": "missioncore.workspace-layout/v1",
            "workspace_id": self.workspace_id,
            "layout_schema_version": self.schema_version,
            "revision": self.revision,
            "name": self.name,
            "layout": self.layout,
            "updated_at_utc": self.updated_at_utc,
        }


@dataclass(frozen=True, slots=True)
class ReplayArtifact:
    """One confined input artifact selected for plugin-owned preparation."""

    artifact_id: str
    path: Path
    media_type: str
    file_byte_length: int
    replay_byte_length: int
    expected_sha256: str | None


@dataclass(frozen=True, slots=True)
class ReplayCommand:
    """Internal replay request containing no vendor format or channel names."""

    session_id: str
    plugin_id: str
    allowed_root: Path
    session_root: Path
    primary_artifact_id: str
    artifacts: tuple[ReplayArtifact, ...]
    timeline_origin_epoch_ns: int
    timeline_origin_monotonic_ns: int
    speed: float
    loop: bool

    @property
    def primary_artifact(self) -> ReplayArtifact:
        matches = tuple(
            artifact
            for artifact in self.artifacts
            if artifact.artifact_id == self.primary_artifact_id
        )
        if len(matches) != 1:
            raise SessionIntegrityError("replay command has no unique primary artifact")
        return matches[0]


@dataclass(frozen=True, slots=True)
class RecordedMediaArtifact:
    """Internal handle for one confined archived camera source.

    The physical source id and filesystem locator never enter the public API.
    ``public_source_id`` and ``artifact_id`` are opaque catalog identifiers.
    """

    session_id: str
    public_source_id: str
    artifact_id: str
    source_path: Path
    byte_length: int


@dataclass(frozen=True, slots=True)
class ObservationArtifactCandidate:
    artifact_id: str
    kind: str
    media_type: str
    locator: Path
    byte_length: int
    replay_byte_length: int
    sha256: str | None
    integrity_status: str


@dataclass(frozen=True, slots=True)
class ObservationSessionCandidate:
    session_id: str
    display_name: str
    status: SessionStatus
    started_at_utc: str | None
    completed_at_utc: str | None
    duration_seconds: float | None
    modalities: tuple[SessionModality, ...]
    replayable: bool
    total_bytes: int
    allowed_root: Path
    session_root: Path
    primary_replay_artifact_id: str | None
    timeline_origin_epoch_ns: int | None
    timeline_origin_monotonic_ns: int | None
    sources: tuple[SessionSource, ...]
    artifacts: tuple[ObservationArtifactCandidate, ...]


@dataclass(frozen=True, slots=True)
class LegacyMediaSourceCandidate:
    source_id: str
    artifact_id: str
    locator: Path
    byte_length: int
    epoch_count: int
