import {
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
  type ComponentType,
} from "react";
import {
  Button,
  GlassSurface,
  Icon,
  StatusBadge,
} from "@nodedc/ui-react";

import { MetricCard } from "../components/MetricCard";
import {
  ObservationMedia,
  ObservationSourcePicker,
  observationSourceStatusLabel,
} from "../components/ObservationSources";
import { ObservationTimeline } from "../components/ObservationTimeline";
import { FloatingObservationWindow } from "../components/FloatingObservationWindow";
import type { ObservationSessionReplayLaunch } from "../core/observation/sessionArchive";
import type { RecordedSessionAdmissionController } from "../core/observation/useRecordedSessionAdmission";
import type {
  RecordedAdmissionPhase,
  RecordedCameraAdmissionState,
} from "../core/observation/recordedSessionAdmission";
import type { ObservationLayoutController } from "../core/observation/useObservationLayout";
import type {
  DeviceModelDefinition,
  DevicePluginConnectionProps,
} from "../core/device-plugins/contracts";
import type {
  BackendStatus,
  MissionRuntimeState,
  ObservationSourceDescriptor,
} from "../core/runtime/contracts";
import {
  RerunViewport,
  isRecordedPlaybackPresentationReady,
  rerunPresentationStatus,
  type RerunPlaybackController,
  type RerunPlaybackState,
  type RerunSelection,
  type RerunViewportStatus,
  type RecordedPerceptionLoadState,
  type RecordedPointColorLoadState,
} from "../components/RerunViewport";
import {
  capabilityStatusLabel,
  type CapabilityStatus,
  type WorkspaceDefinition,
} from "../productModel";
import { finiteMetric, formatNumber, pipelineLatency, sourceModeLabel } from "../presentation";
import type { SceneSettings } from "../sceneSettings";
import { LidarQualityWorkspace } from "./LidarQualityWorkspace";
import { DatasetGatewayWorkspace } from "./DatasetGatewayWorkspace";

function statusTone(status: CapabilityStatus): "success" | "accent" | "warning" | "neutral" {
  if (status === "active") return "success";
  if (status === "ready") return "accent";
  if (status === "contract") return "warning";
  return "neutral";
}

function FeatureInventory({ definition }: { definition: WorkspaceDefinition }) {
  return (
    <div className="feature-inventory">
      {definition.groups.map((group) => (
        <section className="feature-group" key={group.title}>
          <header>
            <span className="section-eyebrow">ФУНКЦИОНАЛЬНЫЙ БЛОК</span>
            <h3>{group.title}</h3>
            <p>{group.description}</p>
          </header>
          <div className="feature-list">
            {group.capabilities.map((capability) => (
              <div className="feature-row" key={capability.label} data-status={capability.status}>
                <span className="feature-row__mark" aria-hidden="true" />
                <div>
                  <strong>{capability.label}</strong>
                  <p>{capability.description}</p>
                </div>
                <StatusBadge tone={statusTone(capability.status)}>
                  {capabilityStatusLabel[capability.status]}
                </StatusBadge>
              </div>
            ))}
          </div>
        </section>
      ))}
    </div>
  );
}

function WorkspaceLead({ definition, note }: { definition: WorkspaceDefinition; note?: string }) {
  return (
    <section className="workspace-lead workspace-lead--compact">
      <div>
        <span className="section-eyebrow">{definition.eyebrow}</span>
        <h2>{definition.title}</h2>
        <p>{definition.description}</p>
      </div>
      {note ? <span className="workspace-lead__note">{note}</span> : null}
    </section>
  );
}

export interface WorkspaceNavigation {
  openView: (viewId: string) => void;
  openSource: () => void;
  openDisplay: () => void;
  openLayers: () => void;
  activateAutomaticSpatialSource: () => void;
}

export interface WorkspaceRendererProps {
  definition: WorkspaceDefinition;
  state: MissionRuntimeState | null;
  backendStatus: BackendStatus;
  sourceUrl: string;
  recordedReplay: ObservationSessionReplayLaunch | null;
  recordedSessionAdmission: RecordedSessionAdmissionController | null;
  sceneSettings: SceneSettings;
  accumulationSeconds: number;
  onAccumulationChange: (value: number) => void;
  onAccumulationCommit: () => void;
  livePerceptionLayers: {
    detections2d: boolean;
    segmentation: boolean;
    cuboids3d: boolean;
  };
  onLivePerceptionLayersChange: (next: {
    detections2d: boolean;
    segmentation: boolean;
    cuboids3d: boolean;
  }) => void;
  observationLayout: ObservationLayoutController;
  deviceLabel: string | null;
  navigation: WorkspaceNavigation;
  spatialControls: {
    View: ComponentType<DevicePluginConnectionProps>;
    model: DeviceModelDefinition;
  } | null;
}

