from __future__ import annotations

import inspect
import re
from collections.abc import Awaitable, Callable, Iterator, Mapping
from itertools import chain
from pathlib import Path
from threading import Lock
from typing import Annotated, Any, Literal, Protocol
from urllib.parse import quote

from fastapi import APIRouter, Body, Header, HTTPException, Query, Request, Response
from fastapi.responses import FileResponse, JSONResponse, StreamingResponse
from pydantic import BaseModel, ConfigDict, Field, StrictBool, field_validator, model_validator
from starlette.concurrency import run_in_threadpool
from starlette.types import Receive, Scope, Send

from k1link.compute import RecordedPerceptionOverlayError, RecordedPerceptionVideo
from k1link.sessions import (
    LayoutConflictError,
    MaterializedRecording,
    RecordedMediaFile,
    RecordedMediaInspector,
    RecordedMediaManifest,
    RecordingMaterializationError,
    RecordingPreparationQueueFull,
    RecordingPreparationSnapshot,
    ReplayCommand,
    SessionIntegrityError,
    SessionNotFoundError,
    SessionNotReplayableError,
    SessionRecordingPreparationManager,
    SessionStore,
    validate_recorded_media_timeline,
)
from k1link.sessions.plugin_contract import RecordedPointColorRenderer
from k1link.viewer.recorded import (
    APPLICATION_ID as RECORDED_APPLICATION_ID,
)
from k1link.viewer.recorded import (
    RecordedBlueprintError,
    recorded_blueprint_rrd,
)
from k1link.viewer.rerun_bridge import RerunSceneSettings

DEFAULT_REPLAY_ACTION_ID = "stream.start-replay"
RECORDED_MEDIA_STREAM_MANIFEST_SCHEMA = "missioncore.observation-recorded-media/v3"
SAFE_SOURCE_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$")
SAFE_SHA256 = re.compile(r"^[a-f0-9]{64}$")
MAX_SAFE_INTEGER = 9_007_199_254_740_991
REVALIDATED_RECORDING_CACHE_CONTROL = "private, no-cache, no-transform"
IMMUTABLE_RECORDING_CACHE_CONTROL = "private, max-age=31536000, immutable, no-transform"


class _ReleasingFileResponse(FileResponse):
    """Release a cache pin exactly once after every ASGI completion path."""

    def __init__(
        self,
        *args: Any,
        release: Callable[[], None],
        **kwargs: Any,
    ) -> None:
        super().__init__(*args, **kwargs)
        self._release = release
        self._release_guard = Lock()
        self._released = False

    def _release_once(self) -> None:
        with self._release_guard:
            if self._released:
                return
            self._released = True
        self._release()

    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        try:
            await super().__call__(scope, receive, send)
        finally:
            # Starlette background tasks do not run when `send` is cancelled
            # or raises.  The finally block makes the response lifetime the
            # authoritative pin lifetime for success, disconnect and failure.
            self._release_once()


class StrictApiModel(BaseModel):
    model_config = ConfigDict(extra="forbid")


class ReplayRequest(StrictApiModel):
    speed: float = Field(default=1.0, ge=0.0, le=100.0)
    loop: bool = False


class RecordedBlueprintRequest(StrictApiModel):
    application_id: Literal["nodedc_mission_core_recorded"]
    recording_id: str = Field(
        min_length=1,
        max_length=128,
        pattern=r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$",
    )
    blueprint_session_id: str = Field(
        min_length=32,
        max_length=32,
        pattern=r"^[a-f0-9]{32}$",
    )
    accumulation_seconds: float = Field(strict=True, ge=0.0, le=3600.0)
    show_points: StrictBool
    show_trajectory: StrictBool
    show_grid: StrictBool
    point_size: float = Field(default=2.5, strict=True, ge=0.1, le=32.0)
    color_mode: Literal["intensity", "height", "distance", "rgb", "class"] = "intensity"
    palette: Literal["turbo", "viridis", "plasma", "grayscale", "custom"] = "turbo"
    custom_color: str = Field(default="#f7f8f4", pattern=r"^#[0-9A-Fa-f]{6}$")
    active_view: Literal["spatial", "perception", "perception3d", "metrics"] = "spatial"
    view_reset_generation: Literal[0, 1] = 0
    unified_perception: StrictBool = False
    show_detections_2d: StrictBool = False
    show_segmentation: StrictBool = False
    show_cuboids_3d: StrictBool = False
    follow_trajectory: StrictBool = False


class RecordedPerceptionRequest(StrictApiModel):
    application_id: Literal["nodedc_mission_core_recorded"]
    recording_id: str = Field(
        min_length=1,
        max_length=128,
        pattern=r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$",
    )


class RecordedPointColorsRequest(StrictApiModel):
    application_id: Literal["nodedc_mission_core_recorded"]
    recording_id: str = Field(
        min_length=1,
        max_length=128,
        pattern=r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$",
    )
    color_mode: Literal["intensity", "height", "distance", "rgb", "class"]
    palette: Literal["turbo", "viridis", "plasma", "grayscale", "custom"]
    custom_color: str = Field(pattern=r"^#[0-9A-Fa-f]{6}$")


class SceneSettingsDocument(StrictApiModel):
    projection: Literal["3d", "2d", "map"]
    point_size: float = Field(ge=0.1, le=32.0)
    color_mode: Literal["intensity", "height", "distance", "rgb", "class"]
    palette: Literal["turbo", "viridis", "plasma", "grayscale", "custom"]
    custom_color: str = Field(pattern=r"^#[0-9A-Fa-f]{6}$")
    accumulation_seconds: float = Field(ge=0.0, le=3600.0)
    show_points: bool
    show_trajectory: bool
    show_grid: bool
    show_labels: bool
    show_camera_frustums: bool


class LegacyToolWindowsDocument(StrictApiModel):
    sources_open: bool
    display_open: bool
    layers_open: bool
    order: list[Literal["sources", "display", "layers"]]

    @field_validator("order")
    @classmethod
    def validate_order(cls, value: list[str]) -> list[str]:
        if len(value) != 3 or len(value) != len(set(value)):
            raise ValueError("tool window order must be a permutation of all three tools")
        return value


class NormalizedWindowRect(StrictApiModel):
    x: float = Field(ge=0.0, le=1.0)
    y: float = Field(ge=0.0, le=1.0)
    width: float = Field(gt=0.0, le=1.0)
    height: float = Field(gt=0.0, le=1.0)

    @model_validator(mode="after")
    def validate_bounds(self) -> NormalizedWindowRect:
        epsilon = 1e-9
        if self.x + self.width > 1.0 + epsilon or self.y + self.height > 1.0 + epsilon:
            raise ValueError("normalized window rectangle escapes the viewport")
        return self


class ViewportSize(StrictApiModel):
    width: float = Field(gt=0.0, le=100_000.0)
    height: float = Field(gt=0.0, le=100_000.0)


class LegacyLayoutV1Document(StrictApiModel):
    version: Literal[1]
    revision: int = Field(ge=0, le=MAX_SAFE_INTEGER)
    workspace_id: Literal["observation.spatial"]
    scene_settings: SceneSettingsDocument
    tool_windows: LegacyToolWindowsDocument
    visible_source_ids: list[str]
    active_floating_source_id: str | None
    window_rects: dict[str, NormalizedWindowRect]
    viewport_size: ViewportSize


