import { useEffect, useRef, useState } from "react";

import type { SceneSettings } from "../sceneSettings";
import type { RecordedAdmissionPhase } from "../core/observation/recordedSessionAdmission";

export type RerunViewportStatus = "idle" | "loading" | "ready" | "error";
export type RecordedRerunView = "spatial" | "perception" | "perception3d" | "metrics";
export type RecordedPerceptionLoadPhase =
  | "idle"
  | "loading"
  | "ready"
  | "unavailable"
  | "error";

export interface RecordedPerceptionLoadState {
  phase: RecordedPerceptionLoadPhase;
  receivedBytes: number;
  totalBytes: number | null;
  progress: number | null;
  message: string;
}

export type RecordedPointColorLoadState = Pick<
  RecordedPerceptionLoadState,
  "phase" | "receivedBytes" | "totalBytes" | "progress" | "message"
>;

export interface RecordedPerceptionLayers {
  enabled: boolean;
  detections2d: boolean;
  segmentation: boolean;
  cuboids3d: boolean;
}

export interface RerunSelection {
  entityPath: string;
  viewName?: string;
  position?: [number, number, number];
}

export interface RerunPlaybackState {
  recordingId: string;
  timeline: string;
  rangeNs: { min: number; max: number } | null;
  currentNs: number;
  playing: boolean;
  /** Latest session-time value currently available to the browser receiver. */
  bufferedEndNs: number | null;
  /** Declared first session-time value, when the archive descriptor provides it. */
  expectedStartNs: number | null;
  /** Declared final session-time value, when the archive descriptor provides it. */
  expectedEndNs: number | null;
  /** Download progress in the closed interval 0..1, or null without a valid expectation. */
  bufferProgress: number | null;
  /** True only when the buffered range has reached the declared archive end. */
  fullyBuffered: boolean;
}

export interface RerunPlaybackController {
  seek: (timeNs: number) => void;
  setPlaying: (playing: boolean) => void;
  jumpToEnd: () => void;
}

export interface RerunViewportProps {
  sourceUrl: string;
  recordedArtifact?: RecordedRrdArtifactDescriptor | null;
  followLive?: boolean;
  autoplayWhenReady?: boolean;
  presentationGate?: RecordedAdmissionPhase;
  expectedTimelineStartSeconds?: number;
  expectedTimelineEndSeconds?: number;
  initialPlaybackStartSeconds?: number;
  onStatusChange?: (status: RerunViewportStatus, message?: string) => void;
  onSelectionChange?: (selection: RerunSelection | null) => void;
  onPlaybackChange?: (state: RerunPlaybackState | null) => void;
  onPlaybackControllerChange?: (controller: RerunPlaybackController | null) => void;
  sceneSettings?: Pick<
    SceneSettings,
    | "accumulationSeconds"
    | "showGrid"
    | "showPoints"
    | "showTrajectory"
    | "pointSize"
    | "colorMode"
    | "palette"
    | "customColor"
  >;
  recordedView?: RecordedRerunView;
  recordedViewResetGeneration?: 0 | 1;
  recordedFollowTrajectory?: boolean;
  recordedPerceptionLayers?: RecordedPerceptionLayers;
  recordedPerceptionRetryGeneration?: number;
  lockPerceptionCameraInteraction?: boolean;
  onPerceptionLoadChange?: (state: RecordedPerceptionLoadState) => void;
  onPointColorLoadChange?: (state: RecordedPointColorLoadState) => void;
}

export interface RecordedRrdArtifactDescriptor {
  sourceUrl: string;
  viewerSourceUrl: string;
  byteLength: number;
  sha256: string;
}

interface RerunBlueprintChannel {
  endpointUrl: string;
  channel: {
    readonly ready: boolean;
    send_rrd: (rrdBytes: Uint8Array) => void;
    close: () => void;
  };
}

interface RecordedRerunIdentity {
  applicationId: "nodedc_mission_core_recorded";
  recordingId: string;
}

const RECORDED_RRD_PATH = /^\/api\/v1\/observation-sessions\/[A-Za-z0-9][A-Za-z0-9._:-]{0,127}\/recording\.rrd$/;
const RECORDED_BLUEPRINT_PATH = /^\/api\/v1\/observation-sessions\/[A-Za-z0-9][A-Za-z0-9._:-]{0,127}\/blueprint\.rrd$/;
const RECORDED_PERCEPTION_PATH = /^\/api\/v1\/observation-sessions\/[A-Za-z0-9][A-Za-z0-9._:-]{0,127}\/perception\.rrd$/;
const RECORDED_POINT_COLORS_PATH = /^\/api\/v1\/observation-sessions\/[A-Za-z0-9][A-Za-z0-9._:-]{0,127}\/point-colors\.rrd$/;
const MAX_BLUEPRINT_BYTES = 1_048_576;
const MAX_PERCEPTION_BYTES = 512 * 1024 * 1024;
const BUFFER_END_TOLERANCE_NS = 1_000_000;
const RECORDED_OPEN_MIN_TIMEOUT_MS = 120_000;
const RECORDED_OPEN_MAX_TIMEOUT_MS = 1_800_000;
const RECORDED_OPEN_GRACE_MS = 30_000;
const RECORDED_OPEN_MIN_BYTES_PER_SECOND = 2 * 1024 * 1024;
const RECORDED_BASE_POINT_COLOR_KEY = "intensity|turbo|-";

export function recordedPointColorKey(
  settings: Pick<SceneSettings, "colorMode" | "palette" | "customColor">,
): string {
  const custom = settings.palette === "custom" || settings.colorMode === "class"
    ? settings.customColor.toLowerCase()
    : "-";
  return `${settings.colorMode}|${settings.palette}|${custom}`;
}

export function recordedOpenWatchdogTimeoutMs(byteLength: number): number {
  if (!Number.isSafeInteger(byteLength) || byteLength < 4) {
    throw new Error("Unsafe recorded RRD byte length");
  }
  const transferBudgetMs = Math.ceil(
    (byteLength / RECORDED_OPEN_MIN_BYTES_PER_SECOND) * 1_000,
  );
  return Math.min(
    RECORDED_OPEN_MAX_TIMEOUT_MS,
    Math.max(RECORDED_OPEN_MIN_TIMEOUT_MS, transferBudgetMs + RECORDED_OPEN_GRACE_MS),
  );
}

export function createRecordedOpenWatchdog<T>({
  byteLength,
  schedule,
  cancel,
  onTimeout,
}: {
  byteLength: number;
  schedule: (callback: () => void, timeoutMs: number) => T;
  cancel: (handle: T) => void;
  onTimeout: () => void;
}): { arm: () => void; clear: () => void; pending: () => boolean } {
  const timeoutMs = recordedOpenWatchdogTimeoutMs(byteLength);
  let handle: T | null = null;
  return {
    arm() {
      if (handle !== null) return;
      handle = schedule(() => {
        handle = null;
        onTimeout();
      }, timeoutMs);
    },
    clear() {
      if (handle === null) return;
      cancel(handle);
      handle = null;
    },
    pending: () => handle !== null,
  };
}