function OverviewWorkspace({
  definition,
  state,
  backendStatus,
  navigation,
}: WorkspaceRendererProps) {
  const streamActive = state?.sourceMode === "live" || state?.sourceMode === "replay";
  const metrics = streamActive ? state?.metrics : undefined;
  const latency = pipelineLatency(metrics);
  const frameRate = finiteMetric(metrics?.frameRateHz);
  const points = finiteMetric(metrics?.pointCount);
  const adapterOnline = backendStatus === "online";
  const rerunReady = Boolean(state?.spatialSource?.url);

  return (
    <div className="standard-workspace overview-workspace">
      <WorkspaceLead
        definition={definition}
        note="Числа появляются только из реального локального контура"
      />
      <section className="metrics-grid" aria-label="Оперативные показатели">
        <MetricCard
          featured
          eyebrow="ДО ПУБЛИКАЦИИ"
          value={formatNumber(latency)}
          unit="мс"
          detail="Вход адаптера → Scene Sink; без экрана"
        />
        <MetricCard
          eyebrow="ЧАСТОТА"
          value={formatNumber(frameRate)}
          unit="кадр/с"
          detail="Последнее измерение потока"
        />
        <MetricCard
          eyebrow="ТОЧЕК В КАДРЕ"
          value={points === null ? "—" : points.toLocaleString("ru-RU", { maximumFractionDigits: 0 })}
          detail="Без синтетического заполнения"
        />
        <MetricCard
          eyebrow="РЕЖИМ"
          value={sourceModeLabel(state?.sourceMode)}
          detail="Реальное время, повтор или ожидание"
        />
      </section>

      <div className="overview-grid">
        <GlassSurface className="contour-panel" padding="lg">
          <header className="panel-heading">
            <div>
              <span className="section-eyebrow">КРИТИЧЕСКИЙ ПУТЬ</span>
              <h2>Контур наблюдения</h2>
            </div>
            <StatusBadge tone={adapterOnline ? "success" : "danger"}>
              {adapterOnline ? "Контур доступен" : "Нет связи"}
            </StatusBadge>
          </header>
          <div className="pipeline-strip" aria-label="Путь данных">
            <div data-state={state?.activeDevice ? "ready" : "idle"}>
              <span>01</span>
              <strong>Устройство</strong>
              <small>{state?.activeDevice?.endpointLabel || state?.activeDevice?.displayName || "не назначено"}</small>
            </div>
            <Icon name="chevron-right" />
            <div data-state={streamActive ? "ready" : "idle"}>
              <span>02</span>
              <strong>Адаптер</strong>
              <small>{sourceModeLabel(state?.sourceMode)}</small>
            </div>
            <Icon name="chevron-right" />
            <div data-state={rerunReady ? "ready" : "idle"}>
              <span>03</span>
              <strong>Rerun</strong>
              <small>{rerunReady ? "gRPC опубликован" : "ожидает поток"}</small>
            </div>
            <Icon name="chevron-right" />
            <div data-state="ready">
              <span>04</span>
              <strong>Оператор</strong>
              <small>интерфейс готов</small>
            </div>
          </div>
        </GlassSurface>

        <GlassSurface className="next-actions-panel" padding="lg">
          <header className="panel-heading">
            <div>
              <span className="section-eyebrow">БЫСТРЫЙ ПЕРЕХОД</span>
              <h2>Продолжить работу</h2>
            </div>
          </header>
          <div className="action-list">
            <button type="button" onClick={() => navigation.openView("local-device")}>
              <span><Icon name="network" /></span>
              <div>
                <strong>Подключить устройство</strong>
                <small>Сценарий установленного плагина и запуск потока</small>
              </div>
              <Icon name="chevron-right" />
            </button>
            <button type="button" onClick={() => navigation.openView("spatial-scene")}>
              <span><Icon name="globe" /></span>
              <div>
                <strong>Открыть пространственную сцену</strong>
                <small>Облако точек, траектория и слои</small>
              </div>
              <Icon name="chevron-right" />
            </button>
            <button type="button" onClick={() => navigation.openView("streams")}>
              <span><Icon name="activity" /></span>
              <div>
                <strong>Проверить потоки</strong>
                <small>Транспорт, схемы и готовность</small>
              </div>
              <Icon name="chevron-right" />
            </button>
          </div>
        </GlassSurface>
      </div>

      <FeatureInventory definition={definition} />
    </div>
  );
}

function EmptySpatialStage({ settings }: { settings: SceneSettings }) {
  return (
    <div className="empty-spatial-stage" data-grid={settings.showGrid ? "true" : undefined}>
      <div className="empty-spatial-stage__grid" aria-hidden="true" />
      <div className="spatial-axis" aria-hidden="true">
        <span data-axis="x">X</span>
        <span data-axis="z">Z</span>
      </div>
      <div className="empty-spatial-stage__message">
        <span className="empty-spatial-stage__icon"><Icon name="globe" size={20} /></span>
        <strong>Визуальный источник не назначен</strong>
        <p>
          Область сцены не подставляет демонстрационные точки. Назначьте RRD или Rerun gRPC, чтобы
          открыть реальные данные.
        </p>
      </div>
    </div>
  );
}