class LayoutPutRequest(StrictApiModel):
    version: Literal[2]
    revision: int = Field(ge=0, le=MAX_SAFE_INTEGER)
    workspace_id: Literal["observation.spatial"]
    scene_settings: SceneSettingsDocument
    visible_source_ids: list[str]
    active_floating_source_id: str | None
    window_rects: dict[str, NormalizedWindowRect]
    viewport_size: ViewportSize

    @field_validator("visible_source_ids")
    @classmethod
    def validate_visible_source_ids(cls, value: list[str]) -> list[str]:
        if len(value) > 256 or len(value) != len(set(value)):
            raise ValueError("visible source identifiers must be unique and bounded")
        for source_id in value:
            _validate_source_id(source_id)
        return value

    @field_validator("active_floating_source_id")
    @classmethod
    def validate_active_source_id(cls, value: str | None) -> str | None:
        if value is not None:
            _validate_source_id(value)
        return value

    @field_validator("window_rects")
    @classmethod
    def validate_window_rect_ids(
        cls,
        value: dict[str, NormalizedWindowRect],
    ) -> dict[str, NormalizedWindowRect]:
        if len(value) > 256:
            raise ValueError("window rectangle catalog exceeds its boundary")
        for source_id in value:
            _validate_source_id(source_id)
        return value

    @model_validator(mode="after")
    def validate_active_source_visibility(self) -> LayoutPutRequest:
        if (
            self.active_floating_source_id is not None
            and self.active_floating_source_id not in self.visible_source_ids
        ):
            raise ValueError("active floating source must be visible")
        return self


class ReplayLauncher(Protocol):
    def __call__(
        self,
        command: ReplayCommand,
    ) -> Mapping[str, Any] | Awaitable[Mapping[str, Any]]: ...


class RecordingMaterializer(Protocol):
    def __call__(self, command: ReplayCommand) -> MaterializedRecording: ...


class CatalogRefresher(Protocol):
    """Synchronous app-owned recovery + discovery transaction hook.

    The host composes hardware-specific recovery before generic session
    discovery, then passes the resulting callable here. Return values stay
    internal and are intentionally ignored by the API.
    """

    def __call__(self) -> object: ...


class RecordedPerceptionOverlayProvider(Protocol):
    def render(
        self,
        session_id: str,
        *,
        application_id: str,
        recording_id: str,
    ) -> bytes | None: ...


class RecordedPerceptionMediaProvider(Protocol):
    def video(
        self,
        session_id: str,
        result_id: str | None = None,
    ) -> RecordedPerceptionVideo | None: ...