export interface RecordedPlaybackBufferState {
  bufferedEndNs: number | null;
  expectedStartNs: number | null;
  expectedEndNs: number | null;
  bufferProgress: number | null;
  fullyBuffered: boolean;
}

/**
 * A Rerun time range is usable as soon as it contains one finite timestamp.
 * A first frame commonly has min === max; waiting for positive duration would
 * needlessly keep that visible frame behind the loading screen.
 */
export function isUsableRecordedPlaybackRange(
  rangeNs: { min: number; max: number } | null,
): rangeNs is { min: number; max: number } {
  return Boolean(
    rangeNs &&
    Number.isFinite(rangeNs.min) &&
    Number.isFinite(rangeNs.max) &&
    rangeNs.max >= rangeNs.min,
  );
}

/** Describe progressive archive availability without gating first rendering. */
export function recordedPlaybackBufferState(
  rangeNs: { min: number; max: number } | null,
  expectedTimelineEndSeconds?: number,
  expectedTimelineStartSeconds?: number,
): RecordedPlaybackBufferState {
  const usableRange = isUsableRecordedPlaybackRange(rangeNs) ? rangeNs : null;
  const bufferedEndNs = usableRange?.max ?? null;
  if (expectedTimelineEndSeconds === undefined) {
    return {
      bufferedEndNs,
      expectedStartNs: null,
      expectedEndNs: null,
      bufferProgress: null,
      fullyBuffered: usableRange !== null,
    };
  }
  if (!Number.isFinite(expectedTimelineEndSeconds) || expectedTimelineEndSeconds < 0) {
    return {
      bufferedEndNs,
      expectedStartNs: null,
      expectedEndNs: null,
      bufferProgress: null,
      fullyBuffered: false,
    };
  }

  const expectedEndNs = expectedTimelineEndSeconds * 1_000_000_000;
  const expectedStartNs = expectedTimelineStartSeconds === undefined
    ? 0
    : expectedTimelineStartSeconds * 1_000_000_000;
  if (
    !Number.isFinite(expectedEndNs) ||
    !Number.isFinite(expectedStartNs) ||
    expectedStartNs < 0 ||
    expectedEndNs < expectedStartNs
  ) {
    return {
      bufferedEndNs,
      expectedStartNs: null,
      expectedEndNs: null,
      bufferProgress: null,
      fullyBuffered: false,
    };
  }
  // Rerun split preserves whole boundary chunks so that all generated splits
  // still sum exactly to the source recording. A verified bounded artifact can
  // therefore begin slightly before its declared operator window. Admission
  // requires coverage of the declared window, not byte-chunk boundary equality.
  const fullyBuffered = usableRange !== null &&
    usableRange.min <= expectedStartNs + BUFFER_END_TOLERANCE_NS &&
    usableRange.max >= expectedEndNs - BUFFER_END_TOLERANCE_NS;
  const expectedDurationNs = expectedEndNs - expectedStartNs;
  const bufferProgress = bufferedEndNs === null
    ? 0
    : expectedDurationNs <= 0
      ? (fullyBuffered ? 1 : 0)
      : Math.min(1, Math.max(0, (bufferedEndNs - expectedStartNs) / expectedDurationNs));
  return {
    bufferedEndNs,
    expectedStartNs,
    expectedEndNs,
    bufferProgress,
    fullyBuffered,
  };
}

/** A recorded viewport is publishable only after its declared range is complete. */
export function isRecordedPlaybackReady(
  viewerStarted: boolean,
  artifactVerified: boolean,
  buffer: RecordedPlaybackBufferState,
): boolean {
  return viewerStarted && artifactVerified && buffer.fullyBuffered;
}

/** Keep the host scrubber and its controls disconnected from a partial archive. */
export function recordedPlaybackRangeWhenReady(
  rangeNs: { min: number; max: number } | null,
  buffer: RecordedPlaybackBufferState,
  artifactVerified: boolean,
): { min: number; max: number } | null {
  if (
    !artifactVerified ||
    !buffer.fullyBuffered ||
    !isUsableRecordedPlaybackRange(rangeNs)
  ) return null;
  return {
    min: buffer.expectedStartNs === null
      ? rangeNs.min
      : Math.max(rangeNs.min, buffer.expectedStartNs),
    max: buffer.expectedEndNs === null
      ? rangeNs.max
      : Math.min(rangeNs.max, buffer.expectedEndNs),
  };
}

/** Do not mount host timeline controls while a recorded generation is partial. */
export function isRecordedPlaybackPresentationReady(
  status: RerunViewportStatus,
  playback: RerunPlaybackState | null,
): boolean {
  return status === "ready" &&
    playback?.fullyBuffered === true &&
    isUsableRecordedPlaybackRange(playback.rangeNs);
}

export function rerunPresentationStatus(
  status: RerunViewportStatus,
  gate: RecordedAdmissionPhase,
  recorded: boolean,
): RerunViewportStatus {
  if (!recorded) return status;
  if (status === "error" || gate === "error") return "error";
  if (gate !== "ready") return "loading";
  return status;
}

export function isRecordedPlaybackFullyBuffered(
  rangeNs: { min: number; max: number } | null,
  expectedTimelineEndSeconds?: number,
): boolean {
  return recordedPlaybackBufferState(rangeNs, expectedTimelineEndSeconds).fullyBuffered;
}

export function attemptRecordedAutoplay(
  seekToStart: () => void,
  startPlaying: () => void,
): boolean {
  try {
    seekToStart();
    startPlaying();
    return true;
  } catch {
    return false;
  }
}

/**
 * Admit autoplay once, after the viewer and the complete recorded range are
 * ready. The attempt is consumed even if a vendor call throws: subsequent
 * polling must never seek the operator back to the beginning a second time.
 */
export function createRecordedAutoplayGate(): {
  attempt: (
    viewerStarted: boolean,
    fullyBuffered: boolean,
    presentationReady: boolean,
    rangeNs: { min: number; max: number } | null,
    seekToStart: (startNs: number) => void,
    startPlaying: () => void,
    preferredStartNs?: number,
  ) => boolean;
  attempted: () => boolean;
} {
  let consumed = false;
  return {
    attempt(
      viewerStarted,
      fullyBuffered,
      presentationReady,
      rangeNs,
      seekToStart,
      startPlaying,
      preferredStartNs,
    ) {
      if (
        consumed ||
        !viewerStarted ||
        !fullyBuffered ||
        !presentationReady ||
        !isUsableRecordedPlaybackRange(rangeNs)
      ) return false;
      consumed = true;
      const startNs = Number.isFinite(preferredStartNs)
        ? Math.min(Math.max(preferredStartNs as number, rangeNs.min), rangeNs.max)
        : rangeNs.min;
      return attemptRecordedAutoplay(
        () => seekToStart(startNs),
        startPlaying,
      );
    },
    attempted: () => consumed,
  };
}

export function canPublishRecordedPlaybackController(
  readyToRender: boolean,
  presentationGate: RecordedAdmissionPhase,
): boolean {
  return readyToRender && presentationGate === "ready";
}