function SpatialWorkspace({
  state,
  sourceUrl,
  recordedReplay,
  recordedSessionAdmission,
  sceneSettings,
  accumulationSeconds,
  onAccumulationChange,
  onAccumulationCommit,
  livePerceptionLayers,
  onLivePerceptionLayersChange,
  observationLayout,
  navigation,
  spatialControls,
}: WorkspaceRendererProps) {
  const [viewerStatus, setViewerStatus] = useState<RerunViewportStatus>(sourceUrl ? "loading" : "idle");
  const [viewerMessage, setViewerMessage] = useState("");
  const [selection, setSelection] = useState<RerunSelection | null>(null);
  const [playbackState, setPlaybackState] = useState<RerunPlaybackState | null>(null);
  const [playbackController, setPlaybackController] = useState<RerunPlaybackController | null>(null);
  const [recordedViewResetGeneration, setRecordedViewResetGeneration] = useState<0 | 1>(0);
  const [followRecordedTrajectory, setFollowRecordedTrajectory] = useState(false);
  const [perceptionLoad, setPerceptionLoad] = useState<RecordedPerceptionLoadState>({
    phase: "idle",
    receivedBytes: 0,
    totalBytes: null,
    progress: null,
    message: "",
  });
  const [perceptionRetryGeneration, setPerceptionRetryGeneration] = useState(0);
  const [pointColorLoad, setPointColorLoad] = useState<RecordedPointColorLoadState>({
    phase: "idle",
    receivedBytes: 0,
    totalBytes: null,
    progress: null,
    message: "",
  });
  const [showDetections2d, setShowDetections2d] = useState(false);
  const [showSegmentation, setShowSegmentation] = useState(false);
  const [showCuboids3d, setShowCuboids3d] = useState(false);
  const recordedSource = state?.sourceMode === "replay" || /\.rrd(?:$|[?#])/i.test(sourceUrl);
  const recordedSessionGate: RecordedAdmissionPhase = recordedSource
    ? recordedSessionAdmission?.phase ?? "loading"
    : "ready";
  const recordedPlaybackReady = !recordedSource ||
    (recordedSessionGate === "ready" &&
      isRecordedPlaybackPresentationReady(viewerStatus, playbackState));
  const streamActive = state?.sourceMode === "live" || state?.sourceMode === "replay";
  const metrics = streamActive ? state?.metrics : undefined;
  const latency = pipelineLatency(metrics);
  const frameRate = finiteMetric(metrics?.frameRateHz);
  const points = finiteMetric(metrics?.pointCount);
  const aiLatency = finiteMetric(metrics?.aiLatencyMs);
  const aiFrameRate = finiteMetric(metrics?.aiFrameRateHz);
  const observationSources = state?.observationSources ?? [];
  const pointCloudSource = observationSources.find((source) => source.modality === "point-cloud");
  const pointCloudVisible = pointCloudSource
    ? observationLayout.visibleSourceIds.has(pointCloudSource.id)
    : Boolean(sourceUrl.trim());
  const mediaSources = observationSources.filter(
    (source) => source.capabilities.overlay && source.modality !== "point-cloud",
  );
  const recordedPerceptionSupported =
    recordedSource && perceptionLoad.phase !== "unavailable";
  const recordedPerceptionReady = recordedSource && perceptionLoad.phase === "ready";
  const recordedPerceptionEnabled =
    showDetections2d || showSegmentation || showCuboids3d;
  // The native recorded camera remains the authoritative original. Only 2D
  // image-space overlays need Rerun's paired camera/world composition; 3D
  // cuboids are added directly to the stable spatial view.
  const unifiedPerception = recordedPerceptionSupported &&
    (showDetections2d || showSegmentation);
  const livePerceptionAvailable = !recordedSource && streamActive;
  const detections2dActive = recordedSource
    ? showDetections2d
    : livePerceptionLayers.detections2d;
  const segmentationActive = recordedSource
    ? showSegmentation
    : livePerceptionLayers.segmentation;
  const cuboids3dActive = recordedSource
    ? showCuboids3d
    : livePerceptionLayers.cuboids3d;
  const visibleMediaSources = mediaSources.filter((source) =>
    observationLayout.visibleSourceIds.has(source.id) &&
    !source.id.startsWith("recorded.perception."),
  );
  const initialRecordedPlaybackStartSeconds = recordedSource
    ? mediaSources.reduce<number | undefined>((earliest, source) => {
        if (
          source.id.startsWith("recorded.perception.") ||
          source.delivery?.kind !== "recorded-fmp4-manifest"
        ) return earliest;
        const start = source.delivery.timelineStartSeconds;
        return earliest === undefined ? start : Math.min(earliest, start);
      }, undefined)
    : undefined;
  const presentedMediaSourceCount = unifiedPerception ? 0 : visibleMediaSources.length;
  const pointCloudFocused = Boolean(
    pointCloudSource && observationLayout.focusedSourceId === pointCloudSource.id,
  );
  const floatingSourceMaximized = !unifiedPerception &&
    Boolean(observationLayout.maximizedFloatingSourceId);
  const timeline = state?.observationTimeline;
  const viewportRef = useRef<HTMLDivElement>(null);
  const intentionalSourceEnd = !recordedSource && [
    "awaiting_external_stop",
    "stopping",
    "finalizing",
    "completed",
  ].includes(state?.acquisition?.state ?? "");
  const presentedViewerStatus = intentionalSourceEnd
    ? "idle"
    : rerunPresentationStatus(
        viewerStatus,
        recordedSessionGate,
        recordedSource,
      );

  const onStatusChange = useCallback((status: RerunViewportStatus, message?: string) => {
    setViewerStatus(status);
    setViewerMessage(message ?? "");
    if (recordedSessionAdmission) {
      recordedSessionAdmission.reportSpatial(
        recordedSessionAdmission.key,
        status === "ready" ? "ready" : status === "error" ? "error" : "loading",
      );
    }
  }, [recordedSessionAdmission?.key, recordedSessionAdmission?.reportSpatial]);
  const onRecordedAdmissionChange = useCallback((
    sourceId: string,
    next: RecordedCameraAdmissionState,
  ) => {
    if (!recordedSessionAdmission) return;
    if (next.admissionKey !== recordedSessionAdmission.key) return;
    recordedSessionAdmission.reportCamera(recordedSessionAdmission.key, sourceId, next);
  }, [recordedSessionAdmission?.key, recordedSessionAdmission?.reportCamera]);
  const shouldPrepareRecordedSource = useCallback((sourceId: string) => {
    if (!recordedSessionAdmission) return false;
    return recordedSessionAdmission.activeCameraSourceIds.has(sourceId) ||
      recordedSessionAdmission.cameras[sourceId]?.phase === "ready";
  }, [recordedSessionAdmission]);
  const onSelectionChange = useCallback((next: RerunSelection | null) => setSelection(next), []);
  const onPlaybackChange = useCallback(
    (next: RerunPlaybackState | null) => setPlaybackState(next),
    [],
  );
  const onPlaybackControllerChange = useCallback(
    (next: RerunPlaybackController | null) => setPlaybackController(next),
    [],
  );
  const onPerceptionLoadChange = useCallback((next: RecordedPerceptionLoadState) => {
    setPerceptionLoad(next);
    if (next.phase === "unavailable" || next.phase === "error") {
      setShowDetections2d(false);
      setShowSegmentation(false);
      setShowCuboids3d(false);
    }
  }, []);
  const onPointColorLoadChange = useCallback((next: RecordedPointColorLoadState) => {
    setPointColorLoad(next);
  }, []);

  useEffect(() => {
    setPerceptionLoad({
      phase: recordedSource ? "loading" : "idle",
      receivedBytes: 0,
      totalBytes: null,
      progress: null,
      message: recordedSource ? "Ожидаем канал AI-слоёв." : "",
    });
    setPerceptionRetryGeneration(0);
    setShowDetections2d(false);
    setShowSegmentation(false);
    setShowCuboids3d(false);
    setRecordedViewResetGeneration(0);
    setFollowRecordedTrajectory(false);
    setPointColorLoad({
      phase: "idle",
      receivedBytes: 0,
      totalBytes: null,
      progress: null,
      message: "",
    });
  }, [recordedSource, sourceUrl]);

  useEffect(() => {
    if (pointCloudVisible && sourceUrl.trim()) return;
    setViewerStatus("idle");
    setViewerMessage("");
    setSelection(null);
    setPlaybackState(null);
    setPlaybackController(null);
    setPerceptionLoad({
      phase: "idle",
      receivedBytes: 0,
      totalBytes: null,
      progress: null,
      message: "",
    });
    setShowDetections2d(false);
    setShowSegmentation(false);
    setShowCuboids3d(false);
    setFollowRecordedTrajectory(false);
    setPointColorLoad({
      phase: "idle",
      receivedBytes: 0,
      totalBytes: null,
      progress: null,
      message: "",
    });
  }, [pointCloudVisible, sourceUrl]);

  useEffect(() => {
    const viewport = viewportRef.current;
    if (!viewport) return;
    const publishViewportSize = () => {
      const bounds = viewport.getBoundingClientRect();
      if (bounds.width < 1 || bounds.height < 1) return;
      observationLayout.setViewportSize({
        width: bounds.width,
        height: bounds.height,
      });
    };
    publishViewportSize();
    const observer = new ResizeObserver(publishViewportSize);
    observer.observe(viewport);
    return () => observer.disconnect();
  }, [observationLayout.setViewportSize]);

  const viewerStatusLabel = {
    idle: intentionalSourceEnd ? "Источник отключён" : "Источник не назначен",
    loading: "Подключение",
    ready: "Визуализатор готов",
    error: "Ошибка источника",
  }[presentedViewerStatus];

  const viewerStatusTone = presentedViewerStatus === "ready"
    ? "success"
    : presentedViewerStatus === "error"
      ? "danger"
      : "neutral";

  return (
    <div
      className="spatial-workspace"
      data-focused={pointCloudFocused || floatingSourceMaximized ? "true" : undefined}
    >
      <div className="spatial-toolbar" data-viewer-controls="v1">
        <div className="spatial-toolbar__actions">
          {recordedPerceptionSupported || livePerceptionAvailable ? (
            <div className="spatial-toolbar__view-switch" role="group" aria-label="Слои распознавания сцены">
              <Button
                size="compact"
                variant="primary"
                icon={<Icon name="video" />}
                aria-pressed="true"
                disabled
              >
                Оригинал
              </Button>
              <Button
                size="compact"
                variant={detections2dActive ? "primary" : "secondary"}
                icon={<Icon name="target" />}
                aria-pressed={detections2dActive}
                disabled={recordedSource && !recordedPerceptionReady}
                onClick={() => recordedSource
                  ? setShowDetections2d((current) => !current)
                  : onLivePerceptionLayersChange({
                      ...livePerceptionLayers,
                      detections2d: !livePerceptionLayers.detections2d,
                    })}
              >
                Объекты 2D
              </Button>
              <Button
                size="compact"
                variant={segmentationActive ? "primary" : "secondary"}
                icon={<Icon name="image" />}
                aria-pressed={segmentationActive}
                disabled={recordedSource && !recordedPerceptionReady}
                onClick={() => recordedSource
                  ? setShowSegmentation((current) => !current)
                  : onLivePerceptionLayersChange({
                      ...livePerceptionLayers,
                      segmentation: !livePerceptionLayers.segmentation,
                    })}
              >
                Сегментация
              </Button>
              <Button
                size="compact"
                variant={cuboids3dActive ? "primary" : "secondary"}
                icon={<Icon name="apps" />}
                aria-pressed={cuboids3dActive}
                disabled={recordedSource && !recordedPerceptionReady}
                onClick={() => recordedSource
                  ? setShowCuboids3d((current) => !current)
                  : onLivePerceptionLayersChange({
                      ...livePerceptionLayers,
                      cuboids3d: !livePerceptionLayers.cuboids3d,
                    })}
              >
                Кубы 3D
              </Button>
            </div>
          ) : null}
          {recordedSource && presentedViewerStatus === "ready" ? (
            <Button
              size="compact"
              variant={followRecordedTrajectory ? "primary" : "secondary"}
              icon={<Icon name="target" />}
              aria-pressed={followRecordedTrajectory}
              title="Удерживать orbital-пивот на риге до повторного нажатия"
              onClick={() => setFollowRecordedTrajectory((current) => !current)}
            >
              Следовать
            </Button>
          ) : null}
          {recordedSource && presentedViewerStatus === "ready" ? (
            <Button
              size="compact"
              variant="secondary"
              icon={<Icon name="refresh" />}
              onClick={() => setRecordedViewResetGeneration((current) => current === 0 ? 1 : 0)}
            >
              Сброс вида
            </Button>
          ) : null}
          <Button size="compact" variant="secondary" icon={<Icon name="network" />} onClick={navigation.openSource}>
            Движок
          </Button>
          <Button size="compact" variant="secondary" icon={<Icon name="list" />} onClick={navigation.openLayers}>
            Слои
          </Button>
          <Button size="compact" variant="secondary" icon={<Icon name="sliders" />} onClick={navigation.openDisplay}>
            Отображение
          </Button>
        </div>
      </div>

      <div
        ref={viewportRef}
        className="spatial-viewport-shell"
        data-primary-focused={pointCloudFocused ? "true" : undefined}
        data-media-maximized={floatingSourceMaximized ? "true" : undefined}
      >
          {sourceUrl.trim() && pointCloudVisible ? (
            <RerunViewport
              sourceUrl={sourceUrl}
              recordedArtifact={recordedSource ? recordedReplay : null}
              followLive={!recordedSource && state?.sourceMode === "live"}
              autoplayWhenReady={recordedSource}
              presentationGate={recordedSessionGate}
              expectedTimelineStartSeconds={recordedSource
                ? state?.observationTimeline?.range?.startSeconds
                : undefined}
              expectedTimelineEndSeconds={recordedSource
                ? state?.observationTimeline?.range?.endSeconds
                : undefined}
              initialPlaybackStartSeconds={initialRecordedPlaybackStartSeconds}
              sceneSettings={sceneSettings}
              recordedViewResetGeneration={recordedViewResetGeneration}
              recordedFollowTrajectory={followRecordedTrajectory}
              recordedPerceptionLayers={{
                enabled:
                  recordedPerceptionSupported &&
                  recordedPerceptionReady &&
                  recordedPerceptionEnabled,
                detections2d: showDetections2d,
                segmentation: showSegmentation,
                cuboids3d: showCuboids3d,
              }}
              recordedPerceptionRetryGeneration={perceptionRetryGeneration}
              lockPerceptionCameraInteraction={unifiedPerception}
              onPerceptionLoadChange={onPerceptionLoadChange}
              onPointColorLoadChange={onPointColorLoadChange}
              onStatusChange={onStatusChange}
              onSelectionChange={onSelectionChange}
              onPlaybackChange={onPlaybackChange}
              onPlaybackControllerChange={onPlaybackControllerChange}
            />
          ) : (
            <EmptySpatialStage settings={sceneSettings} />
          )}

          {spatialControls && !recordedSource ? (
            <div className="scene-device-controls">
              <spatialControls.View
                model={spatialControls.model}
                host={{
                  openSpatialScene: () => navigation.openView("spatial-scene"),
                  activateAutomaticSpatialSource: navigation.activateAutomaticSpatialSource,
                }}
              />
            </div>
          ) : null}

          {pointCloudFocused ? (
            <button
              type="button"
              className="scene-focus-exit"
              aria-label="Выйти из полноэкранного режима облака точек"
              onClick={() => observationLayout.setFocusedSourceId(null)}
            >
              <Icon name="minimize" size={16} />
            </button>
          ) : !floatingSourceMaximized ? (
            <div className="scene-source-controls">
              <ObservationSourcePicker
                sources={unifiedPerception
                  ? observationSources.filter((source) => source.modality === "point-cloud")
                  : observationSources}
                visibleSourceIds={observationLayout.visibleSourceIds}
                pendingSourceIds={observationLayout.pendingSourceIds}
                onToggle={observationLayout.toggleSource}
              />
              {pointCloudSource?.capabilities.fullscreen && pointCloudVisible && sourceUrl.trim() ? (
                <button
                  type="button"
                  className="scene-source-control"
                  aria-label="Развернуть облако точек"
                  onClick={() => observationLayout.setFocusedSourceId(pointCloudSource.id)}
                >
                  <Icon name="expand" size={16} />
                </button>
              ) : null}
            </div>
          ) : null}

          <div
            className="scene-status scene-status--top-left"
            aria-hidden={pointCloudFocused || floatingSourceMaximized}
          >
            <span className="section-eyebrow">ВИЗУАЛЬНЫЙ ДВИЖОК</span>
            <StatusBadge tone={viewerStatusTone}>{viewerStatusLabel}</StatusBadge>
            {!intentionalSourceEnd && viewerMessage ? <small>{viewerMessage}</small> : null}
          </div>

          <div
            className="scene-metrics"
            aria-label="Метрики пространственной сцены"
            aria-hidden={pointCloudFocused || floatingSourceMaximized}
          >
            <div>
              <span>КАДР/С</span>
              <strong>{formatNumber(frameRate)}</strong>
            </div>
            <div>
              <span>Точек</span>
              <strong>{points === null ? "—" : points.toLocaleString("ru-RU", { maximumFractionDigits: 0 })}</strong>
            </div>
            <div>
              <span>До публикации</span>
              <strong>{formatNumber(latency)}<small> мс</small></strong>
            </div>
            {streamActive ? (
              <div>
                <span>AI</span>
                <strong>
                  {aiLatency === null ? "—" : formatNumber(aiLatency)}
                  <small>{aiLatency === null ? "" : " мс"}</small>
                  {aiFrameRate === null ? null : <small> · {formatNumber(aiFrameRate)} Гц</small>}
                </strong>
              </div>
            ) : null}
          </div>

          {!pointCloudFocused && !floatingSourceMaximized && state?.sourceMode && state.sourceMode !== "idle" && !sourceUrl.trim() ? (
            <div className="scene-adapter-note">
              <Icon name="alert" />
              <span>
                Локальный поток <strong>{sourceModeLabel(state.sourceMode).toLocaleLowerCase("ru-RU")}</strong> активен,
                Rerun-мост запускается и опубликует адрес автоматически.
              </span>
            </div>
          ) : null}

          {!pointCloudFocused && !floatingSourceMaximized && selection ? (
            <div className="scene-selection">
              <span>Выбрано</span>
              <strong>{selection.entityPath}</strong>
              {selection.viewName ? <small>{selection.viewName}</small> : null}
            </div>
          ) : null}

          {!floatingSourceMaximized && recordedSource && (
            perceptionLoad.phase === "loading" ||
            perceptionLoad.phase === "error" ||
            pointColorLoad.phase === "loading" ||
            pointColorLoad.phase === "error"
          ) ? (
            <div className="scene-operation-status-stack">
              {perceptionLoad.phase === "loading" ? (
                <div className="scene-operation-status" role="status">
                  <span className="busy-indicator" aria-hidden="true" />
                  <span>{perceptionLoad.message || "Готовим AI-слои."}</span>
                </div>
              ) : null}
              {perceptionLoad.phase === "error" ? (
                <button
                  type="button"
                  className="scene-operation-status scene-operation-status--action"
                  onClick={() => setPerceptionRetryGeneration((generation) => generation + 1)}
                >
                  <Icon name="refresh" size={12} />
                  <span>Повторить AI</span>
                </button>
              ) : null}
              {pointColorLoad.phase === "loading" ? (
                <div className="scene-operation-status" role="status">
                  <span className="busy-indicator" aria-hidden="true" />
                  <span>{pointColorLoad.message}</span>
                </div>
              ) : null}
              {pointColorLoad.phase === "error" ? (
                <div className="scene-operation-status scene-operation-status--error" role="status">
                  <span>{pointColorLoad.message}</span>
                </div>
              ) : null}
            </div>
          ) : null}

          {presentedViewerStatus === "ready" && !floatingSourceMaximized ? (
            <div className="scene-navigation-hint" aria-label="Навигация по 3D-сцене">
              <span>Колесо · зум к курсору</span>
              <span>WASD · свободный проход</span>
            </div>
          ) : null}

          {!pointCloudFocused && !floatingSourceMaximized && recordedPlaybackReady ? (
            <ObservationTimeline
              active={presentedViewerStatus === "ready"}
              sourceCount={Math.max(1, (unifiedPerception ? 2 : 1) + presentedMediaSourceCount)}
              mode={recordedSource && playbackState?.rangeNs
                ? "recorded"
                : timeline?.mode}
              seekable={recordedSource && playbackState?.rangeNs
                ? true
                : timeline?.seekable}
              synchronization={timeline?.synchronization}
              rangeNs={playbackState?.rangeNs}
              currentNs={playbackState?.currentNs}
              playing={playbackState?.playing}
              onSeek={playbackController?.seek}
              onPlayingChange={playbackController?.setPlaying}
              onJumpToEnd={playbackController?.jumpToEnd}
              accumulationSeconds={accumulationSeconds}
              onAccumulationChange={onAccumulationChange}
              onAccumulationCommit={onAccumulationCommit}
              className="scene-timeline"
            />
          ) : null}

          {visibleMediaSources.map((source, index) => (
            <FloatingObservationWindow
              key={source.id}
              source={source}
              index={index}
              count={visibleMediaSources.length}
              boundsRef={viewportRef}
              rect={observationLayout.windowRects[source.id]}
              maximized={observationLayout.maximizedFloatingSourceId === source.id}
              active={observationLayout.activeFloatingSourceId === source.id}
              hidden={pointCloudFocused || unifiedPerception}
              onRectChange={(rect) => observationLayout.setWindowRect(source.id, rect)}
              onMaximizedChange={(maximized) =>
                observationLayout.setFloatingMaximized(source.id, maximized)}
              onActivate={() => observationLayout.activateFloatingSource(source.id)}
              playback={recordedSource && playbackState ? {
                currentSeconds: playbackState.currentNs / 1_000_000_000,
                playing: playbackState.playing,
              } : null}
              prepareRecorded={!recordedSource || shouldPrepareRecordedSource(source.id)}
              recordedSessionGate={recordedSessionGate}
              recordedAdmissionKey={recordedSessionAdmission?.key ?? null}
              onRecordedAdmissionChange={onRecordedAdmissionChange}
              onClose={() => {
                if (observationLayout.pendingSourceIds.has(source.id)) return;
                observationLayout.setFloatingMaximized(source.id, false);
                void observationLayout.hideSource(source.id);
              }}
            />
          ))}
          {recordedSource ? (
            <div className="recorded-session-preloaders" aria-hidden="true">
              {mediaSources.filter((source) => (
                source.delivery?.kind === "recorded-fmp4-manifest" &&
                !observationLayout.visibleSourceIds.has(source.id) &&
                shouldPrepareRecordedSource(source.id)
              )).map((source) => (
                <ObservationMedia
                  key={source.id}
                  source={source}
                  playback={playbackState ? {
                    currentSeconds: playbackState.currentNs / 1_000_000_000,
                    playing: false,
                  } : null}
                  prepareRecorded
                  recordedSessionGate="loading"
                  recordedAdmissionKey={recordedSessionAdmission?.key ?? null}
                  onRecordedAdmissionChange={onRecordedAdmissionChange}
                />
              ))}
            </div>
          ) : null}
      </div>

      <div className="spatial-contract-strip">
        <span><i data-state="ready" />Облако точек</span>
        <span><i data-state="ready" />Траектория</span>
        <span><i data-state="ready" />Преобразования</span>
        <span><i data-state="contract" />Камеры в 3D</span>
        <span><i data-state={detections2dActive ? "ready" : "contract"} />Объекты 2D</span>
        <span><i data-state={segmentationActive ? "ready" : "contract"} />Сегментация</span>
        <span><i data-state={cuboids3dActive ? "ready" : "contract"} />Кубы 3D</span>
        <span><i data-state="contract" />Компоновка</span>
      </div>
    </div>
  );
}

function CameraSourceCard({
  source,
  focused,
  visible,
  pending,
  selectedPeer,
  onToggle,
  onFocus,
  onClose,
  prepareRecorded,
  recordedSessionGate,
  recordedAdmissionKey,
  onRecordedAdmissionChange,
}: {
  source: ObservationSourceDescriptor;
  focused: boolean;
  visible: boolean;
  pending: boolean;
  selectedPeer: boolean;
  onToggle: () => void;
  onFocus: () => void;
  onClose: () => void;
  prepareRecorded: boolean;
  recordedSessionGate: RecordedAdmissionPhase;
  recordedAdmissionKey: string | null;
  onRecordedAdmissionChange: (
    sourceId: string,
    state: RecordedCameraAdmissionState,
  ) => void;
}) {
  const deliveryActive = Boolean(
    (source.delivery || source.previewUrl) &&
    (!source.activation || source.activation.selected),
  );
  const restartAvailable = Boolean(
    source.activation?.selected &&
    source.activation.controllable &&
    (source.availability === "error" || !source.delivery),
  );
  const activationLabel = pending
    ? "Переключение…"
    : restartAvailable
      ? "Повторить подключение"
      : source.activation?.selected && visible
      ? "Закрыть канал"
      : source.activation?.selected
        ? "Показать канал"
        : selectedPeer
          ? "Переключить"
          : "Открыть канал";
  const sourceDisconnected = Boolean(source.activation && !source.activation.selected && visible);

  return (
    <article className="camera-slot" data-focused={focused ? "true" : undefined}>
      <header>
        <span>{source.label}</span>
        <div className="camera-slot__head-actions">
          <span className="camera-slot__status">
            <i
              data-availability={sourceDisconnected ? "unverified" : source.availability}
              aria-hidden="true"
            />
            {sourceDisconnected ? "Источник отключён" : observationSourceStatusLabel(source)}
          </span>
          {source.activation && !focused ? (
            <button
              type="button"
              className="camera-slot__activation"
              disabled={pending || (!source.activation.selected && !source.activation.controllable)}
              onClick={onToggle}
            >
              {activationLabel}
            </button>
          ) : null}
          {source.capabilities.fullscreen && (focused || deliveryActive) ? (
            <button
              type="button"
              aria-label={focused ? `Свернуть ${source.label}` : `Развернуть ${source.label}`}
              onClick={onFocus}
            >
              <Icon name={focused ? "minimize" : "expand"} size={15} />
            </button>
          ) : null}
          {focused ? (
            <button
              type="button"
              aria-label={`Закрыть ${source.label}`}
              disabled={pending}
              onClick={onClose}
            >
              <Icon name="close" size={15} />
            </button>
          ) : null}
        </div>
      </header>
      <div className="camera-slot__body">
        <ObservationMedia
          source={source}
          prepareRecorded={prepareRecorded}
          recordedSessionGate={recordedSessionGate}
          recordedAdmissionKey={recordedAdmissionKey}
          onRecordedAdmissionChange={onRecordedAdmissionChange}
        />
      </div>
      <footer>
        <span>{source.description}</span>
        <small>{source.endpointLabel || source.transport}</small>
      </footer>
    </article>
  );
}

function CamerasWorkspace({
  definition,
  state,
  observationLayout,
  recordedReplay,
  recordedSessionAdmission,
}: WorkspaceRendererProps) {
  const sources = (state?.observationSources ?? []).filter(
    (source) => source.modality === "video" || source.modality === "image" || source.modality === "depth",
  );
  const focusedSource = sources.find((source) => source.id === observationLayout.focusedSourceId) ?? null;
  const timeline = state?.observationTimeline;
  const displayedSources = focusedSource ? [focusedSource] : sources;
  const recordedSessionGate: RecordedAdmissionPhase = recordedReplay
    ? recordedSessionAdmission?.phase ?? "loading"
    : "ready";
  const onRecordedAdmissionChange = useCallback((
    sourceId: string,
    next: RecordedCameraAdmissionState,
  ) => {
    if (!recordedSessionAdmission) return;
    if (next.admissionKey !== recordedSessionAdmission.key) return;
    recordedSessionAdmission.reportCamera(recordedSessionAdmission.key, sourceId, next);
  }, [recordedSessionAdmission?.key, recordedSessionAdmission?.reportCamera]);
  const shouldPrepareRecordedSource = useCallback((sourceId: string) => {
    if (!recordedReplay) return true;
    if (!recordedSessionAdmission) return false;
    return recordedSessionAdmission.activeCameraSourceIds.has(sourceId) ||
      recordedSessionAdmission.cameras[sourceId]?.phase === "ready";
  }, [recordedReplay, recordedSessionAdmission]);
  return (
    <div className="standard-workspace cameras-workspace" data-focused={focusedSource ? "true" : undefined}>
      <WorkspaceLead definition={definition} note="Кадры не подменяются демонстрационным видео" />
      <div className="camera-workspace__catalog">
        <span>{sources.length ? `${sources.length} визуальных каналов` : "Каталог источников пуст"}</span>
      </div>
      <div className="camera-grid" data-count={displayedSources.length}>
        {displayedSources.length ? displayedSources.map((source) => (
          <CameraSourceCard
            key={source.id}
            source={source}
            focused={focusedSource?.id === source.id}
            visible={observationLayout.visibleSourceIds.has(source.id)}
            pending={observationLayout.pendingSourceIds.has(source.id)}
            selectedPeer={Boolean(
              source.activation && sources.some((candidate) =>
                candidate.id !== source.id &&
                candidate.activation?.groupId === source.activation?.groupId &&
                candidate.activation?.selected === true),
            )}
            onToggle={() => void observationLayout.toggleSource(source.id)}
            onFocus={() => observationLayout.setFocusedSourceId(
              focusedSource?.id === source.id ? null : source.id,
            )}
            onClose={() => {
              observationLayout.setFocusedSourceId(null);
              void observationLayout.hideSource(source.id);
            }}
            prepareRecorded={shouldPrepareRecordedSource(source.id)}
            recordedSessionGate={recordedSessionGate}
            recordedAdmissionKey={recordedSessionAdmission?.key ?? null}
            onRecordedAdmissionChange={onRecordedAdmissionChange}
          />
        )) : (
          <div className="camera-grid__empty">
            <Icon name="video" size={22} />
            <strong>Камеры не объявлены</strong>
            <span>Выберите модель устройства — плагин опубликует доступные видеоканалы.</span>
          </div>
        )}
      </div>
      {recordedReplay ? (
        <div className="recorded-session-preloaders" aria-hidden="true">
          {sources.filter((source) => (
            !displayedSources.some(({ id }) => id === source.id) &&
            source.delivery?.kind === "recorded-fmp4-manifest" &&
            shouldPrepareRecordedSource(source.id)
          )).map((source) => (
            <ObservationMedia
              key={source.id}
              source={source}
              prepareRecorded
              recordedSessionGate="loading"
              recordedAdmissionKey={recordedSessionAdmission?.key ?? null}
              onRecordedAdmissionChange={onRecordedAdmissionChange}
            />
          ))}
        </div>
      ) : null}
      {!recordedReplay || recordedSessionGate === "ready" ? <ObservationTimeline
        active={sources.some((source) =>
          source.availability === "streaming" &&
          (!source.activation || source.activation.selected))}
        sourceCount={sources.filter((source) =>
          !source.activation || source.activation.selected).length}
        mode={timeline?.mode}
        seekable={timeline?.seekable}
        synchronization={timeline?.synchronization}
        className="camera-timeline"
      /> : null}
    </div>
  );
}

function MapWorkspace({ definition }: WorkspaceRendererProps) {
  const isMissionRoute = definition.id === "routes";
  return (
    <div className="standard-workspace map-workspace">
      <WorkspaceLead
        definition={definition}
        note={isMissionRoute ? "Схема интерфейса — координаты и командный канал не подключены" : "Картографический модуль не подключён"}
      />
      <div className="map-layout">
        <div className="map-canvas" data-route={isMissionRoute ? "true" : undefined}>
          <div className="map-canvas__grid" aria-hidden="true" />
          <div className="map-canvas__contours" aria-hidden="true">
            <span /><span /><span /><span />
          </div>
          <svg viewBox="0 0 1000 560" role="img" aria-label="Схематический черновик маршрута без географических координат">
            <path d="M165 420 C 235 350, 272 375, 340 292 S 480 235, 545 290 S 680 340, 765 210 S 845 125, 892 155" />
            <circle cx="165" cy="420" r="8" />
            <circle cx="340" cy="292" r="8" />
            <circle cx="545" cy="290" r="8" />
            <circle cx="765" cy="210" r="8" />
            <circle cx="892" cy="155" r="8" />
          </svg>
          <div className="map-point-label" style={{ left: "15%", top: "72%" }}>A · старт</div>
          <div className="map-point-label" style={{ left: "52%", top: "49%" }}>C · наблюдение</div>
          <div className="map-point-label" style={{ left: "84%", top: "22%" }}>E · финиш</div>
          <div className="map-canvas__notice">
            <StatusBadge tone="warning">Схема интерфейса</StatusBadge>
            <span>Не является картой местности и не содержит реальных координат.</span>
          </div>
        </div>
        <GlassSurface className="map-inspector" padding="lg">
          <span className="section-eyebrow">СЛОИ КАРТЫ</span>
          <h3>Рабочая композиция</h3>
          <div className="layer-summary-list">
            <div><span data-tone="cyan" /><strong>Маршрут</strong><small>5 точек · схема</small></div>
            <div><span data-tone="violet" /><strong>Геозоны</strong><small>не назначены</small></div>
            <div><span data-tone="green" /><strong>Высота</strong><small>контракт GridMap</small></div>
            <div><span data-tone="orange" /><strong>Проходимость</strong><small>обработчик не подключён</small></div>
          </div>
          <Button variant="secondary" width="full" disabled>Редактировать точки</Button>
        </GlassSurface>
      </div>
      <FeatureInventory definition={definition} />
    </div>
  );
}

function TimelineWorkspace({ definition, state }: WorkspaceRendererProps) {
  const rows = useMemo(() => [
    { label: "Устройство", value: state?.phase ? sourceModeLabel(state.sourceMode) : "нет событий", active: Boolean(state?.phase) },
    { label: "Поток точек", value: state?.sourceMode && state.sourceMode !== "idle" ? "активен" : "ожидание", active: state?.sourceMode !== "idle" && Boolean(state?.sourceMode) },
    { label: "Поза", value: state?.metrics?.pointCount ? "синхронно" : "нет данных", active: Boolean(state?.metrics?.pointCount) },
    { label: "Камеры", value: "каналы не назначены", active: false },
    { label: "События", value: state?.message || "нет событий", active: Boolean(state?.message) },
  ], [state]);

  return (
    <div className="standard-workspace timeline-workspace">
      <WorkspaceLead definition={definition} note="Отображаются только доступные состояния" />
      <GlassSurface className="timeline-panel" padding="lg">
        <header className="timeline-panel__head">
          <div>
            <span className="section-eyebrow">СИНХРОННЫЕ КАНАЛЫ</span>
            <h3>Текущий интервал</h3>
          </div>
          <div className="timeline-panel__controls">
            <Button size="compact" variant="secondary" disabled>Воспроизвести</Button>
            <code>—:—:—.———</code>
          </div>
        </header>
        <div className="timeline-ruler" aria-hidden="true">
          {Array.from({ length: 11 }, (_, index) => <span key={index}>{index * 10}</span>)}
        </div>
        <div className="timeline-lanes">
          {rows.map((row, index) => (
            <div className="timeline-lane" key={row.label} data-active={row.active ? "true" : undefined}>
              <strong>{row.label}</strong>
              <div><span style={{ left: row.active ? `${18 + index * 9}%` : "0" }} /></div>
              <small>{row.value}</small>
            </div>
          ))}
        </div>
      </GlassSurface>
      <FeatureInventory definition={definition} />
    </div>
  );
}

function MissionWorkspace({ definition }: WorkspaceRendererProps) {
  const steps = [
    { id: "01", label: "Аппарат", value: "Не назначен" },
    { id: "02", label: "Зона", value: "Не задана" },
    { id: "03", label: "Маршрут", value: "Черновик · 0 точек" },
    { id: "04", label: "Наблюдение", value: "Облако точек" },
    { id: "05", label: "Завершение", value: "Безопасная остановка" },
  ];
  return (
    <div className="standard-workspace mission-workspace">
      <WorkspaceLead definition={definition} note="Команды на физический аппарат отключены" />
      <div className="mission-layout">
        <GlassSurface className="mission-sequence" padding="lg">
          <header className="panel-heading">
            <div>
              <span className="section-eyebrow">МИССИЯ / ЧЕРНОВИК</span>
              <h2>Новая миссия</h2>
            </div>
            <StatusBadge tone="warning">Интерфейс готов</StatusBadge>
          </header>
          <div className="mission-steps">
            {steps.map((step) => (
              <button key={step.id} type="button" disabled>
                <span>{step.id}</span>
                <div><strong>{step.label}</strong><small>{step.value}</small></div>
                <Icon name="chevron-right" />
              </button>
            ))}
          </div>
        </GlassSurface>
        <GlassSurface className="mission-summary" padding="lg">
          <span className="section-eyebrow">ГОТОВНОСТЬ</span>
          <div className="mission-readiness"><strong>0</strong><span>/ 5 блоков</span></div>
          <p>Сохранение и отправка станут доступны после подключения исполнителя миссий и проверки безопасности.</p>
          <div className="mission-summary__checks">
            <span><i />Аппарат</span>
            <span><i />Геометрия</span>
            <span><i />Связь</span>
            <span><i />Безопасность</span>
          </div>
          <Button variant="primary" width="full" disabled>Сохранить миссию</Button>
        </GlassSurface>
      </div>
      <FeatureInventory definition={definition} />
    </div>
  );
}

function CatalogWorkspace({ definition }: WorkspaceRendererProps) {
  const total = definition.groups.reduce((count, group) => count + group.capabilities.length, 0);
  const active = definition.groups.reduce(
    (count, group) => count + group.capabilities.filter((capability) => capability.status === "active").length,
    0,
  );
  const ready = definition.groups.reduce(
    (count, group) => count + group.capabilities.filter((capability) => capability.status === "ready").length,
    0,
  );

  return (
    <div className="standard-workspace catalog-workspace">
      <WorkspaceLead definition={definition} note="Архитектурный и интерфейсный контракт" />
      <section className="capability-summary" aria-label="Готовность функционала">
        <div><span>Всего функций</span><strong>{total}</strong></div>
        <div><span>Работает сейчас</span><strong>{active}</strong></div>
        <div><span>Готово к данным</span><strong>{ready}</strong></div>
        <div><span>Без серверной логики</span><strong>{Math.max(0, total - active - ready)}</strong></div>
      </section>
      <FeatureInventory definition={definition} />
    </div>
  );
}

export function WorkspaceRenderer(props: WorkspaceRendererProps) {
  switch (props.definition.kind) {
    case "overview":
      return <OverviewWorkspace {...props} />;
    case "spatial":
      return <SpatialWorkspace {...props} />;
    case "cameras":
      return <CamerasWorkspace {...props} />;
    case "map":
      return <MapWorkspace {...props} />;
    case "timeline":
      return <TimelineWorkspace {...props} />;
    case "missions":
      return <MissionWorkspace {...props} />;
    case "catalog":
      return <CatalogWorkspace {...props} />;
    case "lidar-quality":
      return (
        <LidarQualityWorkspace
          deviceLabel={props.deviceLabel}
          onOpenObservation={() => props.navigation.openView("spatial-scene")}
        />
      );
    case "polygon-datasets":
      return <DatasetGatewayWorkspace />;
    case "device":
      return null;
  }
}