def build_session_router(
    store: SessionStore,
    *,
    catalog_refresher: CatalogRefresher | None = None,
    replay_launcher: ReplayLauncher | None = None,
    recording_materializer: RecordingMaterializer | None = None,
    recording_preparation_manager: SessionRecordingPreparationManager | None = None,
    media_inspector: RecordedMediaInspector | None = None,
    perception_overlay_provider: RecordedPerceptionOverlayProvider | None = None,
    perception_media_provider: RecordedPerceptionMediaProvider | None = None,
    point_color_renderers: Mapping[str, RecordedPointColorRenderer] | None = None,
    allow_synchronous_recording_fallback: bool = False,
    replay_action_id: str = DEFAULT_REPLAY_ACTION_ID,
) -> APIRouter:
    """Build generic host APIs without exposing filesystem locators to clients."""

    router = APIRouter(tags=["observation-sessions"])
    recorded_media_inspector = media_inspector or RecordedMediaInspector()

    @router.get("/api/v1/observation-sessions")
    def list_observation_sessions(
        limit: int = Query(default=20, ge=1, le=100),
        cursor: str | None = Query(default=None, max_length=128),
    ) -> dict[str, Any]:
        try:
            _refresh_catalog(catalog_refresher)
            page = store.list_recent(limit=limit, cursor=cursor)
            return {
                "items": [
                    {
                        "id": item.session_id,
                        "label": item.display_name,
                        "started_at_utc": item.started_at_utc,
                        "completed_at_utc": item.completed_at_utc,
                        "status": item.status,
                        "modalities": list(item.modalities),
                        "duration_seconds": item.duration_seconds or 0.0,
                        "replayable": item.replayable,
                        **({"lab": item.lab.as_dict()} if item.lab is not None else {}),
                        **(
                            {
                                "preparation": _catalog_preparation_document(
                                    store,
                                    recording_preparation_manager,
                                    item.session_id,
                                    item.replayable,
                                )
                            }
                            if recording_preparation_manager is not None
                            else {}
                        ),
                    }
                    for item in page.items
                    if item.started_at_utc is not None
                ]
            }
        except SessionNotFoundError as exc:
            raise HTTPException(status_code=404, detail=str(exc)) from exc
        except ValueError as exc:
            raise HTTPException(status_code=422, detail="Некорректные параметры запроса.") from exc

    @router.get("/api/v1/observation-sessions/{session_id}")
    def get_observation_session(session_id: str) -> dict[str, Any]:
        try:
            _refresh_catalog(catalog_refresher)
            return store.get_session(session_id).as_dict()
        except SessionNotFoundError as exc:
            raise HTTPException(status_code=404, detail=str(exc)) from exc
        except ValueError as exc:
            raise HTTPException(
                status_code=422,
                detail="Некорректный идентификатор сессии.",
            ) from exc

    @router.delete(
        "/api/v1/observation-sessions/{session_id}",
        status_code=204,
    )
    async def delete_observation_session(session_id: str) -> Response:
        try:
            store.get_session(session_id)
            recorded_artifacts = store.list_recorded_media(session_id)
        except SessionNotFoundError as exc:
            raise HTTPException(status_code=404, detail=str(exc)) from exc
        except ValueError as exc:
            raise HTTPException(
                status_code=422,
                detail="Некорректный идентификатор сессии.",
            ) from exc

        if recording_preparation_manager is not None:
            snapshot = recording_preparation_manager.status(session_id)
            if snapshot is not None and snapshot.state in {
                "queued",
                "validating",
                "exporting",
                "finalizing",
            }:
                raise HTTPException(
                    status_code=409,
                    detail="Дождитесь завершения подготовки записи перед удалением.",
                )
            await run_in_threadpool(
                recording_preparation_manager.release_launch_reservation,
                session_id,
            )

        cache_deleter = getattr(recording_materializer, "delete_cached", None)
        if callable(cache_deleter):
            try:
                cache_deleted = await run_in_threadpool(cache_deleter, session_id)
            except (OSError, RecordingMaterializationError) as exc:
                raise HTTPException(
                    status_code=409,
                    detail="Подготовленную запись пока нельзя удалить.",
                ) from exc
            if cache_deleted is not True:
                raise HTTPException(
                    status_code=409,
                    detail="Запись сейчас открыта в интерфейсе. Закройте её и повторите удаление.",
                )

        if recording_preparation_manager is not None and not recording_preparation_manager.discard(
            session_id
        ):
            raise HTTPException(
                status_code=409,
                detail="Подготовка записи ещё выполняется.",
            )

        try:
            await run_in_threadpool(
                recorded_media_inspector.delete_prepared,
                session_id,
                (artifact.artifact_id for artifact in recorded_artifacts),
            )
            await run_in_threadpool(store.delete_session, session_id)
        except SessionNotFoundError as exc:
            raise HTTPException(status_code=404, detail=str(exc)) from exc
        except (OSError, SessionIntegrityError) as exc:
            raise HTTPException(
                status_code=409,
                detail="Сервер не смог безопасно удалить файлы этой сессии.",
            ) from exc
        return Response(status_code=204)

    @router.post("/api/v1/observation-sessions/{session_id}/replay")
    async def replay_observation_session(
        session_id: str,
        request: Annotated[ReplayRequest | None, Body()] = None,
    ) -> Any:
        replay_request = request or ReplayRequest()
        try:
            command = await run_in_threadpool(
                _prepare_replay,
                store,
                catalog_refresher,
                session_id,
                replay_request.speed,
                replay_request.loop,
                True,
            )
        except SessionNotFoundError as exc:
            raise HTTPException(status_code=404, detail=str(exc)) from exc
        except SessionNotReplayableError as exc:
            raise HTTPException(status_code=409, detail=str(exc)) from exc
        except SessionIntegrityError as exc:
            raise HTTPException(status_code=409, detail=str(exc)) from exc
        except ValueError as exc:
            raise HTTPException(status_code=422, detail="Некорректные параметры повтора.") from exc

        if recording_preparation_manager is not None:
            try:
                snapshot = recording_preparation_manager.status(command.session_id)
                if snapshot is None:
                    snapshot = await run_in_threadpool(
                        recording_preparation_manager.restore_published,
                        command,
                    )
                if snapshot is None:
                    snapshot = recording_preparation_manager.enqueue(
                        command,
                        retry_failed=True,
                    )
            except RecordingPreparationQueueFull as exc:
                raise HTTPException(
                    status_code=503,
                    detail="Очередь подготовки записей переполнена.",
                ) from exc
            if snapshot.state != "ready" or snapshot.recording is None:
                return _preparation_response(snapshot)
            reserved = recording_preparation_manager.reserve_ready(
                command.session_id,
                preparation_id=snapshot.preparation_id,
            )
            if reserved is None or reserved.recording is None:
                try:
                    replacement = recording_preparation_manager.enqueue(
                        command,
                        retry_failed=True,
                    )
                except RecordingPreparationQueueFull as exc:
                    raise HTTPException(
                        status_code=503,
                        detail="Очередь подготовки записей переполнена.",
                    ) from exc
                return _preparation_response(replacement)
            return _recording_launch_document(
                reserved.recording,
                command,
                reserved.recorded_media or (),
                perception_media_provider=perception_media_provider,
            )

        if recording_materializer is not None and allow_synchronous_recording_fallback:
            recording = await _materialize(recording_materializer, command)
            try:
                recorded_media = await run_in_threadpool(
                    _inspect_recorded_media,
                    store,
                    recorded_media_inspector,
                    command,
                    recording,
                )
            except SessionIntegrityError as exc:
                raise HTTPException(status_code=409, detail=str(exc)) from exc
            return _recording_launch_document(
                recording,
                command,
                recorded_media,
                perception_media_provider=perception_media_provider,
            )
        if recording_materializer is not None:
            raise HTTPException(
                status_code=503,
                detail="Сервис фоновой подготовки записей не настроен.",
            )

        runtime_state: dict[str, Any] | None = None
        dispatched = replay_launcher is not None
        if replay_launcher is not None:
            launched = replay_launcher(command)
            result = await launched if inspect.isawaitable(launched) else launched
            runtime_state = dict(result)
        return {
            "schema_version": "missioncore.observation-session-replay/v1",
            "launch": {
                "kind": "plugin-action",
                "plugin_id": command.plugin_id,
                "action_id": replay_action_id,
                "session_id": command.session_id,
                "speed": command.speed,
                "loop": command.loop,
                "dispatched": dispatched,
            },
            "runtime_state": runtime_state,
        }

    @router.get("/api/v1/observation-sessions/{session_id}/recording-preparation")
    async def get_recording_preparation(
        session_id: str,
        if_match: Annotated[str | None, Header(alias="If-Match")] = None,
    ) -> Any:
        if recording_preparation_manager is None:
            raise HTTPException(
                status_code=409,
                detail="Сервис фоновой подготовки записей не настроен.",
            )
        snapshot = recording_preparation_manager.status(session_id)
        if snapshot is None:
            if if_match is None:
                raise HTTPException(
                    status_code=428,
                    detail="Укажите ETag подготовки в заголовке If-Match.",
                )
            # A polling URL is generation-bound. It must never silently create
            # a replacement and bind an old browser poller to the new job.
            raise HTTPException(
                status_code=412,
                detail="Подготовка была заменена более новой операцией.",
            )
        _require_matching_preparation(snapshot, if_match)
        if snapshot.state == "ready" and snapshot.recording is not None:
            reserved = recording_preparation_manager.reserve_ready(
                session_id,
                preparation_id=snapshot.preparation_id,
            )
            if reserved is None or reserved.recording is None:
                raise HTTPException(
                    status_code=412,
                    detail="Подготовка была заменена более новой операцией.",
                )
            snapshot = reserved
            return JSONResponse(
                content=_recording_launch_document(
                    reserved.recording,
                    snapshot.command,
                    snapshot.recorded_media or (),
                    perception_media_provider=perception_media_provider,
                ),
                headers={
                    "Cache-Control": "no-store",
                    "ETag": _preparation_etag(snapshot.preparation_id),
                    "X-Content-Type-Options": "nosniff",
                },
            )
        return _preparation_response(snapshot)

    @router.delete(
        "/api/v1/observation-sessions/{session_id}/recording-preparation",
        status_code=204,
    )
    def delete_recording_preparation(
        session_id: str,
        if_match: Annotated[str | None, Header(alias="If-Match")] = None,
    ) -> Response:
        if recording_preparation_manager is None:
            raise HTTPException(
                status_code=409,
                detail="Сервис фоновой подготовки записей не настроен.",
            )
        snapshot = recording_preparation_manager.status(session_id)
        if snapshot is None:
            raise HTTPException(status_code=404, detail="Подготовка записи не найдена.")
        _require_matching_preparation(snapshot, if_match)
        recording_preparation_manager.cancel(
            session_id,
            preparation_id=snapshot.preparation_id,
        )
        return Response(status_code=204)

    @router.get("/api/v1/observation-sessions/{session_id}/recording.rrd")
    async def get_observation_session_recording(
        session_id: str,
        generation: Annotated[str | None, Query(max_length=64)] = None,
        if_match: Annotated[str | None, Header(alias="If-Match")] = None,
        if_none_match: Annotated[str | None, Header()] = None,
    ) -> Response:
        if generation is not None and SAFE_SHA256.fullmatch(generation) is None:
            raise HTTPException(
                status_code=412,
                detail="Поколение подготовленной записи не совпадает.",
            )
        if recording_materializer is None:
            raise HTTPException(
                status_code=409,
                detail="Сервис seekable-записей не настроен.",
            )
        release_recording: Callable[[], None] | None
        if recording_preparation_manager is not None:
            if if_match is None and generation is None:
                raise HTTPException(
                    status_code=428,
                    detail="Укажите digest записи через generation или If-Match.",
                )
            snapshot = recording_preparation_manager.status(session_id)
            if snapshot is None:
                try:
                    command = await run_in_threadpool(
                        _prepare_replay,
                        store,
                        catalog_refresher,
                        session_id,
                        1.0,
                        False,
                        False,
                    )
                    snapshot = await run_in_threadpool(
                        recording_preparation_manager.restore_published,
                        command,
                    )
                    if snapshot is None:
                        snapshot = recording_preparation_manager.enqueue(command)
                except SessionNotFoundError as exc:
                    raise HTTPException(status_code=404, detail=str(exc)) from exc
                except (SessionNotReplayableError, SessionIntegrityError) as exc:
                    raise HTTPException(status_code=409, detail=str(exc)) from exc
                except RecordingPreparationQueueFull as exc:
                    raise HTTPException(
                        status_code=503,
                        detail="Очередь подготовки записей переполнена.",
                    ) from exc
                except ValueError as exc:
                    raise HTTPException(
                        status_code=422,
                        detail="Некорректный идентификатор сессии.",
                    ) from exc
            if snapshot.state in {
                "queued",
                "validating",
                "exporting",
                "finalizing",
            }:
                return _preparation_response(snapshot)
            if snapshot.state != "ready" or snapshot.recording is None:
                return _preparation_response(snapshot)
            recording = snapshot.recording
            etag = f'"sha256:{recording.sha256}"'
            if generation is not None:
                _require_matching_recording_generation(recording.sha256, generation)
            if if_match is not None:
                _require_matching_digest(etag, if_match)
            pinned = recording_preparation_manager.pin_ready(
                session_id,
                preparation_id=snapshot.preparation_id,
            )
            if pinned is None:
                raise HTTPException(
                    status_code=412,
                    detail="Подготовленная запись была заменена.",
                )
            snapshot, pinned_release = pinned
            release_recording = pinned_release
            pinned_recording = snapshot.recording
            if pinned_recording is None:
                release_recording()
                raise HTTPException(
                    status_code=500,
                    detail="Подготовленная запись сессии недоступна.",
                )
            recording = pinned_recording
        elif allow_synchronous_recording_fallback:
            try:
                command = await run_in_threadpool(
                    _prepare_replay,
                    store,
                    catalog_refresher,
                    session_id,
                    1.0,
                    False,
                    False,
                )
            except SessionNotFoundError as exc:
                raise HTTPException(status_code=404, detail=str(exc)) from exc
            except (SessionNotReplayableError, SessionIntegrityError) as exc:
                raise HTTPException(status_code=409, detail=str(exc)) from exc
            except ValueError as exc:
                raise HTTPException(
                    status_code=422,
                    detail="Некорректный идентификатор сессии.",
                ) from exc
            recording, release_recording = await _materialize_pinned(
                recording_materializer,
                command,
            )
            try:
                if generation is not None:
                    _require_matching_recording_generation(recording.sha256, generation)
                if if_match is not None:
                    _require_matching_digest(f'"sha256:{recording.sha256}"', if_match)
            except HTTPException:
                if release_recording is not None:
                    release_recording()
                raise
        else:
            raise HTTPException(
                status_code=503,
                detail="Сервис фоновой подготовки записей не настроен.",
            )
        etag = f'"sha256:{recording.sha256}"'
        cache_control = (
            IMMUTABLE_RECORDING_CACHE_CONTROL
            if generation is not None
            else REVALIDATED_RECORDING_CACHE_CONTROL
        )
        if _if_none_match_matches(if_none_match, etag):
            if release_recording is not None:
                release_recording()
            return Response(
                status_code=304,
                headers={
                    "Cache-Control": cache_control,
                    "ETag": etag,
                    "X-Content-Type-Options": "nosniff",
                },
            )
        try:
            stat_result = recording.path.stat()
        except OSError as exc:
            if release_recording is not None:
                release_recording()
            raise HTTPException(
                status_code=500,
                detail="Подготовленная запись сессии недоступна.",
            ) from exc
        response_type = _ReleasingFileResponse if release_recording is not None else FileResponse
        response_kwargs: dict[str, Any] = {}
        if release_recording is not None:
            response_kwargs["release"] = release_recording
        return response_type(
            recording.path,
            media_type=recording.media_type,
            filename=f"{recording.session_id}.rrd",
            content_disposition_type="inline",
            stat_result=stat_result,
            headers={
                "Cache-Control": cache_control,
                "ETag": etag,
                "Content-Length": str(recording.byte_length),
                "X-Content-Type-Options": "nosniff",
            },
            **response_kwargs,
        )

    @router.post("/api/v1/observation-sessions/{session_id}/blueprint.rrd")
    async def get_observation_session_blueprint(
        session_id: str,
        request: RecordedBlueprintRequest,
    ) -> Response:
        try:
            await run_in_threadpool(
                _prepare_replay,
                store,
                catalog_refresher,
                session_id,
                1.0,
                False,
                False,
            )
            payload = await run_in_threadpool(
                recorded_blueprint_rrd,
                RerunSceneSettings(
                    point_size=request.point_size,
                    color_mode=request.color_mode,
                    palette=request.palette,
                    custom_color=request.custom_color,
                    accumulation_seconds=request.accumulation_seconds,
                    show_points=request.show_points,
                    show_trajectory=request.show_trajectory,
                    show_grid=request.show_grid,
                ),
                application_id=RECORDED_APPLICATION_ID,
                recording_id=request.recording_id,
                blueprint_session_id=request.blueprint_session_id,
                active_view=request.active_view,
                view_reset_generation=request.view_reset_generation,
                unified_perception=request.unified_perception,
                show_detections_2d=request.show_detections_2d,
                show_segmentation=request.show_segmentation,
                show_cuboids_3d=request.show_cuboids_3d,
                follow_trajectory=request.follow_trajectory,
            )
        except SessionNotFoundError as exc:
            raise HTTPException(status_code=404, detail=str(exc)) from exc
        except (SessionNotReplayableError, SessionIntegrityError) as exc:
            raise HTTPException(status_code=409, detail=str(exc)) from exc
        except RecordedBlueprintError as exc:
            raise HTTPException(
                status_code=500,
                detail="Не удалось подготовить настройки визуализатора.",
            ) from exc
        except ValueError as exc:
            raise HTTPException(
                status_code=422,
                detail="Некорректный идентификатор сессии.",
            ) from exc
        return Response(
            content=payload,
            media_type="application/vnd.rerun.rrd",
            headers={
                "Cache-Control": "no-store",
                "X-Content-Type-Options": "nosniff",
                "Content-Disposition": 'inline; filename="blueprint.rrd"',
            },
        )

    @router.post("/api/v1/observation-sessions/{session_id}/perception.rrd")
    async def get_observation_session_perception(
        session_id: str,
        request: RecordedPerceptionRequest,
    ) -> Response:
        if perception_overlay_provider is None:
            return Response(status_code=204, headers={"Cache-Control": "no-store"})
        try:
            await run_in_threadpool(
                _prepare_replay,
                store,
                catalog_refresher,
                session_id,
                1.0,
                False,
                False,
            )
            payload = await run_in_threadpool(
                perception_overlay_provider.render,
                session_id,
                application_id=request.application_id,
                recording_id=request.recording_id,
            )
        except SessionNotFoundError as exc:
            raise HTTPException(status_code=404, detail=str(exc)) from exc
        except (SessionNotReplayableError, SessionIntegrityError) as exc:
            raise HTTPException(status_code=409, detail=str(exc)) from exc
        except RecordedPerceptionOverlayError as exc:
            raise HTTPException(
                status_code=500,
                detail="Не удалось подготовить слой распознавания.",
            ) from exc
        except ValueError as exc:
            raise HTTPException(
                status_code=422,
                detail="Некорректный идентификатор сессии.",
            ) from exc
        if payload is None:
            return Response(status_code=204, headers={"Cache-Control": "no-store"})
        return Response(
            content=payload,
            media_type="application/vnd.rerun.rrd",
            headers={
                "Cache-Control": "private, no-cache, no-transform",
                "Content-Length": str(len(payload)),
                "X-Content-Type-Options": "nosniff",
                "Content-Disposition": 'inline; filename="perception.rrd"',
            },
        )

    @router.post("/api/v1/observation-sessions/{session_id}/point-colors.rrd")
    async def get_observation_session_point_colors(
        session_id: str,
        request: RecordedPointColorsRequest,
    ) -> Response:
        try:
            command = await run_in_threadpool(
                _prepare_replay,
                store,
                catalog_refresher,
                session_id,
                1.0,
                False,
                False,
            )
            point_color_renderer = (point_color_renderers or {}).get(command.plugin_id)
            if point_color_renderer is None:
                raise HTTPException(
                    status_code=409,
                    detail="Сервис архивной окраски точек не настроен.",
                )
            payload = await run_in_threadpool(
                point_color_renderer,
                command,
                application_id=request.application_id,
                recording_id=request.recording_id,
                color_mode=request.color_mode,
                palette=request.palette,
                custom_color=request.custom_color,
            )
        except SessionNotFoundError as exc:
            raise HTTPException(status_code=404, detail=str(exc)) from exc
        except (SessionNotReplayableError, SessionIntegrityError) as exc:
            raise HTTPException(status_code=409, detail=str(exc)) from exc
        except ValueError as exc:
            raise HTTPException(
                status_code=422,
                detail="Некорректные параметры окраски точек.",
            ) from exc
        except RuntimeError as exc:
            raise HTTPException(
                status_code=500,
                detail="Не удалось подготовить окраску облака точек.",
            ) from exc
        return Response(
            content=payload,
            media_type="application/vnd.rerun.rrd",
            headers={
                "Cache-Control": "private, no-cache, no-transform",
                "Content-Length": str(len(payload)),
                "X-Content-Type-Options": "nosniff",
                "Content-Disposition": 'inline; filename="point-colors.rrd"',
            },
        )

    @router.get("/api/v1/observation-sessions/{session_id}/perception-media/{result_id}/manifest")
    def get_recorded_perception_media_manifest(
        session_id: str,
        result_id: str,
        if_match: Annotated[str | None, Header(alias="If-Match")] = None,
    ) -> JSONResponse:
        video = _resolve_recorded_perception_video(
            store,
            catalog_refresher,
            perception_media_provider,
            session_id,
            result_id,
        )
        etag = f'"sha256:{video.sha256}"'
        _require_matching_digest(etag, if_match)
        return JSONResponse(
            content=_recorded_perception_manifest_document(video),
            headers={
                "Cache-Control": "private, no-cache",
                "ETag": etag,
                "X-Content-Type-Options": "nosniff",
            },
        )

    @router.api_route(
        "/api/v1/observation-sessions/{session_id}/perception-media/{result_id}/recording.mp4",
        methods=["GET", "HEAD"],
    )
    def stream_recorded_perception_media(
        request: Request,
        session_id: str,
        result_id: str,
        generation: Annotated[str, Query(min_length=64, max_length=64)],
        range_header: Annotated[str | None, Header(alias="Range")] = None,
    ) -> Response:
        video = _resolve_recorded_perception_video(
            store,
            catalog_refresher,
            perception_media_provider,
            session_id,
            result_id,
        )
        if SAFE_SHA256.fullmatch(generation) is None or generation != video.sha256:
            raise HTTPException(
                status_code=412,
                detail="Поколение видео сегментации было заменено.",
            )
        return _recorded_perception_video_response(
            video,
            range_header,
            head_only=request.method == "HEAD",
        )

    @router.get("/api/v1/observation-sessions/{session_id}/media/{artifact_id}/manifest")
    def get_recorded_media_manifest(
        session_id: str,
        artifact_id: str,
        if_match: Annotated[str | None, Header(alias="If-Match")] = None,
    ) -> JSONResponse:
        manifest = _resolve_recorded_media_manifest(
            store,
            recorded_media_inspector,
            recording_preparation_manager,
            allow_synchronous_recording_fallback,
            catalog_refresher,
            session_id,
            artifact_id,
        )
        manifest_etag = f'"sha256:{manifest.generation_sha256}"'
        _require_matching_digest(manifest_etag, if_match)
        return JSONResponse(
            content=_recorded_media_manifest_document(manifest),
            headers={
                "Cache-Control": "private, no-cache",
                "ETag": manifest_etag,
                "X-Content-Type-Options": "nosniff",
            },
        )

    @router.get(
        "/api/v1/observation-sessions/{session_id}/media/{artifact_id}/"
        "epochs/{epoch_ordinal}/init.mp4"
    )
    def get_recorded_media_init(
        session_id: str,
        artifact_id: str,
        epoch_ordinal: int,
        if_match: Annotated[str | None, Header(alias="If-Match")] = None,
        range_header: Annotated[str | None, Header(alias="Range")] = None,
    ) -> Response:
        manifest = _resolve_recorded_media_manifest(
            store,
            recorded_media_inspector,
            recording_preparation_manager,
            allow_synchronous_recording_fallback,
            catalog_refresher,
            session_id,
            artifact_id,
        )
        expected_sha256 = _recorded_media_init_sha256(manifest, epoch_ordinal)
        _require_matching_digest(f'"sha256:{expected_sha256}"', if_match)
        try:
            media_file = recorded_media_inspector.get_init(manifest, epoch_ordinal)
        except SessionIntegrityError as exc:
            raise HTTPException(status_code=409, detail=str(exc)) from exc
        return _recorded_media_file_response(media_file, range_header)

    @router.get(
        "/api/v1/observation-sessions/{session_id}/media/{artifact_id}/"
        "epochs/{epoch_ordinal}/segments/{segment_sequence}.m4s"
    )
    def get_recorded_media_segment(
        session_id: str,
        artifact_id: str,
        epoch_ordinal: int,
        segment_sequence: int,
        if_match: Annotated[str | None, Header(alias="If-Match")] = None,
        range_header: Annotated[str | None, Header(alias="Range")] = None,
    ) -> Response:
        manifest = _resolve_recorded_media_manifest(
            store,
            recorded_media_inspector,
            recording_preparation_manager,
            allow_synchronous_recording_fallback,
            catalog_refresher,
            session_id,
            artifact_id,
        )
        expected_sha256 = _recorded_media_segment_sha256(
            manifest,
            epoch_ordinal,
            segment_sequence,
        )
        _require_matching_digest(f'"sha256:{expected_sha256}"', if_match)
        try:
            media_file = recorded_media_inspector.get_segment(
                manifest,
                epoch_ordinal,
                segment_sequence,
            )
        except SessionIntegrityError as exc:
            raise HTTPException(status_code=409, detail=str(exc)) from exc
        return _recorded_media_file_response(media_file, range_header)

    @router.api_route(
        "/api/v1/observation-sessions/{session_id}/media/{artifact_id}/"
        "epochs/{epoch_ordinal}/recording.mp4",
        methods=["GET", "HEAD"],
    )
    def stream_recorded_media_epoch(
        request: Request,
        session_id: str,
        artifact_id: str,
        epoch_ordinal: int,
        generation: Annotated[str, Query(min_length=64, max_length=64)],
        range_header: Annotated[str | None, Header(alias="Range")] = None,
    ) -> Response:
        manifest = _resolve_recorded_media_manifest(
            store,
            recorded_media_inspector,
            recording_preparation_manager,
            allow_synchronous_recording_fallback,
            catalog_refresher,
            session_id,
            artifact_id,
        )
        if SAFE_SHA256.fullmatch(generation) is None or generation != manifest.generation_sha256:
            raise HTTPException(
                status_code=412,
                detail="Поколение записанного видео было заменено.",
            )
        return _recorded_media_epoch_response(
            recorded_media_inspector,
            manifest,
            epoch_ordinal,
            range_header,
            head_only=request.method == "HEAD",
        )

    @router.get("/api/v1/workspace-layouts/{workspace_id}")
    def get_workspace_layout(workspace_id: str, response: Response) -> dict[str, Any]:
        try:
            stored = store.get_layout(workspace_id)
            document = _layout_document(
                stored.workspace_id,
                stored.schema_version,
                stored.revision,
                stored.layout,
            )
            response.headers["ETag"] = f'"{stored.revision}"'
            return document
        except SessionNotFoundError as exc:
            raise HTTPException(status_code=404, detail=str(exc)) from exc
        except ValueError as exc:
            raise HTTPException(status_code=422, detail="Некорректный workspace id.") from exc

    @router.put("/api/v1/workspace-layouts/{workspace_id}")
    def put_workspace_layout(
        workspace_id: str,
        request: LayoutPutRequest,
        response: Response,
        if_match: Annotated[str, Header(alias="If-Match")],
    ) -> dict[str, Any]:
        header_revision = _parse_if_match(if_match)
        if workspace_id != request.workspace_id:
            raise HTTPException(status_code=422, detail="Workspace id не совпадает с документом.")
        if header_revision != request.revision:
            raise HTTPException(status_code=412, detail="Ревизия документа устарела.")
        payload = request.model_dump(
            mode="json",
            exclude={"version", "revision", "workspace_id"},
        )
        try:
            stored = store.save_layout(
                workspace_id,
                schema_version=request.version,
                expected_revision=request.revision,
                name="Пространственная сцена",
                layout=payload,
            )
            response.headers["ETag"] = f'"{stored.revision}"'
            return _layout_document(
                stored.workspace_id,
                stored.schema_version,
                stored.revision,
                stored.layout,
            )
        except LayoutConflictError as exc:
            raise HTTPException(status_code=412, detail=str(exc)) from exc
        except ValueError as exc:
            raise HTTPException(status_code=422, detail="Некорректная компоновка.") from exc

    return router