export function createLatestAnimationFrameEmitter<T>({
  emit,
  requestFrame,
  cancelFrame,
}: {
  emit: (value: T) => void;
  requestFrame: (callback: FrameRequestCallback) => number;
  cancelFrame: (handle: number) => void;
}): { push: (value: T) => void; cancel: () => void } {
  let frameHandle: number | null = null;
  let latestValue: T | undefined;
  let hasPendingValue = false;
  let cancelled = false;

  return {
    push(value) {
      if (cancelled) return;
      latestValue = value;
      hasPendingValue = true;
      if (frameHandle !== null) return;
      frameHandle = requestFrame(() => {
        frameHandle = null;
        if (cancelled || !hasPendingValue) return;
        const valueToEmit = latestValue as T;
        latestValue = undefined;
        hasPendingValue = false;
        emit(valueToEmit);
      });
    },
    cancel() {
      cancelled = true;
      latestValue = undefined;
      hasPendingValue = false;
      if (frameHandle === null) return;
      cancelFrame(frameHandle);
      frameHandle = null;
    },
  };
}

export function resolveRerunSourceUrl(sourceUrl: string, origin: string): string {
  const normalized = sourceUrl.trim();
  if (!normalized.startsWith("/") || normalized.startsWith("//")) return normalized;

  const base = new URL(origin);
  return new URL(normalized, `${base.origin}/`).href;
}

/**
 * The native Rerun HTTP receiver is the only browser API in 0.34.1 with a
 * persistent incremental RRD decoder. Bind it to the exact immutable
 * generation selected by the launch contract; arbitrary `send_rrd` byte
 * slices are independently decoded files and are therefore invalid.
 */
export function resolveRecordedViewerSourceUrl(
  descriptor: RecordedRrdArtifactDescriptor,
  origin: string,
): string {
  const base = new URL(origin);
  const expectedViewerSourceUrl = `${descriptor.sourceUrl}?generation=${descriptor.sha256}`;
  const endpoint = new URL(descriptor.viewerSourceUrl, `${base.origin}/`);
  if (
    !RECORDED_RRD_PATH.test(descriptor.sourceUrl) ||
    descriptor.viewerSourceUrl !== expectedViewerSourceUrl ||
    endpoint.origin !== base.origin ||
    endpoint.pathname !== descriptor.sourceUrl ||
    endpoint.search !== `?generation=${descriptor.sha256}` ||
    endpoint.hash ||
    !Number.isSafeInteger(descriptor.byteLength) ||
    descriptor.byteLength < 4 ||
    !/^[a-f0-9]{64}$/.test(descriptor.sha256)
  ) {
    throw new Error("Unsafe recorded RRD viewer descriptor");
  }
  return endpoint.href;
}

export function rerunViewerInitialSource(
  resolvedSourceUrl: string,
): string {
  return resolvedSourceUrl;
}

export function resolveRecordedBlueprintUrl(sourceUrl: string, origin: string): string | null {
  const normalized = sourceUrl.trim();
  if (!RECORDED_RRD_PATH.test(normalized)) return null;
  const base = new URL(origin);
  const endpoint = new URL(
    normalized.replace(/\/recording\.rrd$/, "/blueprint.rrd"),
    `${base.origin}/`,
  );
  return endpoint.origin === base.origin ? endpoint.href : null;
}

export function resolveRecordedPerceptionUrl(sourceUrl: string, origin: string): string | null {
  const normalized = sourceUrl.trim();
  if (!RECORDED_RRD_PATH.test(normalized)) return null;
  const base = new URL(origin);
  const endpoint = new URL(
    normalized.replace(/\/recording\.rrd$/, "/perception.rrd"),
    `${base.origin}/`,
  );
  return endpoint.origin === base.origin ? endpoint.href : null;
}

export function resolveRecordedPointColorsUrl(sourceUrl: string, origin: string): string | null {
  const normalized = sourceUrl.trim();
  if (!RECORDED_RRD_PATH.test(normalized)) return null;
  const base = new URL(origin);
  const endpoint = new URL(
    normalized.replace(/\/recording\.rrd$/, "/point-colors.rrd"),
    `${base.origin}/`,
  );
  return endpoint.origin === base.origin ? endpoint.href : null;
}

export async function fetchRecordedPointColorsRrd(
  endpointUrl: string,
  settings: Pick<SceneSettings, "colorMode" | "palette" | "customColor">,
  identity: RecordedRerunIdentity,
  {
    origin,
    signal,
    fetcher = globalThis.fetch,
  }: {
    origin: string;
    signal?: AbortSignal;
    fetcher?: typeof globalThis.fetch;
  },
): Promise<Uint8Array> {
  const base = new URL(origin);
  const endpoint = new URL(endpointUrl, base.origin);
  if (
    endpoint.origin !== base.origin ||
    endpoint.search ||
    endpoint.hash ||
    !RECORDED_POINT_COLORS_PATH.test(endpoint.pathname) ||
    !["intensity", "height", "distance", "rgb", "class"].includes(settings.colorMode) ||
    !["turbo", "viridis", "plasma", "grayscale", "custom"].includes(settings.palette) ||
    !/^#[0-9A-Fa-f]{6}$/.test(settings.customColor) ||
    identity.applicationId !== "nodedc_mission_core_recorded" ||
    !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(identity.recordingId)
  ) {
    throw new Error("Unsafe recorded point-color request");
  }
  const response = await fetcher(endpoint.href, {
    method: "POST",
    credentials: "same-origin",
    headers: {
      Accept: "application/vnd.rerun.rrd",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      application_id: identity.applicationId,
      recording_id: identity.recordingId,
      color_mode: settings.colorMode,
      palette: settings.palette,
      custom_color: settings.customColor,
    }),
    signal,
  });
  const contentType = response.headers.get("Content-Type")?.split(";", 1)[0].trim();
  const declaredLength = Number(response.headers.get("Content-Length"));
  if (
    !response.ok ||
    contentType !== "application/vnd.rerun.rrd" ||
    (Number.isFinite(declaredLength) && declaredLength < 4)
  ) {
    throw new Error("Invalid recorded point-color response");
  }
  const payload = new Uint8Array(await response.arrayBuffer());
  if (
    payload.byteLength < 4 ||
    payload[0] !== 0x52 ||
    payload[1] !== 0x52 ||
    payload[2] !== 0x46 ||
    payload[3] !== 0x32
  ) {
    throw new Error("Invalid recorded point-color RRD");
  }
  return payload;
}

