import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
  AdminNavigationPanel,
  AppHeader,
  ApplicationPanel,
  ApplicationShell,
  Button,
  Checker,
  ColorField,
  ControlRow,
  HeaderAvatar,
  HeaderNavigation,
  HeaderProfile,
  HeaderProfileButton,
  HeaderWorkspace,
  Icon,
  Inspector,
  RangeControl,
  Select,
  StatusBadge,
  TextField,
  Window,
  WindowFooterActions,
  useApplicationWorkspace,
  type ApplicationPanelUtilityAction,
} from "@nodedc/ui-react";

import { LandingStage } from "./components/LandingStage";
import { ObservationSessionSelect } from "./components/ObservationSessionSelect";
import { useDevicePluginHost } from "./core/device-plugins/DevicePluginHost";
import { useMissionRuntime } from "./core/runtime/MissionRuntimeContext";
import type { ViewerSettings } from "./core/runtime/contracts";
import {
  SPATIAL_SOURCE_SWITCH_BLOCKED_REASON,
  isSpatialSourceSwitchBlocked,
} from "./core/runtime/acquisitionGuard";
import {
  createLatestAsyncCommitter,
  type LatestAsyncCommitter,
} from "./core/runtime/latestAsyncCommitter";
import { useObservationLayout } from "./core/observation/useObservationLayout";
import { recordedObservationSources } from "./core/observation/recordedObservationSources";
import type {
  ObservationSessionReplayLaunch,
  ObservationSessionSummary,
} from "./core/observation/sessionArchive";
import { useRecordedSessionAdmission } from "./core/observation/useRecordedSessionAdmission";
import { useWorkspaceLayoutProfile } from "./core/observation/useWorkspaceLayoutProfile";
import {
  OBSERVATION_WORKSPACE_ID,
  OBSERVATION_WORKSPACE_LAYOUT_VERSION,
  type ObservationWorkspaceLayoutProfile,
} from "./core/observation/workspaceLayout";
import {
  rootById,
  roots,
  workspaceById,
  workspacesForRoot,
  type RootId,
} from "./productModel";
import { backendLabel, phaseLabel, phaseTone } from "./presentation";
import {
  defaultSceneSettings,
  type PointColorMode,
  type PointPalette,
  type SceneSettings,
} from "./sceneSettings";
import { DeviceWorkspace } from "./workspaces/DeviceWorkspace";
import { WorkspaceRenderer } from "./workspaces/Workspaces";
import "./styles/scene-windows.css";

type SceneToolWindowId = "sources" | "display" | "layers";
const viewerSettingsQuietPeriodMs = 750;

const colorModeOptions: Array<{ value: PointColorMode; label: string; description: string }> = [
  { value: "intensity", label: "Интенсивность", description: "Значение отражённого сигнала" },
  { value: "height", label: "Высота Z", description: "Градиент по вертикальной координате" },
  { value: "distance", label: "Расстояние", description: "Дальность точки от сенсора" },
  { value: "rgb", label: "RGB", description: "Цвет, если он присутствует в данных" },
  { value: "class", label: "Класс", description: "Категория внешнего модуля восприятия" },
];

const paletteOptions: Array<{ value: PointPalette; label: string; description: string }> = [
  { value: "turbo", label: "Turbo", description: "Контрастный спектральный градиент" },
  { value: "viridis", label: "Viridis", description: "Равномерный перцептивный градиент" },
  { value: "plasma", label: "Plasma", description: "Тёплый контрастный градиент" },
  { value: "grayscale", label: "Серый", description: "Монохромное отображение" },
  { value: "custom", label: "Свой цвет", description: "Один назначенный цвет" },
];

interface LivePerceptionLayers {
  detections2d: boolean;
  segmentation: boolean;
  cuboids3d: boolean;
}

const defaultLivePerceptionLayers: LivePerceptionLayers = {
  detections2d: false,
  segmentation: false,
  cuboids3d: false,
};

function toViewerSettings(
  settings: SceneSettings,
  perception: LivePerceptionLayers = defaultLivePerceptionLayers,
): ViewerSettings {
  return {
    point_size: settings.pointSize,
    color_mode: settings.colorMode,
    palette: settings.palette,
    custom_color: settings.customColor,
    accumulation_seconds: settings.accumulationSeconds,
    show_points: settings.showPoints,
    show_trajectory: settings.showTrajectory,
    show_grid: settings.showGrid,
    show_detections_2d: perception.detections2d,
    show_segmentation: perception.segmentation,
    show_cuboids_3d: perception.cuboids3d,
  };
}

function mergeViewerSettings(
  current: SceneSettings,
  remote: ViewerSettings,
): SceneSettings {
  const next = {
    ...current,
    pointSize: remote.point_size,
    colorMode: remote.color_mode,
    palette: remote.palette,
    customColor: remote.custom_color,
    accumulationSeconds: remote.accumulation_seconds,
    showPoints: remote.show_points,
    showTrajectory: remote.show_trajectory,
    showGrid: remote.show_grid,
  };
  const unchanged =
    current.pointSize === next.pointSize &&
    current.colorMode === next.colorMode &&
    current.palette === next.palette &&
    current.customColor === next.customColor &&
    current.accumulationSeconds === next.accumulationSeconds &&
    current.showPoints === next.showPoints &&
    current.showTrajectory === next.showTrajectory &&
    current.showGrid === next.showGrid;
  return unchanged ? current : next;
}