def _refresh_catalog(refresher: CatalogRefresher | None) -> None:
    if refresher is None:
        return
    try:
        result = refresher()
        if inspect.isawaitable(result):
            if inspect.iscoroutine(result):
                result.close()
            raise TypeError("catalog refresher must be synchronous")
    except Exception as exc:
        raise HTTPException(
            status_code=503,
            detail="Не удалось обновить каталог сохранённых сессий.",
        ) from exc


def _prepare_replay(
    store: SessionStore,
    catalog_refresher: CatalogRefresher | None,
    session_id: str,
    speed: float,
    loop: bool,
    refresh_first: bool,
) -> ReplayCommand:
    if refresh_first:
        _refresh_catalog(catalog_refresher)
    try:
        return store.prepare_replay(session_id, speed=speed, loop=loop)
    except SessionNotFoundError:
        if refresh_first or catalog_refresher is None:
            raise
        _refresh_catalog(catalog_refresher)
        return store.prepare_replay(session_id, speed=speed, loop=loop)


def _validate_source_id(source_id: str) -> None:
    if SAFE_SOURCE_ID.fullmatch(source_id) is None:
        raise ValueError("source identifier has an invalid shape")


def _parse_if_match(value: str) -> int:
    normalized = value.strip()
    if len(normalized) < 3 or normalized[0] != '"' or normalized[-1] != '"':
        raise HTTPException(status_code=422, detail="If-Match должен содержать quoted revision.")
    revision = normalized[1:-1]
    if not revision.isascii() or not revision.isdecimal():
        raise HTTPException(status_code=422, detail="If-Match содержит некорректную ревизию.")
    return int(revision)