export async function fetchRecordedBlueprintRrd(
  endpointUrl: string,
  settings: Pick<
    SceneSettings,
    | "accumulationSeconds"
    | "showGrid"
    | "showPoints"
    | "showTrajectory"
    | "pointSize"
    | "colorMode"
    | "palette"
    | "customColor"
  >,
  identity: RecordedRerunIdentity,
  {
    origin,
    blueprintSessionId,
    signal,
    activeView = "spatial",
    viewResetGeneration = 0,
    followTrajectory = false,
    perceptionLayers = {
      enabled: false,
      detections2d: false,
      segmentation: false,
      cuboids3d: false,
    },
    fetcher = globalThis.fetch,
  }: {
    origin: string;
    blueprintSessionId: string;
    signal?: AbortSignal;
    activeView?: RecordedRerunView;
    viewResetGeneration?: 0 | 1;
    followTrajectory?: boolean;
    perceptionLayers?: RecordedPerceptionLayers;
    fetcher?: typeof globalThis.fetch;
  },
): Promise<Uint8Array> {
  const base = new URL(origin);
  const endpoint = new URL(endpointUrl, base.origin);
  if (
    endpoint.origin !== base.origin ||
    endpoint.search ||
    endpoint.hash ||
    !RECORDED_BLUEPRINT_PATH.test(endpoint.pathname) ||
    !Number.isFinite(settings.accumulationSeconds) ||
    settings.accumulationSeconds < 0 ||
    settings.accumulationSeconds > 3600 ||
    !Number.isFinite(settings.pointSize) ||
    settings.pointSize < 0.1 ||
    settings.pointSize > 32 ||
    !["intensity", "height", "distance", "rgb", "class"].includes(settings.colorMode) ||
    !["turbo", "viridis", "plasma", "grayscale", "custom"].includes(settings.palette) ||
    !/^#[0-9A-Fa-f]{6}$/.test(settings.customColor) ||
    !["spatial", "perception", "perception3d", "metrics"].includes(activeView) ||
    ![0, 1].includes(viewResetGeneration) ||
    [
      perceptionLayers.enabled,
      perceptionLayers.detections2d,
      perceptionLayers.segmentation,
      perceptionLayers.cuboids3d,
    ].some((value) => typeof value !== "boolean") ||
    identity.applicationId !== "nodedc_mission_core_recorded" ||
    !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(identity.recordingId) ||
    !/^[a-f0-9]{32}$/.test(blueprintSessionId)
  ) {
    throw new Error("Unsafe recorded blueprint request");
  }
  const response = await fetcher(endpoint.href, {
    method: "POST",
    credentials: "same-origin",
    headers: {
      Accept: "application/vnd.rerun.rrd",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      application_id: identity.applicationId,
      recording_id: identity.recordingId,
      blueprint_session_id: blueprintSessionId,
      accumulation_seconds: settings.accumulationSeconds,
      show_grid: settings.showGrid,
      show_points: settings.showPoints,
      show_trajectory: settings.showTrajectory,
      point_size: settings.pointSize,
      color_mode: settings.colorMode,
      palette: settings.palette,
      custom_color: settings.customColor,
      active_view: activeView,
      view_reset_generation: viewResetGeneration,
      follow_trajectory: followTrajectory,
      unified_perception:
        perceptionLayers.detections2d || perceptionLayers.segmentation,
      show_detections_2d: perceptionLayers.detections2d,
      show_segmentation: perceptionLayers.segmentation,
      show_cuboids_3d: perceptionLayers.cuboids3d,
    }),
    signal,
  });
  const contentType = response.headers.get("Content-Type")?.split(";", 1)[0].trim();
  const declaredLength = Number(response.headers.get("Content-Length"));
  if (
    !response.ok ||
    contentType !== "application/vnd.rerun.rrd" ||
    (Number.isFinite(declaredLength) && declaredLength > MAX_BLUEPRINT_BYTES)
  ) {
    throw new Error("Invalid recorded blueprint response");
  }
  const payload = new Uint8Array(await response.arrayBuffer());
  if (
    payload.byteLength < 4 ||
    payload.byteLength > MAX_BLUEPRINT_BYTES ||
    payload[0] !== 0x52 ||
    payload[1] !== 0x52 ||
    payload[2] !== 0x46 ||
    payload[3] !== 0x32
  ) {
    throw new Error("Invalid recorded blueprint RRD");
  }
  return payload;
}

export async function fetchRecordedPerceptionRrd(
  endpointUrl: string,
  identity: RecordedRerunIdentity,
  {
    origin,
    signal,
    fetcher = globalThis.fetch,
    onProgress,
  }: {
    origin: string;
    signal?: AbortSignal;
    fetcher?: typeof globalThis.fetch;
    onProgress?: (receivedBytes: number, totalBytes: number) => void;
  },
): Promise<Uint8Array | null> {
  const base = new URL(origin);
  const endpoint = new URL(endpointUrl, base.origin);
  if (
    endpoint.origin !== base.origin ||
    endpoint.search ||
    endpoint.hash ||
    !RECORDED_PERCEPTION_PATH.test(endpoint.pathname) ||
    identity.applicationId !== "nodedc_mission_core_recorded" ||
    !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(identity.recordingId)
  ) {
    throw new Error("Unsafe recorded perception request");
  }
  const response = await fetcher(endpoint.href, {
    method: "POST",
    credentials: "same-origin",
    headers: {
      Accept: "application/vnd.rerun.rrd",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      application_id: identity.applicationId,
      recording_id: identity.recordingId,
    }),
    signal,
  });
  if (response.status === 204) return null;
  const contentType = response.headers.get("Content-Type")?.split(";", 1)[0].trim();
  const declaredLength = Number(response.headers.get("Content-Length"));
  if (
    !response.ok ||
    contentType !== "application/vnd.rerun.rrd" ||
    !Number.isSafeInteger(declaredLength) ||
    declaredLength < 4 ||
    declaredLength > MAX_PERCEPTION_BYTES
  ) {
    throw new Error("Invalid recorded perception response");
  }
  const payload = new Uint8Array(declaredLength);
  let receivedBytes = 0;
  onProgress?.(receivedBytes, declaredLength);
  if (response.body) {
    const reader = response.body.getReader();
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      if (receivedBytes + value.byteLength > declaredLength) {
        await reader.cancel();
        throw new Error("Recorded perception response exceeds its declared length");
      }
      payload.set(value, receivedBytes);
      receivedBytes += value.byteLength;
      onProgress?.(receivedBytes, declaredLength);
    }
  } else {
    const fallback = new Uint8Array(await response.arrayBuffer());
    if (fallback.byteLength <= declaredLength) {
      payload.set(fallback);
      receivedBytes = fallback.byteLength;
      onProgress?.(receivedBytes, declaredLength);
    }
  }
  if (
    receivedBytes !== declaredLength ||
    payload[0] !== 0x52 ||
    payload[1] !== 0x52 ||
    payload[2] !== 0x46 ||
    payload[3] !== 0x32
  ) {
    throw new Error("Invalid recorded perception RRD");
  }
  return payload;
}