export default function App() {
  const runtime = useMissionRuntime();
  const { selection } = useDevicePluginHost();
  const workspace = useApplicationWorkspace<string>({
    navigationOpen: false,
    contentExpanded: true,
  });

  const [activeRoot, setActiveRoot] = useState<RootId | null>(null);
  const [sourceUrl, setSourceUrl] = useState("");
  const [recordedReplay, setRecordedReplay] = useState<ObservationSessionReplayLaunch | null>(null);
  const [recordedReplayLabel, setRecordedReplayLabel] = useState<string | null>(null);
  const [replayTransitioning, setReplayTransitioning] = useState(false);
  const [sourceDraft, setSourceDraft] = useState("");
  const [sourceWindowOpen, setSourceWindowOpen] = useState(false);
  const [displayWindowOpen, setDisplayWindowOpen] = useState(false);
  const [layerInspectorOpen, setLayerInspectorOpen] = useState(false);
  const [sceneWindowOrder, setSceneWindowOrder] = useState<SceneToolWindowId[]>([]);
  const [layoutSaveNotice, setLayoutSaveNotice] = useState<string | null>(null);
  const [sceneSettings, setSceneSettings] = useState<SceneSettings>(defaultSceneSettings);
  const [displayDraft, setDisplayDraft] = useState<SceneSettings>(defaultSceneSettings);
  const [livePerceptionLayers, setLivePerceptionLayers] = useState<LivePerceptionLayers>(
    defaultLivePerceptionLayers,
  );
  const sourceSwitchBlocked = isSpatialSourceSwitchBlocked(runtime.state);
  const sourceSwitchBlockedReason = sourceSwitchBlocked
    ? SPATIAL_SOURCE_SWITCH_BLOCKED_REASON
    : null;
  const sourceSwitchBlockedRef = useRef(sourceSwitchBlocked);
  sourceSwitchBlockedRef.current = sourceSwitchBlocked;
  const appliedProfileKeyRef = useRef<string | null>(null);
  const sceneSettingsRef = useRef<SceneSettings>(defaultSceneSettings);
  const displayDraftRef = useRef<SceneSettings>(defaultSceneSettings);
  const confirmedSceneSettingsRef = useRef<SceneSettings>(defaultSceneSettings);
  const viewerSettingsCommitTimerRef = useRef<number | null>(null);
  const runtimeUpdateViewerSettingsRef = useRef(runtime.updateViewerSettings);
  const livePerceptionLayersRef = useRef<LivePerceptionLayers>(defaultLivePerceptionLayers);
  const livePerceptionRevisionRef = useRef(0);
  const replayActiveRef = useRef(false);
  const sceneSettingsCommitterActiveRef = useRef(true);
  const sceneSettingsCommitterRef = useRef<LatestAsyncCommitter<SceneSettings> | null>(null);

  const currentRoot = rootById(activeRoot);
  const activeDefinition = workspaceById(workspace.activeView);
  const spatialWorkspaceActive = Boolean(
    workspace.contentOpen && activeDefinition?.kind === "spatial",
  );
  const rootWorkspaces = workspacesForRoot(activeRoot);
  const activeSceneWindow = sceneWindowOrder[sceneWindowOrder.length - 1] ?? null;
  const automaticSourceUrl = runtime.state?.spatialSource?.url.trim() ?? "";
  const effectiveSourceUrl = replayTransitioning ? "" : sourceUrl || automaticSourceUrl;
  const replayActive = Boolean(recordedReplay && sourceUrl === recordedReplay.sourceUrl);
  const recordedSessionAdmission = useRecordedSessionAdmission(
    replayActive ? recordedReplay : null,
  );
  runtimeUpdateViewerSettingsRef.current = runtime.updateViewerSettings;
  replayActiveRef.current = replayActive;

  if (!sceneSettingsCommitterRef.current) {
    sceneSettingsCommitterRef.current = createLatestAsyncCommitter<SceneSettings>({
      commit: (settings) => replayActiveRef.current
        ? Promise.resolve(true)
        : runtimeUpdateViewerSettingsRef.current(
            toViewerSettings(settings, livePerceptionLayersRef.current),
          ),
      onSettled: ({ value, applied, superseded }) => {
        if (!sceneSettingsCommitterActiveRef.current) return;
        if (applied) confirmedSceneSettingsRef.current = value;
        if (superseded) return;

        const next = applied ? value : confirmedSceneSettingsRef.current;
        sceneSettingsRef.current = next;
        setSceneSettings(next);
        if (!applied && displayDraftRef.current === value) {
          displayDraftRef.current = next;
          setDisplayDraft(next);
        }
      },
    });
  }
  const replaySources = useMemo(
    () => recordedObservationSources(recordedReplay),
    [recordedReplay],
  );
  const activeObservationSources = replayActive
    ? replaySources
    : runtime.state?.observationSources ?? [];
  const activeRuntimeState = useMemo(() => {
    if (!replayActive || !recordedReplay) return runtime.state;
    return {
      ...(runtime.state ?? { phase: "replaying" as const }),
      phase: "replaying" as const,
      sourceMode: "replay" as const,
      spatialSource: {
        id: "recorded.spatial.primary",
        url: recordedReplay.sourceUrl,
        label: "Сохранённая пространственная сцена",
        kind: "rrd" as const,
      },
      observationSources: replaySources,
      observationTimeline: {
        mode: "recorded" as const,
        seekable: true,
        sessionRecording: true,
        synchronization: "host-arrival-best-effort" as const,
        range: {
          startSeconds: recordedReplay.timelineStartSeconds,
          endSeconds: recordedReplay.timelineEndSeconds,
        },
      },
    };
  }, [recordedReplay, replayActive, replaySources, runtime.state]);
  const viewerSettingsTargetIdentity = [
    runtime.state?.activeDevice?.pluginId,
    runtime.state?.activeDevice?.modelId,
    runtime.state?.activeDevice?.instanceId,
    runtime.state?.deviceSession?.sessionId,
    runtime.state?.deviceSession?.deviceId,
    runtime.state?.acquisition?.acquisitionId,
  ].filter(Boolean).join(":") || "local-runtime";
  const observationLayout = useObservationLayout(
    activeObservationSources,
    replayActive ? undefined : runtime.setObservationSourceActive,
  );
  const workspaceLayoutProfile = useWorkspaceLayoutProfile();
  const focusedObservationSource = activeObservationSources.find(
    (source) => source.id === observationLayout.focusedSourceId,
  );
  const observationFullscreenActive = Boolean(
    activeDefinition?.kind === "spatial"
      ? observationLayout.maximizedFloatingSourceId || focusedObservationSource?.modality === "point-cloud"
      : activeDefinition?.kind === "cameras" && focusedObservationSource,
  );

  useEffect(() => {
    const remote = runtime.state?.viewerSettings;
    if (
      !remote ||
      workspaceLayoutProfile.profile ||
      sceneSettingsCommitterRef.current?.isBusy()
    ) return;
    const merged = mergeViewerSettings(sceneSettingsRef.current, remote);
    const remoteLayers = {
      detections2d: remote.show_detections_2d ?? false,
      segmentation: remote.show_segmentation ?? false,
      cuboids3d: remote.show_cuboids_3d ?? false,
    };
    livePerceptionLayersRef.current = remoteLayers;
    setLivePerceptionLayers(remoteLayers);
    sceneSettingsRef.current = merged;
    confirmedSceneSettingsRef.current = merged;
    setSceneSettings(merged);
    if (viewerSettingsCommitTimerRef.current === null) {
      displayDraftRef.current = merged;
      setDisplayDraft(merged);
    }
  }, [runtime.state?.viewerSettings, workspaceLayoutProfile.profile]);

  useEffect(() => {
    const profile = workspaceLayoutProfile.profile;
    if (!profile) return;
    if (viewerSettingsCommitTimerRef.current !== null) {
      window.clearTimeout(viewerSettingsCommitTimerRef.current);
      viewerSettingsCommitTimerRef.current = null;
    }
    sceneSettingsRef.current = profile.sceneSettings;
    displayDraftRef.current = profile.sceneSettings;
    confirmedSceneSettingsRef.current = profile.sceneSettings;
    setSceneSettings(profile.sceneSettings);
    setDisplayDraft(profile.sceneSettings);
    observationLayout.restore(profile);
  }, [observationLayout.restore, workspaceLayoutProfile.profile]);

  useEffect(() => {
    if (spatialWorkspaceActive) return;
    // Scene tools are transient children of the spatial workspace. They must
    // never survive a route/root/content close or appear over the landing page.
    setSourceWindowOpen(false);
    setDisplayWindowOpen(false);
    setLayerInspectorOpen(false);
    setSceneWindowOrder([]);
  }, [spatialWorkspaceActive]);

  useEffect(() => {
    sceneSettingsCommitterActiveRef.current = true;
    return () => {
      sceneSettingsCommitterActiveRef.current = false;
      if (viewerSettingsCommitTimerRef.current !== null) {
        window.clearTimeout(viewerSettingsCommitTimerRef.current);
        viewerSettingsCommitTimerRef.current = null;
      }
    };
  }, []);

  useEffect(() => {
    const profile = workspaceLayoutProfile.profile;
    const applicationKey = profile
      ? `${profile.revision}:${viewerSettingsTargetIdentity}`
      : null;
    if (
      !profile ||
      runtime.backendStatus !== "online" ||
      !applicationKey ||
      appliedProfileKeyRef.current === applicationKey
    ) {
      return;
    }
    appliedProfileKeyRef.current = applicationKey;
    void runtime.updateViewerSettings(
      toViewerSettings(profile.sceneSettings, livePerceptionLayersRef.current),
    ).then((applied) => {
      if (!applied && appliedProfileKeyRef.current === applicationKey) {
        appliedProfileKeyRef.current = null;
      }
    });
  }, [
    runtime.backendStatus,
    runtime.updateViewerSettings,
    viewerSettingsTargetIdentity,
    workspaceLayoutProfile.profile,
  ]);

  const activateSceneWindow = useCallback((windowId: SceneToolWindowId) => {
    setSceneWindowOrder((current) => [
      ...current.filter((candidate) => candidate !== windowId),
      windowId,
    ]);
  }, []);

  const closeSceneWindow = useCallback((windowId: SceneToolWindowId) => {
    if (windowId === "sources") setSourceWindowOpen(false);
    if (windowId === "display") setDisplayWindowOpen(false);
    if (windowId === "layers") setLayerInspectorOpen(false);
    setSceneWindowOrder((current) => current.filter((candidate) => candidate !== windowId));
  }, []);

  useEffect(() => {
    if (!spatialWorkspaceActive || !activeSceneWindow) return;

    const closeActiveWindow = (event: KeyboardEvent) => {
      if (event.key !== "Escape" || event.defaultPrevented) return;
      if (document.querySelector(".nodedc-dropdown-surface")) return;
      if (document.querySelector('.nodedc-overlay[data-placement="center"]')) return;
      event.preventDefault();
      closeSceneWindow(activeSceneWindow);
    };

    document.addEventListener("keydown", closeActiveWindow);
    return () => document.removeEventListener("keydown", closeActiveWindow);
  }, [activeSceneWindow, closeSceneWindow, spatialWorkspaceActive]);

  const selectRoot = (rootId: RootId) => {
    setActiveRoot(rootId);
    workspace.closeView();
    workspace.openNavigation();
  };

  const openView = (viewId: string) => {
    const definition = workspaceById(viewId);
    if (!definition) return;
    setActiveRoot(definition.root);
    workspace.openView(viewId);
  };

  const openSource = () => {
    if (!spatialWorkspaceActive) return;
    setSourceDraft(sourceUrl);
    setSourceWindowOpen(true);
    activateSceneWindow("sources");
  };

  const commitDisplaySettings = useCallback((next: SceneSettings) => {
    if (viewerSettingsCommitTimerRef.current !== null) {
      window.clearTimeout(viewerSettingsCommitTimerRef.current);
      viewerSettingsCommitTimerRef.current = null;
    }
    displayDraftRef.current = next;
    setDisplayDraft(next);
    sceneSettingsCommitterRef.current?.enqueue(next);
  }, []);

  const stageDisplayPatch = useCallback((patch: Partial<SceneSettings>) => {
    const next = { ...displayDraftRef.current, ...patch };
    displayDraftRef.current = next;
    setDisplayDraft(next);
    if (viewerSettingsCommitTimerRef.current !== null) {
      window.clearTimeout(viewerSettingsCommitTimerRef.current);
    }
    viewerSettingsCommitTimerRef.current = window.setTimeout(() => {
      viewerSettingsCommitTimerRef.current = null;
      sceneSettingsCommitterRef.current?.enqueue(displayDraftRef.current);
    }, viewerSettingsQuietPeriodMs);
  }, []);

  const flushDisplaySettings = useCallback(() => {
    if (viewerSettingsCommitTimerRef.current === null) return;
    window.clearTimeout(viewerSettingsCommitTimerRef.current);
    viewerSettingsCommitTimerRef.current = null;
    sceneSettingsCommitterRef.current?.enqueue(displayDraftRef.current);
  }, []);

  const commitDisplayPatch = useCallback((patch: Partial<SceneSettings>) => {
    commitDisplaySettings({ ...displayDraftRef.current, ...patch });
  }, [commitDisplaySettings]);

  const openDisplay = () => {
    if (!spatialWorkspaceActive) return;
    setDisplayWindowOpen(true);
    activateSceneWindow("display");
  };

  const openLayers = () => {
    if (!spatialWorkspaceActive) return;
    setLayerInspectorOpen(true);
    activateSceneWindow("layers");
  };

  const changeLivePerceptionLayers = useCallback((next: LivePerceptionLayers) => {
    const previous = livePerceptionLayersRef.current;
    const revision = ++livePerceptionRevisionRef.current;
    livePerceptionLayersRef.current = next;
    setLivePerceptionLayers(next);
    if (replayActiveRef.current) return;
    void runtimeUpdateViewerSettingsRef.current(
      toViewerSettings(sceneSettingsRef.current, next),
    ).then((applied) => {
      if (applied || livePerceptionRevisionRef.current !== revision) return;
      livePerceptionLayersRef.current = previous;
      setLivePerceptionLayers(previous);
    });
  }, []);

  const beginRecordedReplaySwitch = useCallback(async () => {
    if (sourceSwitchBlockedRef.current) {
      throw new Error(SPATIAL_SOURCE_SWITCH_BLOCKED_REASON);
    }
    // useObservationSessions calls this only after backend preparation has
    // produced a validated launch descriptor. Keep the old scene mounted
    // before this point; now perform one controlled receiver teardown before
    // accepting the already-ready archive.
    setReplayTransitioning(true);
    setRecordedReplay(null);
    setRecordedReplayLabel(null);
    setSourceUrl("");
    setSourceDraft("");
    await new Promise<void>((resolve) => {
      window.requestAnimationFrame(() => window.setTimeout(resolve, 0));
    });
  }, []);

  const acceptRecordedReplay = useCallback((
    session: ObservationSessionSummary,
    launch: ObservationSessionReplayLaunch,
  ) => {
    if (sourceSwitchBlockedRef.current) {
      throw new Error(SPATIAL_SOURCE_SWITCH_BLOCKED_REASON);
    }
    setRecordedReplay(launch);
    setRecordedReplayLabel(session.label);
    setSourceUrl(launch.sourceUrl);
    setSourceDraft(launch.sourceUrl);
    setReplayTransitioning(false);
  }, []);

  const settleRecordedReplaySwitch = useCallback((outcome: "accepted" | "error" | "cancelled") => {
    if (outcome !== "accepted") setReplayTransitioning(false);
  }, []);

  const activateAutomaticSpatialSource = useCallback(() => {
    // A plugin-owned live/file-replay start must release any host-selected
    // archive or manual URL before the new acquisition becomes non-terminal.
    // This transition is an internal start boundary, not an operator source
    // switch, so it intentionally does not consult the acquisition guard.
    setReplayTransitioning(false);
    setRecordedReplay(null);
    setRecordedReplayLabel(null);
    setSourceUrl("");
    setSourceDraft("");
  }, []);

  useEffect(() => {
    // ObservationSessionSelect owns the cancellable request and unmounts when
    // the operator leaves the spatial workspace. Its unmount cannot safely
    // call back into this owner, so clear the transient blanking state here.
    // Returning to Observation then starts from a deterministic idle source
    // instead of an orphaned `replayTransitioning=true` state.
    if (activeDefinition?.kind !== "spatial") setReplayTransitioning(false);
  }, [activeDefinition?.kind]);

  const applyScenePatch = (patch: Partial<SceneSettings>) => commitDisplayPatch(patch);

  const saveWorkspaceLayout = useCallback(async () => {
    flushDisplaySettings();
    await sceneSettingsCommitterRef.current?.waitForIdle();

    const layout = observationLayout.snapshot();
    if (!layout) {
      setLayoutSaveNotice("Сцена ещё не измерила рабочую область.");
      return;
    }
    const draft: ObservationWorkspaceLayoutProfile = {
      version: OBSERVATION_WORKSPACE_LAYOUT_VERSION,
      revision: workspaceLayoutProfile.profile?.revision ?? 0,
      workspaceId: OBSERVATION_WORKSPACE_ID,
      sceneSettings: sceneSettingsRef.current,
      ...layout,
    };
    const saved = await workspaceLayoutProfile.save(draft);
    setLayoutSaveNotice(saved ? "Компоновка сохранена" : workspaceLayoutProfile.error);
  }, [
    flushDisplaySettings,
    observationLayout.snapshot,
    workspaceLayoutProfile,
  ]);

  useEffect(() => {
    if (!layoutSaveNotice || layoutSaveNotice !== "Компоновка сохранена") return;
    const timer = window.setTimeout(() => setLayoutSaveNotice(null), 2600);
    return () => window.clearTimeout(timer);
  }, [layoutSaveNotice]);

  const contentActions = useMemo<ApplicationPanelUtilityAction[]>(() => {
    const actions: ApplicationPanelUtilityAction[] = [];

    if (activeDefinition?.kind === "device") {
      actions.push({
        label: "Обновить состояние локального контура",
        icon: "refresh",
        onClick: () => void runtime.refresh(),
      });
    }

    if (activeDefinition?.kind === "spatial") {
      actions.push(
        {
          label: workspaceLayoutProfile.state === "saving"
            ? "Сохраняем компоновку"
            : "Сохранить компоновку",
          icon: "save",
          disabled: workspaceLayoutProfile.state === "saving",
          onClick: () => void saveWorkspaceLayout(),
        },
      );
    }

    return actions;
  }, [activeDefinition?.kind, runtime, saveWorkspaceLayout, workspaceLayoutProfile.state]);

  const header = (
    <AppHeader
      brand={<img src="/nodedc-logo.svg" alt="NODE.DC" />}
      brandLabel="NODEDC MISSION CORE"
      center={
        <>
          <HeaderWorkspace kind="mark" label="Mission Core" imageUrl="/nodedc-mark.svg" />
          <HeaderNavigation
            label="Архитектурные блоки пункта управления"
            value={activeRoot ?? undefined}
            items={roots.map((root) => ({ value: root.id, label: root.label }))}
            onChange={selectRoot}
          />
        </>
      }
      right={
        <HeaderProfile>
          <HeaderProfileButton onClick={() => void runtime.refresh()} title="Обновить локальный контур">
            <span className="api-dot" data-status={runtime.backendStatus} aria-hidden="true" />
            {backendLabel(runtime.backendStatus)}
          </HeaderProfileButton>
          <HeaderAvatar label="DC" />
        </HeaderProfile>
      }
    />
  );

  return (
    <>
      <ApplicationShell
        data-nodedc-ui
        className="control-station"
        data-observation-fullscreen={observationFullscreenActive ? "true" : undefined}
        navigationOpen={workspace.navigationOpen && activeRoot !== null}
        contentOpen={workspace.contentOpen && activeDefinition !== null}
        contentExpanded={workspace.contentExpanded}
        header={header}
        stage={
          <LandingStage
            root={currentRoot}
            backendStatus={runtime.backendStatus}
            phase={runtime.state?.phase}
            message={runtime.state?.message}
            onOpenObservation={() => openView("spatial-scene")}
            onOpenDevice={() => openView("local-device")}
          />
        }
        navigation={currentRoot ? (
          <AdminNavigationPanel
            eyebrow="MISSION CORE"
            title={currentRoot.title}
            closeLabel={`Закрыть раздел «${currentRoot.label}»`}
            navigationLabel={`Рабочие поверхности раздела «${currentRoot.label}»`}
            onClose={workspace.closeNavigation}
            contexts={[
              {
                id: "local-contour",
                label: "Локальный контур",
                description:
                  runtime.state?.activeDevice?.endpointLabel ||
                  selection?.model.displayName ||
                  "Модель не выбрана",
                icon: <Icon name="network" />,
                active: runtime.backendStatus !== "offline" && runtime.backendStatus !== "unconfigured",
              },
            ]}
            items={rootWorkspaces.map((item) => ({
              id: item.id,
              label: item.label,
              icon: <Icon name={item.icon} />,
            }))}
            activeId={workspace.activeView ?? undefined}
            onItemChange={openView}
            footer={
              <>
                <span className="nodedc-admin-panel__nav-icon" aria-hidden="true">
                  <Icon name="activity" />
                </span>
                <span>{rootWorkspaces.length} рабочих поверхностей</span>
              </>
            }
          />
        ) : null}
        content={activeDefinition ? (
          <ApplicationPanel
            key={activeDefinition.id}
            eyebrow={activeDefinition.eyebrow}
            title={
              activeDefinition.kind === "spatial" && replayActive && recordedReplayLabel
                ? `${activeDefinition.title}: ${recordedReplayLabel}`
                : activeDefinition.title
            }
            description={activeDefinition.description}
            expanded={workspace.contentExpanded}
            onExpandedChange={workspace.setContentExpanded}
            headerTools={
              activeDefinition.kind === "device" ? (
                <StatusBadge tone={phaseTone(runtime.state?.phase)}>
                  {phaseLabel(runtime.state?.phase)}
                </StatusBadge>
              ) : activeDefinition.kind === "spatial" ? (
                <div className="observation-header-tools">
                  <ObservationSessionSelect
                    disabled={runtime.pendingAction !== null || sourceSwitchBlocked}
                    blockedReason={sourceSwitchBlockedReason}
                    onReplayBegin={beginRecordedReplaySwitch}
                    onReplayAccepted={acceptRecordedReplay}
                    onReplaySettled={(_session, outcome) => settleRecordedReplaySwitch(outcome)}
                  />
                  {layoutSaveNotice || workspaceLayoutProfile.error ? (
                    <span
                      className="workspace-layout-feedback"
                      data-error={workspaceLayoutProfile.error ? "true" : undefined}
                      role={workspaceLayoutProfile.error ? "alert" : "status"}
                    >
                      <i
                        className="api-dot"
                        data-status={workspaceLayoutProfile.error ? "error" : "online"}
                        aria-hidden="true"
                      />
                      {layoutSaveNotice || workspaceLayoutProfile.error}
                    </span>
                  ) : null}
                </div>
              ) : (
                <StatusBadge tone="warning">Интерфейс готов</StatusBadge>
              )
            }
            utilityActions={contentActions}
            onClose={workspace.closeView}
          >
            {activeDefinition.kind === "device" ? (
              <DeviceWorkspace
                onOpenSpatialScene={() => openView("spatial-scene")}
                onActivateAutomaticSpatialSource={activateAutomaticSpatialSource}
              />
            ) : (
              <WorkspaceRenderer
                definition={activeDefinition}
                state={activeRuntimeState}
                backendStatus={runtime.backendStatus}
                sourceUrl={effectiveSourceUrl}
                recordedReplay={replayActive ? recordedReplay : null}
                recordedSessionAdmission={recordedSessionAdmission}
                sceneSettings={sceneSettings}
                accumulationSeconds={displayDraft.accumulationSeconds}
                onAccumulationChange={(accumulationSeconds) =>
                  stageDisplayPatch({ accumulationSeconds })}
                onAccumulationCommit={flushDisplaySettings}
                livePerceptionLayers={livePerceptionLayers}
                onLivePerceptionLayersChange={changeLivePerceptionLayers}
                observationLayout={observationLayout}
                spatialControls={selection?.SpatialControlsView
                  ? {
                      View: selection.SpatialControlsView,
                      model: selection.model,
                    }
                  : null}
                navigation={{
                  openView,
                  openSource,
                  openDisplay,
                  openLayers,
                  activateAutomaticSpatialSource,
                }}
              />
            )}
          </ApplicationPanel>
        ) : null}
      />

      <Window
        open={spatialWorkspaceActive && sourceWindowOpen}
        title="Визуальный движок"
        subtitle="Rerun gRPC и записи пространственной сцены"
        placement="end"
        draggable
        closeOnBackdrop={false}
        closeOnEscape={false}
        lockBodyScroll={false}
        trapFocus={false}
        className="scene-tool-window scene-tool-window--sources"
        data-scene-window="sources"
        data-scene-active={activeSceneWindow === "sources" ? "true" : undefined}
        onPointerDown={() => activateSceneWindow("sources")}
        onClose={() => closeSceneWindow("sources")}
        footer={
          <WindowFooterActions>
            <Button
              variant="ghost"
              disabled={sourceSwitchBlocked}
              title={sourceSwitchBlockedReason ?? undefined}
              onClick={() => {
                if (sourceSwitchBlockedRef.current) return;
                setSourceDraft("");
                setSourceUrl("");
                setRecordedReplay(null);
                setRecordedReplayLabel(null);
              }}
            >
              Сбросить адрес
            </Button>
            <Button
              variant="primary"
              shape="pill"
              disabled={sourceSwitchBlocked || !sourceDraft.trim()}
              title={sourceSwitchBlockedReason ?? undefined}
              onClick={() => {
                if (sourceSwitchBlockedRef.current) return;
                setSourceUrl(sourceDraft.trim());
                setRecordedReplay(null);
                setRecordedReplayLabel(null);
              }}
            >
              Применить адрес
            </Button>
          </WindowFooterActions>
        }
      >
        <Inspector
          defaultOpen={["connection"]}
          singleOpen
          sections={[
            {
              id: "connection",
              label: "Подключение",
              description: "Адрес потока или записи",
              content: (
                <div className="inspector-control-stack">
                  <ControlRow label="Состояние">
                    <span className="scene-window-state">
                      <i className="api-dot" data-status={effectiveSourceUrl ? "online" : "checking"} aria-hidden="true" />
                      {effectiveSourceUrl ? "Источник назначен" : "Источник не назначен"}
                    </span>
                  </ControlRow>
                  {effectiveSourceUrl ? (
                    <ControlRow label="Активный адрес" layout="stack">
                      <code className="scene-window-code">{effectiveSourceUrl}</code>
                    </ControlRow>
                  ) : null}
                  <ControlRow label="Автоматический" layout="stack">
                    <code className="scene-window-code">
                      {automaticSourceUrl || "Локальный источник ещё не опубликован"}
                    </code>
                  </ControlRow>
                  <TextField
                    label="Ручной адрес"
                    hint="необязательно"
                    value={sourceDraft}
                    onChange={(event) => setSourceDraft(event.target.value)}
                    disabled={sourceSwitchBlocked}
                    spellCheck={false}
                    placeholder="rerun+http://127.0.0.1:9876/proxy"
                    description={sourceSwitchBlockedReason ?? "Пустое значение использует автоматический локальный источник. Ручной адрес нужен для другого gRPC-потока или записи RRD."}
                  />
                </div>
              ),
            },
            {
              id: "formats",
              label: "Поддерживаемые адреса",
              description: "Rerun gRPC и RRD",
              content: (
                <div className="inspector-control-stack">
                  <ControlRow label="Живой поток" layout="stack">
                    <code className="scene-window-code">rerun+http://…/proxy</code>
                  </ControlRow>
                  <ControlRow label="Локальная запись" layout="stack">
                    <code className="scene-window-code">http://…/recording.rrd</code>
                  </ControlRow>
                  <ControlRow label="Удалённая запись" layout="stack">
                    <code className="scene-window-code">https://…/recording.rrd</code>
                  </ControlRow>
                </div>
              ),
            },
            {
              id: "adapter",
              label: "Локальный адаптер",
              description: "Транспорт устройства",
              content: (
                <div className="inspector-control-stack">
                  <ControlRow label="Контур">
                    <span className="scene-window-state">
                      <i className="api-dot" data-status={runtime.backendStatus} aria-hidden="true" />
                      {backendLabel(runtime.backendStatus)}
                    </span>
                  </ControlRow>
                  <p className="scene-window-note">
                    Встроенный адаптер публикует локальный Rerun gRPC автоматически после запуска
                    живого приёма или повтора записи. Ручной адрес для этого не требуется.
                  </p>
                </div>
              ),
            },
          ]}
        />
      </Window>

      <Window
        open={spatialWorkspaceActive && displayWindowOpen}
        title="Отображение"
        subtitle="Параметры пространственной сцены"
        placement="end"
        draggable
        closeOnBackdrop={false}
        closeOnEscape={false}
        lockBodyScroll={false}
        trapFocus={false}
        className="scene-tool-window scene-tool-window--display"
        data-scene-window="display"
        data-scene-active={activeSceneWindow === "display" ? "true" : undefined}
        onPointerDown={() => activateSceneWindow("display")}
        onClose={() => closeSceneWindow("display")}
      >
        <Inspector
          defaultOpen={["points"]}
          singleOpen
          sections={[
            {
              id: "points",
              label: "Облако точек",
              description: "Размер и способ окрашивания",
              content: (
                <div className="inspector-control-stack">
                  <div
                    className="scene-settings-commit-field"
                    onPointerUp={flushDisplaySettings}
                    onKeyUp={flushDisplaySettings}
                    onBlur={flushDisplaySettings}
                  >
                    <RangeControl
                      label="Размер точки"
                      value={displayDraft.pointSize}
                      min={0.5}
                      max={12}
                      step={0.5}
                      formatValue={(value) => `${value.toFixed(1)} пкс`}
                      onChange={(pointSize) => stageDisplayPatch({ pointSize })}
                    />
                  </div>
                  <div className="scene-settings-commit-field" onBlur={flushDisplaySettings}>
                    <ControlRow label="Атрибут цвета">
                      <Select
                        variant="split"
                        label="Атрибут цвета"
                        value={displayDraft.colorMode}
                        options={colorModeOptions}
                        onChange={(colorMode) => stageDisplayPatch({ colorMode })}
                      />
                    </ControlRow>
                  </div>
                  <div className="scene-settings-commit-field" onBlur={flushDisplaySettings}>
                    <ControlRow label="Палитра">
                      <Select
                        variant="split"
                        label="Палитра"
                        value={displayDraft.palette}
                        options={paletteOptions}
                        onChange={(palette) => stageDisplayPatch({ palette })}
                      />
                    </ControlRow>
                  </div>
                  {displayDraft.palette === "custom" || displayDraft.colorMode === "class" ? (
                    <div
                      className="scene-settings-commit-field"
                      onPointerUp={flushDisplaySettings}
                      onKeyUp={flushDisplaySettings}
                      onBlur={flushDisplaySettings}
                    >
                      <ControlRow label="Цвет точек">
                        <ColorField
                          label="Цвет точек"
                          value={displayDraft.customColor}
                          onChange={(customColor) => stageDisplayPatch({ customColor })}
                        />
                      </ControlRow>
                    </div>
                  ) : null}
                  {replayActive ? (
                    <p className="scene-window-note">
                      Первый новый режим читает индекс архивных точек. Следующие палитры
                      переключаются из подготовленного цветового кэша без переэкспорта геометрии.
                    </p>
                  ) : null}
                </div>
              ),
            },
            {
              id: "history",
              label: "Накопление и время",
              description: "История облака и траектория",
              content: (
                <div className="inspector-control-stack">
                  <div
                    className="scene-settings-commit-field"
                    onPointerUp={flushDisplaySettings}
                    onKeyUp={flushDisplaySettings}
                    onBlur={flushDisplaySettings}
                  >
                    <RangeControl
                      label="Окно накопления"
                      value={displayDraft.accumulationSeconds}
                      min={0}
                      max={120}
                      step={1}
                      formatValue={(value) => (value === 0 ? "Только кадр" : `${value} с`)}
                      onChange={(accumulationSeconds) => stageDisplayPatch({ accumulationSeconds })}
                    />
                  </div>
                  <div className="nodedc-field">
                    <span className="nodedc-field__description">Линия пути устройства в координатах сцены</span>
                    <Checker
                      checked={displayDraft.showTrajectory}
                      label="Показывать траекторию"
                      onChange={(showTrajectory) => commitDisplayPatch({ showTrajectory })}
                    />
                  </div>
                </div>
              ),
            },
            {
              id: "scene",
              label: "Окружение сцены",
              description: "Сетка, подписи и камеры",
              content: (
                <div className="inspector-control-stack">
                  <Checker checked={displayDraft.showGrid} label="Сетка и оси" onChange={(showGrid) => commitDisplayPatch({ showGrid })} />
                  <div className="nodedc-field">
                    <span className="nodedc-field__description">Появятся после подключения семантических сущностей</span>
                    <Checker checked={false} disabled label="Подписи сущностей" onChange={() => undefined} />
                  </div>
                  <div className="nodedc-field">
                    <span className="nodedc-field__description">Камерный канал пока не подключён</span>
                    <Checker checked={false} disabled label="Области обзора камер" onChange={() => undefined} />
                  </div>
                </div>
              ),
            },
          ]}
        />
      </Window>

      <Window
        open={spatialWorkspaceActive && layerInspectorOpen}
        title="Слои сцены"
        subtitle="Сущности пространственной сцены"
        placement="end"
        draggable
        closeOnBackdrop={false}
        closeOnEscape={false}
        lockBodyScroll={false}
        trapFocus={false}
        className="scene-tool-window scene-tool-window--layers"
        data-scene-window="layers"
        data-scene-active={activeSceneWindow === "layers" ? "true" : undefined}
        onPointerDown={() => activateSceneWindow("layers")}
        onClose={() => closeSceneWindow("layers")}
      >
        <div className="layer-inspector-intro">
          <span className="scene-window-state">
            <i className="api-dot" data-status={effectiveSourceUrl ? "online" : "checking"} aria-hidden="true" />
            {effectiveSourceUrl ? "Источник назначен" : "Источник не назначен"}
          </span>
          <p>Структура повторяет продуктовые сущности, а не внутренние панели визуального движка.</p>
        </div>
        <Inspector
          defaultOpen={["geometry"]}
          singleOpen
          sections={[
            {
              id: "geometry",
              label: "Геометрия",
              description: "Облако точек и путь",
              content: (
                <div className="inspector-control-stack">
                  <div className="nodedc-field">
                    <span className="nodedc-field__description">Основной поток геометрии лидара</span>
                    <Checker disabled={runtime.pendingAction === "viewer"} checked={sceneSettings.showPoints} label="Облако точек" onChange={(showPoints) => void applyScenePatch({ showPoints })} />
                  </div>
                  <div className="nodedc-field">
                    <span className="nodedc-field__description">Положение и ориентация устройства во времени</span>
                    <Checker disabled={runtime.pendingAction === "viewer"} checked={sceneSettings.showTrajectory} label="Траектория" onChange={(showTrajectory) => void applyScenePatch({ showTrajectory })} />
                  </div>
                </div>
              ),
            },
            {
              id: "frames",
              label: "Координаты и камеры",
              description: "Преобразования и области обзора",
              content: (
                <div className="inspector-control-stack">
                  <Checker disabled={runtime.pendingAction === "viewer"} checked={sceneSettings.showGrid} label="Сетка и оси" onChange={(showGrid) => void applyScenePatch({ showGrid })} />
                  <div className="nodedc-field">
                    <span className="nodedc-field__description">Камерный канал пока не подключён</span>
                    <Checker checked={false} disabled label="Области обзора камер" onChange={() => undefined} />
                  </div>
                </div>
              ),
            },
            {
              id: "perception",
              label: "Объекты и маски",
              description: "Ожидают внешние обработчики",
              content: (
                <div className="inspector-control-stack">
                  <div className="nodedc-field">
                    <span className="nodedc-field__description">Внешний обработчик не подключён</span>
                    <Checker checked={false} disabled label="Рамки 2D и 3D" onChange={() => undefined} />
                  </div>
                  <div className="nodedc-field">
                    <span className="nodedc-field__description">Внешний обработчик не подключён</span>
                    <Checker checked={false} disabled label="Маски сегментации" onChange={() => undefined} />
                  </div>
                  <div className="nodedc-field">
                    <span className="nodedc-field__description">Внешний обработчик не подключён</span>
                    <Checker checked={false} disabled label="Ключевые точки и треки" onChange={() => undefined} />
                  </div>
                </div>
              ),
            },
          ]}
        />
      </Window>

    </>
  );
}