def _layout_document(
    workspace_id: str,
    schema_version: int,
    revision: int,
    layout: dict[str, Any],
) -> dict[str, Any]:
    if schema_version == 1:
        try:
            legacy = LegacyLayoutV1Document.model_validate(
                {
                    "version": 1,
                    "revision": revision,
                    "workspace_id": workspace_id,
                    **layout,
                }
            )
        except ValueError as exc:
            raise SessionIntegrityError(
                "stored workspace layout violates schema version 1"
            ) from exc
        layout = legacy.model_dump(
            mode="json",
            exclude={"version", "revision", "workspace_id", "tool_windows"},
        )
        schema_version = 2
    if schema_version != 2:
        raise SessionIntegrityError("stored workspace layout schema is unsupported")
    try:
        document = LayoutPutRequest.model_validate(
            {
                "version": 2,
                "revision": revision,
                "workspace_id": workspace_id,
                **layout,
            }
        )
    except ValueError as exc:
        raise SessionIntegrityError("stored workspace layout violates schema version 2") from exc
    return document.model_dump(mode="json")


ReplayLauncherCallable = Callable[
    [ReplayCommand],
    Mapping[str, Any] | Awaitable[Mapping[str, Any]],
]


async def _materialize(
    materializer: RecordingMaterializer,
    command: ReplayCommand,
) -> MaterializedRecording:
    try:
        recording = await run_in_threadpool(materializer, command)
    except RecordingMaterializationError as exc:
        raise HTTPException(
            status_code=500,
            detail="Не удалось подготовить запись сессии.",
        ) from exc
    if not isinstance(recording, MaterializedRecording):
        raise HTTPException(
            status_code=500,
            detail="Сервис записи вернул некорректный результат.",
        )
    return recording