export function RerunViewport({
  sourceUrl,
  recordedArtifact = null,
  followLive = false,
  autoplayWhenReady = false,
  presentationGate = "ready",
  expectedTimelineStartSeconds,
  expectedTimelineEndSeconds,
  initialPlaybackStartSeconds,
  onStatusChange,
  onSelectionChange,
  onPlaybackChange,
  onPlaybackControllerChange,
  sceneSettings,
  recordedView = "spatial",
  recordedViewResetGeneration = 0,
  recordedFollowTrajectory = false,
  recordedPerceptionLayers = {
    enabled: false,
    detections2d: false,
    segmentation: false,
    cuboids3d: false,
  },
  recordedPerceptionRetryGeneration = 0,
  lockPerceptionCameraInteraction = false,
  onPerceptionLoadChange,
  onPointColorLoadChange,
}: RerunViewportProps) {
  const hostRef = useRef<HTMLDivElement>(null);
  const [status, setStatus] = useState<RerunViewportStatus>(sourceUrl ? "loading" : "idle");
  const [recordingBufferProgress, setRecordingBufferProgress] = useState<number | null>(null);
  const [retryNonce, setRetryNonce] = useState(0);
  const blueprintChannelRef = useRef<RerunBlueprintChannel | null>(null);
  const perceptionChannelRef = useRef<RerunBlueprintChannel | null>(null);
  const loadedPerceptionChannelRef = useRef<RerunBlueprintChannel | null>(null);
  const appliedPointColorKeyRef = useRef<string | null>(null);
  const recordedIdentityRef = useRef<RecordedRerunIdentity | null>(null);
  const blueprintSessionIdRef = useRef(crypto.randomUUID().replaceAll("-", ""));
  const presentationGateRef = useRef(presentationGate);
  presentationGateRef.current = presentationGate;
  const [blueprintChannelRevision, setBlueprintChannelRevision] = useState(0);
  const [perceptionChannelRevision, setPerceptionChannelRevision] = useState(0);
  const recordedBlueprintUrl = sourceUrl
    ? resolveRecordedBlueprintUrl(sourceUrl, window.location.origin)
    : null;
  const recordedPerceptionUrl = sourceUrl
    ? resolveRecordedPerceptionUrl(sourceUrl, window.location.origin)
    : null;
  const recordedPointColorsUrl = sourceUrl
    ? resolveRecordedPointColorsUrl(sourceUrl, window.location.origin)
    : null;
  const presentationStatus = rerunPresentationStatus(
    status,
    presentationGate,
    recordedArtifact !== null,
  );

  useEffect(() => {
    const normalizedSource = sourceUrl.trim();
    const host = hostRef.current;
    if (!normalizedSource || !host) {
      setStatus("idle");
      setRecordingBufferProgress(null);
      onStatusChange?.("idle");
      onSelectionChange?.(null);
      onPlaybackChange?.(null);
      onPlaybackControllerChange?.(null);
      return;
    }
    const isRecordedSource = RECORDED_RRD_PATH.test(normalizedSource);
    let resolvedSource: string;
    try {
      if (isRecordedSource) {
        if (!recordedArtifact || recordedArtifact.sourceUrl !== normalizedSource) {
          throw new Error("Recorded generation is missing");
        }
        resolvedSource = resolveRecordedViewerSourceUrl(
          recordedArtifact,
          window.location.origin,
        );
      } else {
        resolvedSource = resolveRerunSourceUrl(normalizedSource, window.location.origin);
      }
    } catch {
      host.replaceChildren();
      setStatus("error");
      setRecordingBufferProgress(null);
      onStatusChange?.("error", "Запись не привязана к проверенному поколению архива.");
      onSelectionChange?.(null);
      onPlaybackChange?.(null);
      onPlaybackControllerChange?.(null);
      return;
    }

    let disposed = false;
    let disposeViewer: (() => void) | undefined;
    let recordingOpenTimer: number | undefined;
    let recordedOpenWatchdog: {
      arm: () => void;
      clear: () => void;
      pending: () => boolean;
    } | null = null;
    let playbackRangeTimer: number | undefined;
    let recordingOpened = false;
    let recordingOpenTimedOut = false;
    let viewerStartResolved = false;
    // The backend serves this exact digest-bound URL or rejects it with 412.
    // Presentation still waits for Rerun to decode the complete declared range.
    const artifactVerified = true;
    let recordedSceneAdmitted = !isRecordedSource;
    let publishPlaybackBufferState: (() => void) | null = null;
    let playbackState: RerunPlaybackState | null = null;
    const recordedAutoplay = createRecordedAutoplayGate();
    let blueprintChannel: RerunBlueprintChannel | null = null;
    let perceptionChannel: RerunBlueprintChannel | null = null;
    const unsubscribers: Array<() => void> = [];
    const unsubscribeAll = () => {
      while (unsubscribers.length > 0) {
        const unsubscribe = unsubscribers.pop();
        try {
          unsubscribe?.();
        } catch {
          // A stopped vendor viewer can already have released its event map.
        }
      }
    };
    const clearRecordedAdmissionWatchdog = () => recordedOpenWatchdog?.clear();
    const clearLiveRecordingOpenTimer = () => {
      if (recordingOpenTimer !== undefined) {
        window.clearTimeout(recordingOpenTimer);
        recordingOpenTimer = undefined;
      }
    };
    const clearRecordingTimers = () => {
      clearRecordedAdmissionWatchdog();
      clearLiveRecordingOpenTimer();
    };
    const clearPlaybackRangeTimer = () => {
      if (playbackRangeTimer === undefined) return;
      window.clearInterval(playbackRangeTimer);
      playbackRangeTimer = undefined;
    };
    const emitPlayback = (patch: Partial<RerunPlaybackState> = {}) => {
      if (!playbackState || disposed) return;
      playbackState = { ...playbackState, ...patch };
      if (recordedSceneAdmitted) onPlaybackChange?.(playbackState);
    };
    const playbackTimeUpdates = createLatestAnimationFrameEmitter<{
      recordingId: string;
      timeNs: number;
    }>({
      emit: ({ recordingId, timeNs }) => {
        if (disposed || recordingId !== playbackState?.recordingId) return;
        emitPlayback({ currentNs: timeNs });
      },
      requestFrame: (callback) => window.requestAnimationFrame(callback),
      cancelFrame: (handle) => window.cancelAnimationFrame(handle),
    });
    const reportError = (message: string) => {
      if (disposed) return;
      setStatus("error");
      onStatusChange?.("error", message);
    };

    host.replaceChildren();
    appliedPointColorKeyRef.current = null;
    setStatus("loading");
    setRecordingBufferProgress(null);
    onStatusChange?.("loading");

    void import("@rerun-io/web-viewer")
      .then(async ({ WebViewer }) => {
        // React StrictMode intentionally disposes the first effect while the
        // dynamic import is still pending. Never start that stale viewer (the
        // vendor API treats a missing host as document.body).
        if (disposed) return;

        const viewer = new WebViewer();
        let viewerDisposed = false;
        disposeViewer = () => {
          if (viewerDisposed) return;
          viewerDisposed = true;
          clearRecordingTimers();
          clearPlaybackRangeTimer();
          playbackTimeUpdates.cancel();
          // Pause before closing the receiver. Besides matching the operator
          // lifecycle, this prevents the old WASM render loop from advancing
          // while React prepares the next saved session.
          if (playbackState) {
            try {
              viewer.set_playing(playbackState.recordingId, false);
            } catch {
              // The recording may already have been removed by the receiver.
            }
          }
          unsubscribeAll();
          onPlaybackControllerChange?.(null);
          onPlaybackChange?.(null);
          if (blueprintChannelRef.current === blueprintChannel) {
            blueprintChannelRef.current = null;
          }
          if (perceptionChannelRef.current === perceptionChannel) {
            perceptionChannelRef.current = null;
          }
          recordedIdentityRef.current = null;
          try {
            blueprintChannel?.channel.close();
          } catch {
            // The viewer may already have closed all auxiliary channels.
          }
          try {
            perceptionChannel?.channel.close();
          } catch {
            // The viewer may already have closed all auxiliary channels.
          }
          try {
            if (viewer.ready) viewer.close(resolvedSource);
          } catch {
            // The receiver may already be gone after a transport failure.
          }
          try {
            viewer.stop();
          } catch {
            // A partially initialized WASM handle can already be gone after a
            // startup failure. The host still has to be cleared below.
          }
          host.replaceChildren();
        };
        if (isRecordedSource && recordedArtifact) {
          recordedOpenWatchdog = createRecordedOpenWatchdog({
            byteLength: recordedArtifact.byteLength,
            schedule: (callback, timeoutMs) => window.setTimeout(callback, timeoutMs),
            cancel: (handle) => window.clearTimeout(handle),
            onTimeout: () => {
              if (disposed || recordedSceneAdmitted) return;
              recordingOpenTimedOut = true;
              disposeViewer?.();
              reportError("Поколение записи не загрузилось полностью за расчётное время.");
            },
          });
        }

        unsubscribers.push(
          viewer.on("recording_open", (event) => {
            if (
              disposed ||
              recordingOpened ||
              (isRecordedSource && event.application_id !== "nodedc_mission_core_recorded")
            ) return;
            recordingOpened = true;
            if (
              recordedBlueprintUrl &&
              event.application_id === "nodedc_mission_core_recorded" &&
              /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(event.recording_id)
            ) {
              const current = recordedIdentityRef.current;
              if (
                current?.applicationId !== event.application_id ||
                current.recordingId !== event.recording_id
              ) {
                recordedIdentityRef.current = {
                  applicationId: event.application_id,
                  recordingId: event.recording_id,
                };
                setBlueprintChannelRevision((revision) => revision + 1);
                setPerceptionChannelRevision((revision) => revision + 1);
              }
            }
            if (!isRecordedSource) clearLiveRecordingOpenTimer();
            // Durable Mission Core recordings use a zero-based session clock.
            // Keeping wall-clock capture_time as a secondary RRD timeline is
            // useful for audit, but the operator scrubber must remain a small,
            // exact duration value that JavaScript can seek without precision
            // loss.
            const timeline = followLive ? "stream_time" : "session_time";
            try {
              // The backend blueprint owns native `Following`; the host only
              // selects its live timeline once. Repeated SetTime calls would
              // force Rerun out of Following and create needless repaints.
              viewer.set_active_timeline(event.recording_id, timeline);
            } catch {
              // A receiver can disappear between the event and the WASM call.
            }
            let rangeNs = viewer.get_time_range(event.recording_id, timeline);
            let currentNs = viewer.get_current_time(event.recording_id, timeline);
            let playing = viewer.get_playing(event.recording_id);
            if (!followLive && playing) {
              try {
                viewer.set_playing(event.recording_id, false);
                playing = false;
              } catch {
                // Loading can race a receiver closing after a failed request.
              }
            }
            const initialRecordedBuffer = recordedPlaybackBufferState(
              rangeNs,
              expectedTimelineEndSeconds,
              expectedTimelineStartSeconds,
            );
            playbackState = {
              recordingId: event.recording_id,
              timeline,
              // The host timeline must not expose a partially decoded archive.
              // Buffer telemetry below still reports download progress.
              rangeNs: followLive ? rangeNs : null,
              currentNs,
              playing,
              ...(followLive
                ? {
                    bufferedEndNs: rangeNs?.max ?? null,
                    expectedStartNs: null,
                    expectedEndNs: null,
                    bufferProgress: null,
                    fullyBuffered: true,
                  }
                : initialRecordedBuffer),
            };
            if (followLive) onPlaybackChange?.(playbackState);
            const playbackController: RerunPlaybackController = {
              seek: (timeNs) => {
                const range = playbackState?.rangeNs;
                if (!range) return;
                const clamped = Math.min(Math.max(timeNs, range.min), range.max);
                viewer.set_current_time(event.recording_id, timeline, clamped);
                emitPlayback({ currentNs: clamped, playing: false });
              },
              setPlaying: (playing) => {
                viewer.set_playing(event.recording_id, playing);
                emitPlayback({ playing });
              },
              jumpToEnd: () => {
                const range = playbackState?.rangeNs;
                if (!range) return;
                viewer.set_current_time(event.recording_id, timeline, range.max);
                emitPlayback({ rangeNs: range, currentNs: range.max, playing: false });
              },
            };
            let playbackControllerPublished = false;
            if (followLive) {
              playbackControllerPublished = true;
              onPlaybackControllerChange?.(playbackController);
            }
            let readyPublished = false;
            const publishBufferedState = () => {
              if (disposed || !playbackState) return;
              try {
                rangeNs = viewer.get_time_range(event.recording_id, timeline);
                currentNs = viewer.get_current_time(event.recording_id, timeline);
                playing = viewer.get_playing(event.recording_id);
              } catch {
                return;
              }
              const recordedBuffer = recordedPlaybackBufferState(
                rangeNs,
                expectedTimelineEndSeconds,
                expectedTimelineStartSeconds,
              );
              if (!followLive && artifactVerified) {
                setRecordingBufferProgress(recordedBuffer.bufferProgress);
              }
              const readyToRender = followLive
                ? viewerStartResolved
                : isRecordedPlaybackReady(viewerStartResolved, artifactVerified, recordedBuffer);
              const presentationReady = presentationGateRef.current === "ready";
              if (!followLive && (!readyToRender || !presentationReady) && playing) {
                try {
                  viewer.set_playing(event.recording_id, false);
                  playing = false;
                } catch {
                  return;
                }
              }
              if (autoplayWhenReady && !followLive && readyToRender) {
                const autoplayStarted = recordedAutoplay.attempt(
                  viewerStartResolved,
                  recordedBuffer.fullyBuffered,
                  presentationReady,
                  rangeNs,
                  (autoplayStartNs) => {
                    viewer.set_current_time(event.recording_id, timeline, autoplayStartNs);
                    currentNs = autoplayStartNs;
                  },
                  () => {
                    viewer.set_playing(event.recording_id, true);
                  },
                  initialPlaybackStartSeconds === undefined
                    ? expectedTimelineStartSeconds === undefined
                      ? undefined
                      : expectedTimelineStartSeconds * 1_000_000_000
                    : initialPlaybackStartSeconds * 1_000_000_000,
                );
                if (autoplayStarted) {
                  playing = true;
                }
              }
              emitPlayback({
                rangeNs: followLive
                  ? rangeNs
                  : recordedPlaybackRangeWhenReady(rangeNs, recordedBuffer, artifactVerified),
                currentNs,
                playing,
                ...(followLive
                  ? {
                      bufferedEndNs: rangeNs?.max ?? null,
                      expectedStartNs: null,
                      expectedEndNs: null,
                      bufferProgress: null,
                      fullyBuffered: true,
                    }
                  : recordedBuffer),
              });
              if (readyToRender && !readyPublished) {
                readyPublished = true;
                if (!followLive) clearRecordedAdmissionWatchdog();
                if (!followLive) setRecordingBufferProgress(1);
                recordedSceneAdmitted = true;
                if (!followLive) onPlaybackChange?.(playbackState);
                setStatus("ready");
                onStatusChange?.("ready");
              }
              if (
                !followLive &&
                !playbackControllerPublished &&
                canPublishRecordedPlaybackController(readyToRender, presentationGateRef.current)
              ) {
                playbackControllerPublished = true;
                onPlaybackControllerChange?.(playbackController);
              }
            };
            publishPlaybackBufferState = publishBufferedState;
            clearPlaybackRangeTimer();
            publishBufferedState();
            playbackRangeTimer = window.setInterval(publishBufferedState, 500);
          }),
        );

        unsubscribers.push(
          viewer.on("time_update", (event) => {
            if (disposed || event.recording_id !== playbackState?.recordingId) return;
            playbackTimeUpdates.push({
              recordingId: event.recording_id,
              timeNs: event.time,
            });
          }),
          viewer.on("play", (event) => {
            if (disposed || event.recording_id !== playbackState?.recordingId) return;
            emitPlayback({ playing: true });
          }),
          viewer.on("pause", (event) => {
            if (disposed || event.recording_id !== playbackState?.recordingId) return;
            emitPlayback({ playing: false });
          }),
        );

        unsubscribers.push(
          viewer.on("selection_change", (event) => {
            if (disposed || !recordedSceneAdmitted) return;
            const entity = event.items.find((item) => item.type === "entity");
            if (!entity || entity.type !== "entity") {
              onSelectionChange?.(null);
              return;
            }
            onSelectionChange?.({
              entityPath: entity.entity_path,
              viewName: entity.view_name,
              position: entity.position,
            });
          }),
        );

        try {
          // `panel_state_overrides` is part of Rerun's runtime AppOptions but
          // omitted from the public WebViewerOptions declaration in 0.34.1.
          // Its generated TypeScript declaration says `hidden`, while the
          // WASM constructor actually deserializes the Rust enum `Hidden`.
          // Keep the post-start overrides below as a compatibility fallback.
          const viewerOptions = {
            width: "100%",
            height: "100%",
            theme: "dark",
            hide_welcome_screen: true,
            enable_history: false,
            allow_fullscreen: false,
            panel_state_overrides: {
              top: "Hidden",
              blueprint: "Hidden",
              selection: "Hidden",
              time: "Hidden",
            },
          } as Parameters<typeof viewer.start>[2] & {
            panel_state_overrides: Record<string, "Hidden">;
          };
          await viewer.start(
            rerunViewerInitialSource(resolvedSource),
            host,
            viewerOptions,
            null,
          );
          if (disposed) {
            disposeViewer();
            return;
          }
          viewerStartResolved = true;
          if (isRecordedSource && !recordedSceneAdmitted) recordedOpenWatchdog?.arm();
          publishPlaybackBufferState?.();

          viewer.override_panel_state("top", "hidden");
          viewer.override_panel_state("blueprint", "hidden");
          viewer.override_panel_state("selection", "hidden");
          viewer.override_panel_state("time", "hidden");

          if (recordedBlueprintUrl) {
            const channel = viewer.open_channel("missioncore/recorded-blueprint");
            blueprintChannel = { endpointUrl: recordedBlueprintUrl, channel };
            blueprintChannelRef.current = blueprintChannel;
            setBlueprintChannelRevision((revision) => revision + 1);
          }
          if (recordedPerceptionUrl) {
            const channel = viewer.open_channel("missioncore/recorded-perception");
            perceptionChannel = { endpointUrl: recordedPerceptionUrl, channel };
            perceptionChannelRef.current = perceptionChannel;
            setPerceptionChannelRevision((revision) => revision + 1);
          }

          if (!recordingOpened && !isRecordedSource) {
            recordingOpenTimer = window.setTimeout(() => {
              recordingOpenTimedOut = true;
              disposeViewer?.();
              reportError("Визуализатор запущен, но поток записи не открылся.");
            }, 12_000);
          }
        } catch {
          disposeViewer();
          if (!recordingOpenTimedOut && !disposed) {
            reportError(isRecordedSource
              ? "Запись не прошла атомарную проверку целостности."
              : "Не удалось запустить встроенный визуализатор.");
          }
        }
      })
      .catch(() => {
        if (disposed) return;
        disposeViewer?.();
        reportError("Не удалось загрузить модуль визуализатора.");
      });

    return () => {
      disposed = true;
      clearRecordingTimers();
      clearPlaybackRangeTimer();
      playbackTimeUpdates.cancel();
      unsubscribeAll();
      disposeViewer?.();
      onSelectionChange?.(null);
      onPlaybackControllerChange?.(null);
      onPlaybackChange?.(null);
    };
  }, [
    autoplayWhenReady,
    expectedTimelineEndSeconds,
    expectedTimelineStartSeconds,
      followLive,
    initialPlaybackStartSeconds,
    onPlaybackChange,
    onPlaybackControllerChange,
    onSelectionChange,
    onStatusChange,
    recordedArtifact?.byteLength,
    recordedArtifact?.sha256,
    recordedArtifact?.sourceUrl,
    recordedArtifact?.viewerSourceUrl,
    retryNonce,
    sourceUrl,
  ]);

  useEffect(() => {
    loadedPerceptionChannelRef.current = null;
  }, [recordedPerceptionUrl]);

  useEffect(() => {
    appliedPointColorKeyRef.current = null;
  }, [recordedPointColorsUrl]);

  useEffect(() => {
    if (!recordedPerceptionUrl) return;
    const active = perceptionChannelRef.current;
    const identity = recordedIdentityRef.current;
    if (
      !active ||
      !identity ||
      active.endpointUrl !== recordedPerceptionUrl ||
      !active.channel.ready
    ) return;
    if (loadedPerceptionChannelRef.current === active) {
      onPerceptionLoadChange?.({
        phase: "ready",
        receivedBytes: 0,
        totalBytes: null,
        progress: 1,
        message: "AI-слои готовы.",
      });
      return;
    }
    const abort = new AbortController();
    let lastReportedPercent = -1;
    onPerceptionLoadChange?.({
      phase: "loading",
      receivedBytes: 0,
      totalBytes: null,
      progress: null,
      message: "Сервер готовит AI-слои.",
    });
    void fetchRecordedPerceptionRrd(recordedPerceptionUrl, identity, {
      origin: window.location.origin,
      signal: abort.signal,
      onProgress: (receivedBytes, totalBytes) => {
        const progress = totalBytes > 0 ? receivedBytes / totalBytes : null;
        const percent = progress === null ? -1 : Math.floor(progress * 100);
        if (percent === lastReportedPercent && receivedBytes !== totalBytes) return;
        lastReportedPercent = percent;
        onPerceptionLoadChange?.({
          phase: "loading",
          receivedBytes,
          totalBytes,
          progress,
          message: progress === null
            ? "Загружаем AI-слои."
            : `Загружаем AI-слои · ${Math.round(progress * 100)}%`,
        });
      },
    }).then((payload) => {
      if (payload === null) {
        onPerceptionLoadChange?.({
          phase: "unavailable",
          receivedBytes: 0,
          totalBytes: null,
          progress: null,
          message: "Для этой сессии AI-слои не опубликованы.",
        });
        return;
      }
      if (
        abort.signal.aborted ||
        perceptionChannelRef.current !== active ||
        recordedIdentityRef.current !== identity ||
        !active.channel.ready
      ) {
        return;
      }
      active.channel.send_rrd(payload);
      loadedPerceptionChannelRef.current = active;
      onPerceptionLoadChange?.({
        phase: "ready",
        receivedBytes: payload.byteLength,
        totalBytes: payload.byteLength,
        progress: 1,
        message: "AI-слои готовы.",
      });
    }).catch((error: unknown) => {
      if (abort.signal.aborted) return;
      onPerceptionLoadChange?.({
        phase: "error",
        receivedBytes: 0,
        totalBytes: null,
        progress: null,
        message: error instanceof Error
          ? "AI-слои не загрузились. Можно повторить."
          : "AI-слои недоступны.",
      });
      // The base recording remains available when no admitted perception layer exists.
    });
    return () => abort.abort();
  }, [
    onPerceptionLoadChange,
    perceptionChannelRevision,
    recordedPerceptionRetryGeneration,
    recordedPerceptionUrl,
  ]);

  useEffect(() => {
    if (!recordedPointColorsUrl || !sceneSettings) return;
    const active = blueprintChannelRef.current;
    const identity = recordedIdentityRef.current;
    if (
      !active ||
      !identity ||
      !active.channel.ready
    ) return;
    const desiredKey = recordedPointColorKey(sceneSettings);
    if (
      appliedPointColorKeyRef.current === null &&
      desiredKey === RECORDED_BASE_POINT_COLOR_KEY
    ) {
      appliedPointColorKeyRef.current = desiredKey;
      onPointColorLoadChange?.({
        phase: "ready",
        receivedBytes: 0,
        totalBytes: 0,
        progress: 1,
        message: "Цвет записи готов.",
      });
      return;
    }
    if (appliedPointColorKeyRef.current === desiredKey) {
      onPointColorLoadChange?.({
        phase: "ready",
        receivedBytes: 0,
        totalBytes: null,
        progress: 1,
        message: "Окраска точек готова.",
      });
      return;
    }
    const abort = new AbortController();
    onPointColorLoadChange?.({
      phase: "loading",
      receivedBytes: 0,
      totalBytes: null,
      progress: null,
      message: "Готовим архивную окраску точек.",
    });
    void fetchRecordedPointColorsRrd(recordedPointColorsUrl, sceneSettings, identity, {
      origin: window.location.origin,
      signal: abort.signal,
    }).then((payload) => {
      if (
        abort.signal.aborted ||
        blueprintChannelRef.current !== active ||
        recordedIdentityRef.current !== identity ||
        !active.channel.ready
      ) {
        return;
      }
      active.channel.send_rrd(payload);
      appliedPointColorKeyRef.current = desiredKey;
      onPointColorLoadChange?.({
        phase: "ready",
        receivedBytes: payload.byteLength,
        totalBytes: payload.byteLength,
        progress: 1,
        message: "Окраска точек готова.",
      });
    }).catch(() => {
      if (abort.signal.aborted) return;
      onPointColorLoadChange?.({
        phase: "error",
        receivedBytes: 0,
        totalBytes: null,
        progress: null,
        message: "Окраска точек не загрузилась.",
      });
    });
    return () => abort.abort();
  }, [
    blueprintChannelRevision,
    onPointColorLoadChange,
    recordedPointColorsUrl,
    sceneSettings?.colorMode,
    sceneSettings?.customColor,
    sceneSettings?.palette,
  ]);

  useEffect(() => {
    if (!recordedBlueprintUrl || !sceneSettings) return;
    const active = blueprintChannelRef.current;
    const identity = recordedIdentityRef.current;
    if (
      !active ||
      !identity ||
      active.endpointUrl !== recordedBlueprintUrl ||
      !active.channel.ready
    ) return;
    const abort = new AbortController();
    void fetchRecordedBlueprintRrd(recordedBlueprintUrl, sceneSettings, identity, {
      origin: window.location.origin,
      blueprintSessionId: blueprintSessionIdRef.current,
      signal: abort.signal,
      activeView: recordedView,
      viewResetGeneration: recordedViewResetGeneration,
      followTrajectory: recordedFollowTrajectory,
      perceptionLayers: recordedPerceptionLayers,
    }).then((payload) => {
      if (
        abort.signal.aborted ||
        blueprintChannelRef.current !== active ||
        recordedIdentityRef.current !== identity ||
        !active.channel.ready
      ) {
        return;
      }
      active.channel.send_rrd(payload);
    }).catch(() => {
      // The recording remains usable with its embedded default blueprint.
      // A later settings change retries through the same small channel.
    });
    return () => abort.abort();
  }, [
    blueprintChannelRevision,
    recordedBlueprintUrl,
    recordedView,
    recordedViewResetGeneration,
    recordedFollowTrajectory,
    recordedPerceptionLayers.enabled,
    recordedPerceptionLayers.detections2d,
    recordedPerceptionLayers.segmentation,
    recordedPerceptionLayers.cuboids3d,
    sceneSettings?.accumulationSeconds,
    sceneSettings?.customColor,
    sceneSettings?.colorMode,
    sceneSettings?.palette,
    sceneSettings?.pointSize,
    sceneSettings?.showPoints,
    sceneSettings?.showTrajectory,
    sceneSettings?.showGrid,
  ]);

  return (
    <div className="rerun-viewport" data-status={presentationStatus}>
      <div
        ref={hostRef}
        className="rerun-viewport__canvas"
        data-presented={recordedArtifact === null || presentationStatus === "ready" ? "true" : "false"}
      />
      {lockPerceptionCameraInteraction && presentationStatus === "ready" ? (
        <div
          className="rerun-viewport__camera-lock"
          aria-hidden="true"
          title="Операторское видео зафиксировано"
        />
      ) : null}
      {presentationStatus === "loading" ? (
        <div className="rerun-viewport__notice" role="status">
          <span className="busy-indicator" aria-hidden="true" />
          <div>
            <strong>
              {recordedBlueprintUrl ? "Загружаем запись" : "Подключаем визуальный источник"}
            </strong>
            <span>
              {recordedBlueprintUrl
                ? recordingBufferProgress === null
                  ? "Запуск визуализатора и проверка архива."
                  : `Загрузка и декодирование · ${Math.round(recordingBufferProgress * 100)}%`
                : "Запуск встроенного визуализатора Rerun."}
            </span>
          </div>
        </div>
      ) : null}
      {presentationStatus === "error" ? (
        <div className="rerun-viewport__notice rerun-viewport__notice--error" role="alert">
          <div>
            <strong>Источник не открыт</strong>
            <span>Проверьте локальный поток Rerun или повторите подключение.</span>
            <button
              type="button"
              className="rerun-viewport__retry"
              onClick={() => setRetryNonce((nonce) => nonce + 1)}
            >
              Повторить подключение
            </button>
          </div>
        </div>
      ) : null}
    </div>
  );
}