async def _materialize_pinned(
    materializer: RecordingMaterializer,
    command: ReplayCommand,
) -> tuple[MaterializedRecording, Callable[[], None] | None]:
    pin = getattr(materializer, "materialize_pinned", None)
    if not callable(pin):
        return await _materialize(materializer, command), None
    try:
        result = await run_in_threadpool(pin, command)
    except RecordingMaterializationError as exc:
        raise HTTPException(
            status_code=500,
            detail="Не удалось подготовить запись сессии.",
        ) from exc
    if (
        not isinstance(result, tuple)
        or len(result) != 2
        or not isinstance(result[0], MaterializedRecording)
        or not callable(result[1])
    ):
        raise HTTPException(
            status_code=500,
            detail="Сервис записи вернул некорректный pinned-результат.",
        )
    return result[0], result[1]


def _preparation_response(snapshot: RecordingPreparationSnapshot) -> JSONResponse:
    encoded_session_id = quote(snapshot.session_id, safe="")
    status_url = f"/api/v1/observation-sessions/{encoded_session_id}/recording-preparation"
    preparation: dict[str, Any] = {
        "preparation_id": snapshot.preparation_id,
        "session_id": snapshot.session_id,
        "state": snapshot.state,
        "progress": snapshot.progress,
        "updated_at_utc": snapshot.updated_at_utc,
        "status_url": status_url,
        "cancellable": snapshot.cancellable,
    }
    terminal_failure = snapshot.state in {"failed", "cancelled"}
    if terminal_failure:
        preparation["retryable"] = snapshot.retryable
        preparation["error"] = snapshot.error or "Подготовка записи отменена."
    headers = {
        "Location": status_url,
        "Cache-Control": "no-store",
        "ETag": _preparation_etag(snapshot.preparation_id),
        "X-Content-Type-Options": "nosniff",
    }
    if not terminal_failure:
        headers["Retry-After"] = "1"
    return JSONResponse(
        status_code=409 if terminal_failure else 202,
        content={
            "schema_version": "missioncore.observation-session-preparation/v1",
            "preparation": preparation,
        },
        headers=headers,
    )


def _preparation_etag(preparation_id: str) -> str:
    return f'"{preparation_id}"'


def _require_matching_preparation(
    snapshot: RecordingPreparationSnapshot,
    if_match: str | None,
) -> None:
    if if_match is None:
        raise HTTPException(
            status_code=428,
            detail="Укажите ETag подготовки в заголовке If-Match.",
        )
    if if_match.strip() != _preparation_etag(snapshot.preparation_id):
        raise HTTPException(
            status_code=412,
            detail="Подготовка была заменена более новой операцией.",
        )


def _require_matching_digest(expected_etag: str, if_match: str | None) -> None:
    if if_match is None:
        raise HTTPException(
            status_code=428,
            detail="Укажите digest поколения в заголовке If-Match.",
        )
    if if_match.strip() != expected_etag:
        raise HTTPException(
            status_code=412,
            detail="Запрошенное поколение данных устарело.",
        )


def _require_matching_recording_generation(
    expected_sha256: str,
    generation: str,
) -> None:
    if SAFE_SHA256.fullmatch(generation) is None or generation != expected_sha256:
        raise HTTPException(
            status_code=412,
            detail="Поколение подготовленной записи не совпадает.",
        )


def _catalog_preparation_document(
    store: SessionStore,
    manager: SessionRecordingPreparationManager | None,
    session_id: str,
    replayable: bool,
) -> dict[str, Any] | None:
    if manager is None or not replayable:
        return None
    snapshot = manager.status(session_id)
    if snapshot is None:
        try:
            snapshot = manager.restore_published(store.prepare_replay(session_id))
        except (
            SessionNotFoundError,
            SessionNotReplayableError,
            SessionIntegrityError,
            ValueError,
        ):
            return None
    if snapshot is None:
        return None
    document: dict[str, Any] = {
        "preparation_id": snapshot.preparation_id,
        "state": snapshot.state,
        "progress": snapshot.progress,
        "updated_at_utc": snapshot.updated_at_utc,
        "cancellable": snapshot.cancellable,
        "retryable": snapshot.retryable,
    }
    if snapshot.error is not None:
        document["error"] = snapshot.error
    return document


def _recording_launch_document(
    recording: MaterializedRecording,
    command: ReplayCommand,
    recorded_media: tuple[RecordedMediaManifest, ...] = (),
    *,
    perception_media_provider: RecordedPerceptionMediaProvider | None = None,
) -> dict[str, Any]:
    del perception_media_provider
    encoded_session_id = quote(recording.session_id, safe="")
    source_url = f"/api/v1/observation-sessions/{encoded_session_id}/recording.rrd"
    media_sources = [
        _recorded_media_launch_source(manifest, index=index)
        for index, manifest in enumerate(recorded_media, start=1)
    ]
    return {
        "schema_version": "missioncore.observation-session-replay/v2",
        "launch": {
            "kind": "rerun-recording",
            "session_id": recording.session_id,
            "source_url": source_url,
            "viewer_source_url": f"{source_url}?generation={recording.sha256}",
            "media_type": recording.media_type,
            "timeline": recording.timeline,
            "timeline_start_seconds": recording.timeline_start_ns / 1_000_000_000,
            "timeline_end_seconds": recording.timeline_end_ns / 1_000_000_000,
            "seekable": True,
            "byte_length": recording.byte_length,
            "sha256": recording.sha256,
            "playback": {
                "speed": command.speed,
                "loop": command.loop,
            },
            "media_sources": media_sources,
        },
    }


def _inspect_recorded_media(
    store: SessionStore,
    inspector: RecordedMediaInspector,
    command: ReplayCommand,
    recording: MaterializedRecording,
) -> tuple[RecordedMediaManifest, ...]:
    manifests = tuple(
        inspector.inspect(artifact, command)
        for artifact in store.list_recorded_media(command.session_id)
    )
    validate_recorded_media_timeline(
        manifests,
        recording_start_seconds=recording.timeline_start_ns / 1_000_000_000,
        recording_end_seconds=recording.timeline_end_ns / 1_000_000_000,
    )
    return manifests


def _recorded_media_launch_source(
    manifest: RecordedMediaManifest,
    *,
    index: int,
) -> dict[str, Any]:
    encoded_session_id = quote(manifest.session_id, safe="")
    encoded_artifact_id = quote(manifest.artifact_id, safe="")
    return {
        "id": manifest.public_source_id,
        "label": f"Записанная камера {index}",
        "modality": "video",
        "manifest_url": (
            f"/api/v1/observation-sessions/{encoded_session_id}/media/"
            f"{encoded_artifact_id}/manifest"
        ),
        "media_type": "video/mp4",
        "manifest_generation_sha256": manifest.generation_sha256,
        "byte_length": manifest.byte_length,
        "timeline_start_seconds": manifest.timeline_start_seconds,
        "timeline_end_seconds": manifest.timeline_end_seconds,
        "seekable": True,
        "synchronization": manifest.synchronization,
    }


def _recorded_perception_launch_source(video: RecordedPerceptionVideo) -> dict[str, Any]:
    encoded_session_id = quote(video.session_id, safe="")
    encoded_result_id = quote(video.result_id, safe="")
    return {
        "id": video.public_source_id,
        "label": video.label,
        "modality": "video",
        "manifest_url": (
            f"/api/v1/observation-sessions/{encoded_session_id}/perception-media/"
            f"{encoded_result_id}/manifest"
        ),
        "media_type": "video/mp4",
        "manifest_generation_sha256": video.sha256,
        "byte_length": video.byte_length,
        "timeline_start_seconds": video.timeline_start_seconds,
        "timeline_end_seconds": video.timeline_end_seconds,
        "seekable": True,
        "synchronization": "host-arrival-best-effort",
    }


def _resolve_recorded_perception_video(
    store: SessionStore,
    catalog_refresher: CatalogRefresher | None,
    provider: RecordedPerceptionMediaProvider | None,
    session_id: str,
    result_id: str,
) -> RecordedPerceptionVideo:
    if provider is None:
        raise HTTPException(status_code=404, detail="Видео сегментации не найдено.")
    try:
        _prepare_replay(
            store,
            catalog_refresher,
            session_id,
            1.0,
            False,
            False,
        )
        video = provider.video(session_id, result_id)
    except SessionNotFoundError as exc:
        raise HTTPException(status_code=404, detail=str(exc)) from exc
    except (SessionNotReplayableError, SessionIntegrityError) as exc:
        raise HTTPException(status_code=409, detail=str(exc)) from exc
    except ValueError as exc:
        raise HTTPException(
            status_code=422,
            detail="Некорректный идентификатор результата сегментации.",
        ) from exc
    if video is None:
        raise HTTPException(status_code=404, detail="Видео сегментации не найдено.")
    return video


def _recorded_perception_manifest_document(
    video: RecordedPerceptionVideo,
) -> dict[str, Any]:
    encoded_session_id = quote(video.session_id, safe="")
    encoded_result_id = quote(video.result_id, safe="")
    base = f"/api/v1/observation-sessions/{encoded_session_id}/perception-media/{encoded_result_id}"
    return {
        "schema_version": RECORDED_MEDIA_STREAM_MANIFEST_SCHEMA,
        "source_id": video.public_source_id,
        "generation_sha256": video.sha256,
        "byte_length": video.byte_length,
        "timeline_start_seconds": video.timeline_start_seconds,
        "timeline_end_seconds": video.timeline_end_seconds,
        "synchronization": "host-arrival-best-effort",
        "epochs": [
            {
                "ordinal": 1,
                "timeline_start_seconds": video.timeline_start_seconds,
                "timeline_end_seconds": video.timeline_end_seconds,
                "media_type": video.media_type,
                "byte_length": video.byte_length,
                "stream_url": f"{base}/recording.mp4?generation={video.sha256}",
            }
        ],
    }


def _recorded_perception_video_response(
    video: RecordedPerceptionVideo,
    range_header: str | None,
    *,
    head_only: bool,
) -> Response:
    start, end = 0, video.byte_length - 1
    status_code = 200
    if range_header is not None:
        start, end = _parse_byte_range(range_header, video.byte_length)
        status_code = 206
    headers = {
        "Accept-Ranges": "bytes",
        "Cache-Control": "private, max-age=31536000, immutable, no-transform",
        "ETag": f'"sha256:{video.sha256}"',
        "Content-Length": str(end - start + 1),
        "X-Content-Type-Options": "nosniff",
        "Content-Disposition": 'inline; filename="recorded-perception.mp4"',
    }
    if status_code == 206:
        headers["Content-Range"] = f"bytes {start}-{end}/{video.byte_length}"
    if head_only:
        return Response(
            status_code=status_code,
            media_type=video.media_type,
            headers=headers,
        )
    return StreamingResponse(
        _iter_regular_file_range(video.path, start, end),
        status_code=status_code,
        media_type=video.media_type,
        headers=headers,
    )


def _iter_regular_file_range(path: Path, start: int, end: int) -> Iterator[bytes]:
    remaining = end - start + 1
    with path.open("rb") as stream:
        stream.seek(start)
        while remaining:
            chunk = stream.read(min(1024 * 1024, remaining))
            if not chunk:
                raise SessionIntegrityError("recorded perception video ended unexpectedly")
            remaining -= len(chunk)
            yield chunk


def _recorded_media_manifest_document(
    manifest: RecordedMediaManifest,
) -> dict[str, Any]:
    encoded_session_id = quote(manifest.session_id, safe="")
    encoded_artifact_id = quote(manifest.artifact_id, safe="")
    base = f"/api/v1/observation-sessions/{encoded_session_id}/media/{encoded_artifact_id}"
    return {
        "schema_version": RECORDED_MEDIA_STREAM_MANIFEST_SCHEMA,
        "source_id": manifest.public_source_id,
        "generation_sha256": manifest.generation_sha256,
        "byte_length": manifest.byte_length,
        "timeline_start_seconds": manifest.timeline_start_seconds,
        "timeline_end_seconds": manifest.timeline_end_seconds,
        "synchronization": manifest.synchronization,
        "epochs": [
            {
                "ordinal": epoch.ordinal,
                "timeline_start_seconds": epoch.timeline_start_seconds,
                "timeline_end_seconds": epoch.timeline_end_seconds,
                "media_type": epoch.media_type,
                "byte_length": epoch.init_byte_length
                + sum(segment.byte_length for segment in epoch.segments),
                "stream_url": (
                    f"{base}/epochs/{epoch.ordinal}/recording.mp4"
                    f"?generation={manifest.generation_sha256}"
                ),
            }
            for epoch in manifest.epochs
        ],
    }


def _resolve_recorded_media_manifest(
    store: SessionStore,
    inspector: RecordedMediaInspector,
    manager: SessionRecordingPreparationManager | None,
    allow_synchronous_fallback: bool,
    catalog_refresher: CatalogRefresher | None,
    session_id: str,
    artifact_id: str,
) -> RecordedMediaManifest:
    if manager is not None:
        try:
            _validate_source_id(artifact_id)
        except ValueError as exc:
            raise HTTPException(
                status_code=422,
                detail="Некорректный идентификатор записанного медиаканала.",
            ) from exc
        snapshot = manager.status(session_id)
        if snapshot is None or snapshot.state != "ready" or snapshot.recorded_media is None:
            raise HTTPException(
                status_code=409,
                detail="Медиаканал ещё не подготовлен.",
            )
        matches = tuple(
            manifest for manifest in snapshot.recorded_media if manifest.artifact_id == artifact_id
        )
        if len(matches) != 1:
            raise HTTPException(status_code=404, detail="Записанный медиаканал не найден.")
        return matches[0]
    if not allow_synchronous_fallback:
        raise HTTPException(
            status_code=503,
            detail="Сервис фоновой подготовки медиаканалов не настроен.",
        )
    try:
        command = _prepare_replay(
            store,
            catalog_refresher,
            session_id,
            1.0,
            False,
            False,
        )
        artifact = store.get_recorded_media(session_id, artifact_id)
        return inspector.inspect(artifact, command)
    except SessionNotFoundError as exc:
        raise HTTPException(status_code=404, detail=str(exc)) from exc
    except (SessionNotReplayableError, SessionIntegrityError) as exc:
        raise HTTPException(status_code=409, detail=str(exc)) from exc
    except ValueError as exc:
        raise HTTPException(
            status_code=422,
            detail="Некорректный идентификатор записанного медиаканала.",
        ) from exc


def _recorded_media_init_sha256(
    manifest: RecordedMediaManifest,
    epoch_ordinal: int,
) -> str:
    matches = tuple(epoch for epoch in manifest.epochs if epoch.ordinal == epoch_ordinal)
    if len(matches) != 1:
        raise HTTPException(status_code=404, detail="Эпоха записанного медиаканала не найдена.")
    return matches[0].init_sha256


def _recorded_media_segment_sha256(
    manifest: RecordedMediaManifest,
    epoch_ordinal: int,
    segment_sequence: int,
) -> str:
    matches = tuple(epoch for epoch in manifest.epochs if epoch.ordinal == epoch_ordinal)
    if len(matches) != 1:
        raise HTTPException(status_code=404, detail="Эпоха записанного медиаканала не найдена.")
    segments = tuple(
        segment for segment in matches[0].segments if segment.sequence == segment_sequence
    )
    if len(segments) != 1:
        raise HTTPException(status_code=404, detail="Сегмент записанного медиаканала не найден.")
    return segments[0].sha256


def _if_none_match_matches(value: str | None, etag: str) -> bool:
    if value is None:
        return False
    # RFC 9110 requires weak comparison for If-None-Match. The recording ETag
    # is content-addressed, so accepting its weak spelling is safe and lets a
    # browser reuse an already-downloaded multi-hundred-megabyte RRD.
    expected = etag.removeprefix("W/")
    for candidate in value.split(","):
        normalized = candidate.strip()
        if normalized == "*" or normalized.removeprefix("W/") == expected:
            return True
    return False


def _recorded_media_file_response(
    media_file: RecordedMediaFile,
    range_header: str | None,
) -> Response:
    payload = media_file.payload
    status_code = 200
    content_range: str | None = None
    if range_header is not None:
        start, end = _parse_byte_range(range_header, len(payload))
        payload = payload[start : end + 1]
        status_code = 206
        content_range = f"bytes {start}-{end}/{media_file.byte_length}"
    headers = {
        "Accept-Ranges": "bytes",
        "Cache-Control": "private, max-age=31536000, immutable, no-transform",
        "ETag": f'"sha256:{media_file.sha256}"',
        "Content-Length": str(len(payload)),
        "X-Content-Type-Options": "nosniff",
        "Content-Disposition": f'inline; filename="{media_file.filename}"',
    }
    if content_range is not None:
        headers["Content-Range"] = content_range
    return Response(
        content=payload,
        status_code=status_code,
        media_type=media_file.media_type,
        headers=headers,
    )


def _recorded_media_epoch_response(
    inspector: RecordedMediaInspector,
    manifest: RecordedMediaManifest,
    epoch_ordinal: int,
    range_header: str | None,
    *,
    head_only: bool,
) -> Response:
    matches = tuple(epoch for epoch in manifest.epochs if epoch.ordinal == epoch_ordinal)
    if len(matches) != 1:
        raise HTTPException(status_code=404, detail="Эпоха записанного медиаканала не найдена.")
    epoch = matches[0]
    byte_length = epoch.init_byte_length + sum(segment.byte_length for segment in epoch.segments)
    start, end = (0, byte_length - 1)
    status_code = 200
    if range_header is not None:
        start, end = _parse_byte_range(range_header, byte_length)
        status_code = 206

    etag = f'"generation:{manifest.generation_sha256}:epoch:{epoch.ordinal}"'
    headers = {
        "Accept-Ranges": "bytes",
        "Cache-Control": "private, max-age=31536000, immutable, no-transform",
        "ETag": etag,
        "Content-Length": str(end - start + 1),
        "X-Content-Type-Options": "nosniff",
        "Content-Disposition": (f'inline; filename="recorded-camera-{epoch.ordinal}.mp4"'),
    }
    if status_code == 206:
        headers["Content-Range"] = f"bytes {start}-{end}/{byte_length}"
    if head_only:
        return Response(
            status_code=status_code,
            media_type=epoch.media_type,
            headers=headers,
        )
    return StreamingResponse(
        _iter_recorded_media_epoch_range(
            inspector,
            manifest,
            epoch.ordinal,
            start,
            end,
        ),
        status_code=status_code,
        media_type=epoch.media_type,
        headers=headers,
    )


def _iter_recorded_media_epoch_range(
    inspector: RecordedMediaInspector,
    manifest: RecordedMediaManifest,
    epoch_ordinal: int,
    start: int,
    end: int,
) -> Iterator[bytes]:
    matches = tuple(epoch for epoch in manifest.epochs if epoch.ordinal == epoch_ordinal)
    if len(matches) != 1:
        raise SessionIntegrityError("recorded media epoch is unavailable")
    epoch = matches[0]
    offset = 0
    parts = chain(
        ((epoch.init_byte_length, None),),
        ((segment.byte_length, segment.sequence) for segment in epoch.segments),
    )
    for part_length, sequence in parts:
        part_end = offset + part_length - 1
        if part_end < start:
            offset += part_length
            continue
        if offset > end:
            break
        media_file = (
            inspector.get_init(manifest, epoch_ordinal)
            if sequence is None
            else inspector.get_segment(manifest, epoch_ordinal, sequence)
        )
        local_start = max(0, start - offset)
        local_end = min(part_length - 1, end - offset)
        yield media_file.payload[local_start : local_end + 1]
        offset += part_length


def _parse_byte_range(value: str, byte_length: int) -> tuple[int, int]:
    match = re.fullmatch(r"bytes=(\d*)-(\d*)", value.strip())
    if match is None or byte_length < 1:
        raise HTTPException(
            status_code=416,
            detail="Некорректный Range для записанного медиасегмента.",
            headers={"Content-Range": f"bytes */{byte_length}"},
        )
    raw_start, raw_end = match.groups()
    if not raw_start and not raw_end:
        raise HTTPException(
            status_code=416,
            detail="Некорректный Range для записанного медиасегмента.",
            headers={"Content-Range": f"bytes */{byte_length}"},
        )
    if not raw_start:
        suffix = int(raw_end)
        if suffix < 1:
            raise HTTPException(
                status_code=416,
                detail="Некорректный Range для записанного медиасегмента.",
                headers={"Content-Range": f"bytes */{byte_length}"},
            )
        start = max(0, byte_length - suffix)
        return start, byte_length - 1
    start = int(raw_start)
    end = byte_length - 1 if not raw_end else int(raw_end)
    if start >= byte_length or end < start:
        raise HTTPException(
            status_code=416,
            detail="Range выходит за границы записанного медиасегмента.",
            headers={"Content-Range": f"bytes */{byte_length}"},
        )
    return start, min(end, byte_length - 1)
