import { forwardRef, lazy, Suspense, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, type CSSProperties, type PointerEvent } from "react";
import { Button, Checker, ColorField, ControlRow, Dropdown, Icon, IconButton, Inspector, InspectorSelectField, RangeControl, SegmentedControl, Window, WorkspaceWindow } from "@nodedc/ui-react";
import type { SelectOption, WorkspaceWindowRect } from "@nodedc/ui-react";
import type {
  CameraSpiralState,
  CesiumMapRendererHandle,
  MapCameraView,
  MapGatewayHealth,
  MapPresentation,
  MapProviderStatus,
} from "./CesiumMapRenderer.js";
import { mapRuntimeEntityId, useMapDataProductRuntime } from "./useMapDataProductRuntime.js";
import {
  compareMapRuntimeFacts,
  mapFactMatchesFilters,
  mapPresentationFacetCounts,
  mapPresentationProfileForFact,
  mapRuntimeDisplayLabel,
  mapRuntimeFactIsRenderable,
  normalizeClientMapPresentationProfiles,
  resolveMapPresentationClass,
  toggleMapPresentationFacetSelection,
  type MapPresentationFilters,
  type MapPresentationProfile,
} from "./mapPresentationProfile.js";
import {
  CAMERA_SURVEY_PRESETS,
  DEFAULT_CAMERA_SURVEY_PRESET,
  OSM_BUILDINGS_OBSERVED_BAND_COUNT,
  cameraSurveySpiralDistance,
  findCameraSurveyPreset,
  type CameraSurveySelection,
} from "./mapCameraPresets.js";
import { buildMapSubjectCardModel, DEFAULT_MAP_SUBJECT_DETAIL_PROFILE } from "./mapSubjectCard.mjs";
import {
  ensureMapReferencePresentationProfiles,
  initialMapReferenceLayers,
  isMapReferencePresentationProfile,
  type MapReferenceLayer,
} from "./mapReferenceStations.js";
import { useMapReferenceRuntime } from "./useMapReferenceRuntime.js";
const CesiumMapRenderer = lazy(() => import("./CesiumMapRenderer.js").then((module) => ({ default: module.CesiumMapRenderer })));

type PreviewFeatures = { inspector?: boolean; toolbar?: boolean; assistant?: boolean };
export type MapPageSettings = Omit<MapPresentation, "cacheRefresh">;
type SurveySettingsSnapshot = Pick<
  MapPageSettings,
  "imageryVisible" | "monochrome" | "cacheEnabled" | "cacheNoOverwrite" | "terrainEnabled" | "buildingsVisible" | "buildingsDetail"
>;
type MapRuntimeConfig = { gatewayHealthUrl?: string | null; resourceProxyBase?: string | null };
type GatewayCheckState = "idle" | "checking" | "ready" | "stale" | "error";
type GatewayHealthOrder = { nextEpoch: number; latestStartedEpoch: number };
const RENDERER_GATEWAY_HEALTH_EPOCH = 1;
const SURVEY_GATEWAY_HEALTH_MAX_AGE_MS = 30_000;

function beginGatewayHealthEpoch(order: GatewayHealthOrder) {
  order.nextEpoch += 1;
  order.latestStartedEpoch = order.nextEpoch;
  return order.nextEpoch;
}

function isLatestGatewayHealthEpoch(order: GatewayHealthOrder, epoch: number) {
  return order.latestStartedEpoch === epoch;
}

function safeGatewayCheckCode(value: unknown, fallback = "gateway_not_ready") {
  const code = value instanceof Error && value.message ? value.message : fallback;
  return code.replace(/[^A-Za-z0-9_.:-]/g, "_").slice(0, 80) || fallback;
}

function gatewayCheckMessage(code: string, stale: boolean) {
  if (code === "persistent_cache_unavailable") {
    return "Persistent TileCache не подключён: карта не должна продолжать работу с локальной временной папкой.";
  }
  const prefix = stale ? "Текущая проверка не прошла" : "Проверка Platform Map Gateway не прошла";
  const suffix = stale
    ? "Показаны последние успешно полученные данные TileCache."
    : "TileCache и runtime profile не изменялись.";
  return `${prefix} (${code}). ${suffix}`;
}

function isWritableAppendOnlyTileCache(health: MapGatewayHealth | null) {
  return health?.cache?.persistent === true
    && health.cache.mode === "readwrite"
    && health.cache.writePolicy === "append-only-no-eviction"
    && health.cache.atCapacity === false;
}

/**
 * Provider-neutral visual binding. Foundry stores this on an Application page
 * instance; a future data binding resolves the live source behind `source`.
 */
export type MapPinBinding = {
  id: string;
  subjectId: string;
  kind: "elevated-spike";
  label: string;
  status: string;
  coordinates: { longitude: number; latitude: number; heightMeters: number };
  source: { entityId: string; streamId: string; displayFields: string[] };
  attributes: Record<string, string | number | boolean>;
};

/**
 * A renderer-neutral declaration of an entity stream assigned to this page.
 *
 * It intentionally contains no provider endpoint, tenant/connection scope,
 * credential reference, or browser token. The Foundry runtime resolves the
 * matching server-side consumer grant from the application/page/binding
 * target before it asks the External Data Plane for a snapshot or patches.
 */
export type MapDataProductBinding = {
  id: string;
  displayName?: string;
  order?: number;
  dataProductId: string;
  slotId: string;
  delivery: "snapshot+patch";
  semanticTypes: string[];
  fieldProjection: string[];
  presentationProfileId?: string;
  subjectDetailProfileId?: string;
  aspectId?: string;
  joinToBindingId?: string;
  dataClass?: "operational" | "restricted";
};

export type MapSubjectDetailProfile = {
  id: string;
  version: string;
  title: string;
  semanticTypes: string[];
  defaultTabId: string;
  tabs: Array<{
    id: string;
    label: string;
    emptyMessage: string;
    sections: Array<{
      id: string;
      label: string;
      fields: Array<{
        id: string;
        aspectId?: string;
        source: "fact" | "attribute" | "geometry" | "context";
        field: string;
        label: string;
        format: "text" | "number" | "timestamp" | "boolean" | "coordinate" | "signal_state" | "movement_state" | "string_list" | "telemetry_readings";
        unit?: string;
        allowedReadingIds?: string[];
      }>;
    }>;
  }>;
};

export type MapSubjectWindowState = {
  open: boolean;
  rect: WorkspaceWindowRect;
  maximized: boolean;
  zIndex: number;
};

export type MapSubjectState = {
  bindingId: string;
  visible: boolean;
  /** Missing facet means unconstrained; an explicit empty list means no matches. */
  filters: Record<string, string[]>;
  window: MapSubjectWindowState;
};

export type MapPageLayout = {
  schemaVersion: 1;
  pageId: "map";
  settings: MapPageSettings;
  mapHeight: number;
  camera: MapCameraView;
  pinBindings: MapPinBinding[];
  presentationProfiles: MapPresentationProfile[];
  subjectDetailProfiles: MapSubjectDetailProfile[];
  dataProductBindings: MapDataProductBinding[];
  subjectStates: MapSubjectState[];
  referenceLayers: MapReferenceLayer[];
  inspectorOpenSections: string[];
  savedAt?: string;
};

export type MapFixturePreviewHandle = {
  getLayout: () => MapPageLayout | null;
};

const initialMapSettings: MapPageSettings = {
  imagerySource: "cesium-live",
  imageryVisible: true,
  cacheEnabled: true,
  cacheNoOverwrite: true,
  terrainEnabled: true,
  terrainExaggeration: 1,
  monochrome: false,
  monochromeColor: "#15151b",
  imageryGamma: 57,
  imageryHue: 13,
  imageryAlpha: 27,
  globeColor: "#15151b",
  backgroundColor: "#08090d",
  atmosphereEnabled: false,
  atmosphereHue: 0,
  atmosphereSaturation: 0,
  atmosphereBrightness: 0,
  fogEnabled: true,
  fogDensity: 2,
  sunEnabled: true,
  sunHour: 12,
  sunIntensity: 200,
  shadowsEnabled: true,
  buildingsVisible: true,
  buildingsColor: "#a27aff",
  buildingsOpacity: 1,
  buildingsDetail: 4,
  imageryBrightness: 118,
  imageryContrast: 102,
  imagerySaturation: 0,
  gridVisible: true,
  gridLodEnabled: true,
  gridHeightMeters: 500,
  gridLod1MaxHeightKm: 10,
  gridLod1StepKm: 1,
  gridLod2MaxHeightKm: 50,
  gridLod2StepKm: 5,
  gridLod3StepKm: 25,
  gridRadiusKm: 40,
  gridLineWidth: 4,
  gridColor: "#f5f5f5",
  gridOpacity: 12,
  gridDotsEnabled: true,
  gridDotsSize: 7,
  gridDotsColor: "#ffffff",
  gridDotsOpacity: 58,
};

// A valid, deterministic scene view is available before Cesium emits its
// first move-end event. It makes the page contract immediately saveable;
// the renderer replaces it with the exact live camera as soon as it is ready.
const fallbackMapCamera: MapCameraView = {
  longitude: 37.618423,
  latitude: 55.751244,
  height: 40_000,
  heading: 0,
  pitch: -0.9,
  roll: 0,
};

export function createDefaultMapPageLayout(expanded = false): MapPageLayout {
  return {
    schemaVersion: 1,
    pageId: "map",
    settings: structuredClone(initialMapSettings),
    mapHeight: expanded ? 620 : 470,
    camera: { ...fallbackMapCamera },
    pinBindings: [],
    presentationProfiles: ensureMapReferencePresentationProfiles([]),
    subjectDetailProfiles: [structuredClone(DEFAULT_MAP_SUBJECT_DETAIL_PROFILE) as MapSubjectDetailProfile],
    dataProductBindings: [],
    subjectStates: [],
    referenceLayers: initialMapReferenceLayers(),
    inspectorOpenSections: ["map-base"],
  };
}

const initialProviderStatus: MapProviderStatus = {
  imagery: "loading",
  terrain: "loading",
  buildings: "loading",
  errors: {},
};

const providerStateLabel: Record<MapProviderStatus["imagery"], string> = {
  loading: "загружается",
  ready: "готов",
  error: "недоступен",
  "not-configured": "не настроен",
};

function defaultSubjectWindowState(index: number): MapSubjectWindowState {
  return {
    open: false,
    rect: {
      x: 24 + (index % 5) * 28,
      y: 56 + (index % 5) * 28,
      width: 280,
      height: 260,
    },
    maximized: false,
    zIndex: 20 + index,
  };
}

const defaultLayersWindowRect: WorkspaceWindowRect = {
  x: 1024,
  y: 72,
  width: 336,
  height: 500,
};

const defaultSubjectCardRect: WorkspaceWindowRect = {
  x: 940,
  y: 52,
  width: 390,
  height: 560,
};

function initialSubjectState(bindings: MapDataProductBinding[], saved: MapSubjectState[] | undefined) {
  const savedByBinding = new Map((saved ?? []).map((state) => [state.bindingId, state]));
  return Object.fromEntries(bindings.map((binding, index) => {
    const state = savedByBinding.get(binding.id);
    return [binding.id, state ?? {
      bindingId: binding.id,
      visible: true,
      filters: {},
      window: defaultSubjectWindowState(index),
    }];
  })) as Record<string, MapSubjectState>;
}

function hasSubjectWindowControls(profile: MapPresentationProfile) {
  return profile.facets.some((facet) => facet.counter || facet.filterable);
}

const logarithmicControlValue = (value: number) => Math.log10(Math.max(Number.MIN_VALUE, value));
const valueFromLogarithmicControl = (value: number) => Math.max(1, Math.round(10 ** value));
const formatMetricDistance = (value: number) => value >= 1000
  ? `${(value / 1000).toLocaleString("ru-RU", { maximumFractionDigits: value >= 10_000 ? 0 : 1 })} км`
  : `${Math.round(value)} м`;
const formatMetricSpeed = (value: number) => value >= 1000
  ? `${(value / 1000).toLocaleString("ru-RU", { maximumFractionDigits: 1 })} км/с`
  : `${Math.round(value)} м/с`;
const formatDuration = (seconds: number) => {
  if (seconds >= 86_400) return `${(seconds / 86_400).toLocaleString("ru-RU", { maximumFractionDigits: 1 })} сут`;
  if (seconds >= 3_600) return `${(seconds / 3_600).toLocaleString("ru-RU", { maximumFractionDigits: 1 })} ч`;
  if (seconds >= 60) return `${Math.round(seconds / 60)} мин`;
  return `${Math.max(1, Math.round(seconds))} с`;
};

function spiralStopMessage(reason: CameraSpiralState["reason"]) {
  if (reason === "target_radius_reached") return "Проход завершён: камера достигла выбранного радиуса.";
  if (reason === "spiral_extent_limit") return "Режим остановлен у безопасной границы геодезического маршрута.";
  if (reason === "terrain_sampling_error") return "Режим остановлен: не удалось получить высоту terrain для следующего участка.";
  if (reason === "tile_loading_timeout") return "Режим остановлен: текущий набор Imagery, Terrain или OSM Buildings не загрузился за отведённое время.";
  if (reason === "tile_loading_error") return "Режим остановлен: Cesium сообщил об ошибке tile в Imagery, Terrain или OSM Buildings. Неполный участок не засчитан.";
  if (reason === "spiral_runtime_error" || reason === "render_error") return "Режим остановлен из-за ошибки рендера камеры.";
  if (reason === "renderer_restarted") return "Режим остановлен после обновления renderer.";
  return null;
}

export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
  features?: PreviewFeatures;
  expanded?: boolean;
  initialLayout?: MapPageLayout | null;
  applicationId?: string;
  pageId?: string;
}>(function MapFixturePreview({ features = { inspector: true, toolbar: true, assistant: false }, expanded = false, initialLayout = null, applicationId, pageId }, ref) {
  const workspaceRef = useRef<HTMLDivElement>(null);
  const [selectedId, setSelectedId] = useState<string>();
  const [subjectCardOpen, setSubjectCardOpen] = useState(false);
  const [subjectCardRect, setSubjectCardRect] = useState<WorkspaceWindowRect>(defaultSubjectCardRect);
  const [subjectCardMaximized, setSubjectCardMaximized] = useState(false);
  const [subjectCardZIndex, setSubjectCardZIndex] = useState(140);
  const [subjectCardActive, setSubjectCardActive] = useState(false);
  const [subjectCardTabId, setSubjectCardTabId] = useState("overview");
  const [expandedFacetRows, setExpandedFacetRows] = useState<Record<string, boolean>>({});
  const [inspectorOpen, setInspectorOpen] = useState(false);
  const [inspectorOpenSections, setInspectorOpenSections] = useState<string[]>(() => (
    initialLayout?.inspectorOpenSections ?? ["map-base"]
  ));
  const [layersOpen, setLayersOpen] = useState(false);
  const [layersWindowRect, setLayersWindowRect] = useState<WorkspaceWindowRect>(defaultLayersWindowRect);
  const [layersWindowMaximized, setLayersWindowMaximized] = useState(false);
  const [layersWindowZIndex, setLayersWindowZIndex] = useState(12);
  const [layersWindowActive, setLayersWindowActive] = useState(false);
  const [toolbarOpen, setToolbarOpen] = useState(Boolean(features.toolbar));
  const [assistantOpen, setAssistantOpen] = useState(false);
  const [mapSettings, setMapSettings] = useState<MapPageSettings>(() => ({
    ...initialMapSettings,
    ...initialLayout?.settings,
    // Layouts saved before the cache policy field existed retain the safe
    // append-only default when they are opened again.
    cacheNoOverwrite: initialLayout?.settings?.cacheNoOverwrite ?? true,
  }));
  const [mapHeight, setMapHeight] = useState(() => initialLayout?.mapHeight ?? (expanded ? 620 : 470));
  const [mapCamera, setMapCamera] = useState<MapCameraView>(initialLayout?.camera ?? fallbackMapCamera);
  const mapRendererRef = useRef<CesiumMapRendererHandle | null>(null);
  const [mapRendererReady, setMapRendererReady] = useState(false);
  const [animationModeEnabled, setAnimationModeEnabled] = useState(false);
  const animationSettingsSnapshotRef = useRef<SurveySettingsSnapshot | null>(null);
  const [spiralRunning, setSpiralRunning] = useState(false);
  const [spiralPresetId, setSpiralPresetId] = useState<CameraSurveySelection>(DEFAULT_CAMERA_SURVEY_PRESET.id);
  const [spiralHeightMeters, setSpiralHeightMeters] = useState<number>(DEFAULT_CAMERA_SURVEY_PRESET.heightAboveGroundMeters);
  const [spiralSpeedMetersPerSecond, setSpiralSpeedMetersPerSecond] = useState<number>(DEFAULT_CAMERA_SURVEY_PRESET.speedMetersPerSecond);
  const [spiralPitchMetersPerTurn, setSpiralPitchMetersPerTurn] = useState<number>(DEFAULT_CAMERA_SURVEY_PRESET.pitchMetersPerTurn);
  const [spiralTargetRadiusMeters, setSpiralTargetRadiusMeters] = useState<number>(DEFAULT_CAMERA_SURVEY_PRESET.targetRadiusMeters);
  const [spiralMessage, setSpiralMessage] = useState<string | null>(null);
  // Map pin bindings belong to the application page instance. They are kept
  // intact when a human changes camera or visual settings and presses Save.
  const [pinBindings] = useState<MapPinBinding[]>(() => initialLayout?.pinBindings ?? []);
  // Presentation profiles are application/page-owned, versioned map.style_profile
  // values. A human camera/settings save must preserve profiles provisioned by MCP.
  const [presentationProfiles, setPresentationProfiles] = useState<MapPresentationProfile[]>(() => (
    ensureMapReferencePresentationProfiles(normalizeClientMapPresentationProfiles(initialLayout?.presentationProfiles ?? []))
  ));
  const [subjectDetailProfiles] = useState<MapSubjectDetailProfile[]>(() => (
    initialLayout?.subjectDetailProfiles?.length
      ? initialLayout.subjectDetailProfiles
      : [structuredClone(DEFAULT_MAP_SUBJECT_DETAIL_PROFILE) as MapSubjectDetailProfile]
  ));
  // Data-product bindings are provisioned by Foundry MCP / Platform and do
  // not belong to the visual inspector. Preserve them verbatim when a human
  // edits camera or presentation settings and saves the page layout.
  const [dataProductBindings] = useState<MapDataProductBinding[]>(() => initialLayout?.dataProductBindings ?? []);
  const [referenceLayers, setReferenceLayers] = useState<MapReferenceLayer[]>(() => (
    initialMapReferenceLayers(initialLayout?.referenceLayers)
  ));
  const [subjectStates, setSubjectStates] = useState<Record<string, MapSubjectState>>(() => (
    initialSubjectState(initialLayout?.dataProductBindings ?? [], initialLayout?.subjectStates)
  ));
  const [activeSubjectBindingId, setActiveSubjectBindingId] = useState<string>();
  const presentationFilters = useMemo<MapPresentationFilters>(() => Object.fromEntries(
    Object.entries(subjectStates).map(([bindingId, state]) => [bindingId, {
      visible: state.visible,
      facets: state.filters,
    }]),
  ), [subjectStates]);
  const runtimeBindings = useMapDataProductRuntime({
    applicationId,
    pageId,
    bindings: dataProductBindings,
    enabled: Boolean(applicationId && pageId),
  });
  const referenceRuntimeBindings = useMapReferenceRuntime(referenceLayers, mapCamera, true);
  const referencePresentationFilters = useMemo<MapPresentationFilters>(() => Object.fromEntries(
    referenceLayers.map((layer) => [layer.id, { visible: layer.visible, facets: {} }]),
  ), [referenceLayers]);
  const rendererPresentationFilters = useMemo<MapPresentationFilters>(() => ({
    ...presentationFilters,
    ...referencePresentationFilters,
  }), [presentationFilters, referencePresentationFilters]);
  const primaryBindingIds = useMemo(() => new Set(
    dataProductBindings.filter((binding) => !binding.joinToBindingId).map((binding) => binding.id),
  ), [dataProductBindings]);
  const primaryRuntimeBindings = useMemo(() => (
    runtimeBindings.filter((binding) => primaryBindingIds.has(binding.bindingId))
  ), [primaryBindingIds, runtimeBindings]);
  const selectable = useMemo(() => (
    primaryRuntimeBindings.flatMap((binding) => {
      const bindingConfig = dataProductBindings.find((candidate) => candidate.id === binding.bindingId);
      const facts = [...binding.facts];
      const primaryProfile = mapPresentationProfileForFact(
        presentationProfiles,
        bindingConfig?.presentationProfileId,
        bindingConfig?.semanticTypes[0] ?? facts[0]?.semanticType ?? "",
      );
      if (primaryProfile) facts.sort((left, right) => compareMapRuntimeFacts(left, right, primaryProfile));
      return facts.map((fact) => {
        const profile = mapPresentationProfileForFact(presentationProfiles, bindingConfig?.presentationProfileId, fact.semanticType);
        const presentationClass = profile ? resolveMapPresentationClass(fact, profile) : undefined;
        return {
          id: mapRuntimeEntityId(binding.bindingId, fact),
          title: mapRuntimeDisplayLabel(fact, profile),
          kind: fact.semanticType,
          status: presentationClass?.label ?? fact.presentationStatus,
          bindingId: binding.bindingId,
          dataProductId: binding.dataProductId,
          fact,
        };
      });
    })
  ), [dataProductBindings, presentationProfiles, primaryRuntimeBindings]);
  const presentationSummaries = useMemo(() => [...dataProductBindings]
    .filter((binding) => !binding.joinToBindingId)
    .sort((left, right) => (left.order ?? 0) - (right.order ?? 0) || left.id.localeCompare(right.id))
    .flatMap((bindingConfig) => {
    const binding = runtimeBindings.find((candidate) => candidate.bindingId === bindingConfig.id);
    const facts = binding?.facts ?? [];
    const semanticType = bindingConfig.semanticTypes[0] ?? facts[0]?.semanticType ?? "";
    const profile = mapPresentationProfileForFact(presentationProfiles, bindingConfig?.presentationProfileId, semanticType);
    if (!profile) return [];
    return [{
      bindingId: bindingConfig.id,
      displayName: bindingConfig.displayName?.trim() || profile.title || bindingConfig.id,
      profile,
      total: facts.length,
      counts: mapPresentationFacetCounts(facts, profile),
    }];
  }), [dataProductBindings, presentationProfiles, runtimeBindings]);
  const referenceObjectSummaries = useMemo(() => referenceLayers.flatMap((layer) => {
    const profile = presentationProfiles.find((candidate) => candidate.id === layer.presentationProfileId);
    if (!profile) return [];
    const runtime = referenceRuntimeBindings.find((candidate) => candidate.bindingId === layer.id);
    return [{
      layer,
      displayName: profile.title,
      total: runtime?.facts.length ?? 0,
    }];
  }), [presentationProfiles, referenceLayers, referenceRuntimeBindings]);
  const objectLayerCount = presentationSummaries.length + referenceObjectSummaries.length;
  const filteredTargets = useMemo(() => primaryRuntimeBindings.flatMap((binding) => {
    const bindingConfig = dataProductBindings.find((candidate) => candidate.id === binding.bindingId);
    return binding.facts.flatMap((fact) => {
      const profile = mapPresentationProfileForFact(
        presentationProfiles,
        bindingConfig?.presentationProfileId,
        fact.semanticType,
      );
      if (!profile || !mapFactMatchesFilters(fact, profile, presentationFilters, binding.bindingId)) return [];
      const presentationClass = resolveMapPresentationClass(fact, profile);
      return [{
        bindingId: binding.bindingId,
        entityId: mapRuntimeEntityId(binding.bindingId, fact),
        title: mapRuntimeDisplayLabel(fact, profile),
        status: presentationClass?.label ?? "",
        renderable: mapRuntimeFactIsRenderable(fact, profile),
      }];
    });
  }).sort((left, right) => left.title.localeCompare(right.title, "ru")), [dataProductBindings, presentationFilters, presentationProfiles, primaryRuntimeBindings]);
  const visibleTargetEntityIds = useMemo(() => (
    filteredTargets.filter((target) => target.renderable).map((target) => target.entityId)
  ), [filteredTargets]);
  // The header Save action can be pressed immediately after Cesium finishes
  // constructing the scene. Keep the last camera synchronously as well as in
  // state, so the imperative page-layout contract never waits for React's
  // render cycle to publish a ready camera.
  const mapCameraRef = useRef<MapCameraView>(initialLayout?.camera ?? fallbackMapCamera);
  const [rendererRevision, setRendererRevision] = useState(0);
  const [gatewayHealth, setGatewayHealth] = useState<MapGatewayHealth | null>(null);
  const gatewayHealthRef = useRef<MapGatewayHealth | null>(null);
  // The renderer owns epoch 1 as a bootstrap health source. As soon as the UI
  // starts an explicit verification, its higher epoch becomes authoritative;
  // a slower renderer request can no longer overwrite that newer result.
  const gatewayHealthOrderRef = useRef<GatewayHealthOrder>({
    nextEpoch: RENDERER_GATEWAY_HEALTH_EPOCH,
    latestStartedEpoch: RENDERER_GATEWAY_HEALTH_EPOCH,
  });
  const gatewayCheckRequestRef = useRef<{ id: symbol; controller: AbortController; promise: Promise<void> } | null>(null);
  const [gatewayEndpoint, setGatewayEndpoint] = useState<string | null>(null);
  const [gatewayCheckState, setGatewayCheckState] = useState<GatewayCheckState>("idle");
  const [gatewayCheckError, setGatewayCheckError] = useState<string | null>(null);
  const [gatewayLastVerifiedAt, setGatewayLastVerifiedAt] = useState<Date | null>(null);
  const [providerStatus, setProviderStatus] = useState<MapProviderStatus>(initialProviderStatus);
  const [cacheRefresh, setCacheRefresh] = useState(false);
  const spiralPresetOptions = useMemo<Array<SelectOption<CameraSurveySelection>>>(() => [
    ...CAMERA_SURVEY_PRESETS.map((preset) => ({
      value: preset.id,
      label: preset.label,
      description: `шаг ${formatMetricDistance(preset.pitchMetersPerTurn)} · до ${formatMetricDistance(preset.targetRadiusMeters)}`,
    })),
    ...(spiralPresetId === "custom" ? [{
      value: "custom" as const,
      label: "Пользовательский",
      description: "Значения изменены вручную",
    }] : []),
  ], [spiralPresetId]);
  const selected = selectable.find((entity) => entity.id === selectedId) ?? selectable[0];
  const selectedSubjectCard = useMemo(() => {
    const entity = selectable.find((candidate) => candidate.id === selectedId);
    if (!entity) return null;
    const primaryBinding = dataProductBindings.find((binding) => binding.id === entity.bindingId);
    const profile = subjectDetailProfiles.find((candidate) => candidate.id === primaryBinding?.subjectDetailProfileId)
      ?? subjectDetailProfiles.find((candidate) => candidate.semanticTypes.includes(entity.fact.semanticType))
      ?? DEFAULT_MAP_SUBJECT_DETAIL_PROFILE;
    const aspects = Object.fromEntries(dataProductBindings
      .filter((binding) => binding.id === entity.bindingId || binding.joinToBindingId === entity.bindingId)
      .flatMap((binding) => {
        const runtime = runtimeBindings.find((candidate) => candidate.bindingId === binding.id);
        const fact = binding.id === entity.bindingId
          ? entity.fact
          : runtime?.facts.find((candidate) => candidate.sourceId === entity.fact.sourceId);
        if (!fact) return [];
        return [[binding.id === entity.bindingId ? "primary" : (binding.aspectId ?? binding.id), {
          fact,
          bindingId: binding.id,
          dataProductId: binding.dataProductId,
          dataClass: binding.dataClass ?? "operational",
        }]];
      }));
    return buildMapSubjectCardModel(entity.fact, {
      title: entity.title,
      bindingId: entity.bindingId,
      dataProductId: entity.dataProductId,
      profile,
      aspects,
    });
  }, [dataProductBindings, runtimeBindings, selectable, selectedId, subjectDetailProfiles]);
  const presentation = useMemo<MapPresentation>(
    () => ({ ...mapSettings, cacheRefresh }),
    [cacheRefresh, mapSettings],
  );
  const updateMapSettings = (patch: Partial<MapPageSettings>) => setMapSettings((current) => ({ ...current, ...patch }));
  const setCacheEnabled = (cacheEnabled: boolean) => {
    updateMapSettings({ cacheEnabled });
    setRendererRevision((value) => value + 1);
  };
  const setCacheNoOverwrite = (cacheNoOverwrite: boolean) => {
    updateMapSettings({ cacheNoOverwrite });
    setCacheRefresh(false);
    setRendererRevision((value) => value + 1);
  };
  const refreshCurrentViewport = () => {
    if (!mapSettings.cacheEnabled) return;
    setCacheRefresh(true);
    setRendererRevision((value) => value + 1);
  };
  const handleCacheRefreshConsumed = useCallback(() => {
    setCacheRefresh(false);
    setRendererRevision((value) => value + 1);
  }, []);

  const handleCameraChange = useCallback((camera: MapCameraView) => {
    mapCameraRef.current = camera;
    setMapCamera(camera);
  }, []);

  const handleSpiralStateChange = useCallback((state: CameraSpiralState) => {
    setSpiralRunning(state.running);
    setSpiralMessage(state.running ? null : spiralStopMessage(state.reason));
  }, []);

  const prepareAnimationSurvey = () => {
    const failedProviderNeedsRetry = [providerStatus.imagery, providerStatus.terrain, providerStatus.buildings]
      .some((state) => state === "error" || state === "not-configured");
    const rendererRestartRequired = !mapSettings.cacheEnabled
      || !mapSettings.cacheNoOverwrite
      || cacheRefresh
      || failedProviderNeedsRetry;
    updateMapSettings({
      imageryVisible: true,
      // Monochrome deliberately hides the imagery layer. A cache survey must
      // render it so Cesium actually requests every visible imagery tile.
      monochrome: false,
      cacheEnabled: true,
      cacheNoOverwrite: true,
      terrainEnabled: true,
      buildingsVisible: true,
      // The measured OSM hierarchy and presets use the canonical SSE value.
      buildingsDetail: 16,
    });
    setCacheRefresh(false);
    if (rendererRestartRequired) {
      setMapRendererReady(false);
      setProviderStatus(initialProviderStatus);
      setRendererRevision((value) => value + 1);
    }
  };

  const setAnimationMode = (enabled: boolean) => {
    if (enabled === animationModeEnabled) return;
    setAnimationModeEnabled(enabled);
    setSpiralMessage(null);
    if (enabled) {
      // Survey preparation is transient. Save the page presentation before
      // forcing all cacheable providers on, and restore it when the mode ends.
      animationSettingsSnapshotRef.current = {
        imageryVisible: mapSettings.imageryVisible,
        monochrome: mapSettings.monochrome,
        cacheEnabled: mapSettings.cacheEnabled,
        cacheNoOverwrite: mapSettings.cacheNoOverwrite,
        terrainEnabled: mapSettings.terrainEnabled,
        buildingsVisible: mapSettings.buildingsVisible,
        buildingsDetail: mapSettings.buildingsDetail,
      };
      prepareAnimationSurvey();
      void verifyGateway();
    } else {
      mapRendererRef.current?.stopSpiralAnimation("mode_disabled");
      setSpiralRunning(false);
      const snapshot = animationSettingsSnapshotRef.current;
      animationSettingsSnapshotRef.current = null;
      if (snapshot) {
        const rendererRestartRequired = snapshot.cacheEnabled !== mapSettings.cacheEnabled
          || snapshot.cacheNoOverwrite !== mapSettings.cacheNoOverwrite;
        // Restore only fields owned by the survey overlay. Any unrelated
        // Inspector edits made while the mode was open remain intact.
        setMapSettings((current) => ({ ...current, ...snapshot }));
        setCacheRefresh(false);
        if (rendererRestartRequired) {
          setMapRendererReady(false);
          setProviderStatus(initialProviderStatus);
          setRendererRevision((value) => value + 1);
        }
      }
    }
  };

  const selectSpiralPreset = (presetId: CameraSurveySelection) => {
    if (presetId === "custom") return;
    const preset = findCameraSurveyPreset(presetId);
    if (!preset) return;
    setSpiralPresetId(preset.id);
    setSpiralHeightMeters(preset.heightAboveGroundMeters);
    setSpiralSpeedMetersPerSecond(preset.speedMetersPerSecond);
    setSpiralPitchMetersPerTurn(preset.pitchMetersPerTurn);
    setSpiralTargetRadiusMeters(preset.targetRadiusMeters);
    setSpiralMessage(null);
  };

  const spiralSurveyConfigured = mapSettings.cacheEnabled
    && mapSettings.cacheNoOverwrite
    && mapSettings.imageryVisible
    && !mapSettings.monochrome
    && mapSettings.terrainEnabled
    && mapSettings.buildingsVisible
    && mapSettings.buildingsDetail === 16;
  const spiralProvidersReady = providerStatus.imagery === "ready"
    && providerStatus.terrain === "ready"
    && providerStatus.buildings === "ready";
  const spiralGatewayHealthFresh = Boolean(
    gatewayLastVerifiedAt
    && Date.now() - gatewayLastVerifiedAt.getTime() <= SURVEY_GATEWAY_HEALTH_MAX_AGE_MS
    && gatewayCheckState !== "error"
    && gatewayCheckState !== "stale",
  );
  const spiralTileCacheReady = spiralGatewayHealthFresh && isWritableAppendOnlyTileCache(gatewayHealth);
  const spiralCanStart = mapRendererReady && spiralSurveyConfigured && spiralProvidersReady && spiralTileCacheReady;

  useEffect(() => {
    if (!spiralRunning || (spiralSurveyConfigured && spiralTileCacheReady)) return;
    mapRendererRef.current?.stopSpiralAnimation("mode_disabled");
  }, [spiralRunning, spiralSurveyConfigured, spiralTileCacheReady]);

  const toggleSpiralAnimation = () => {
    if (spiralRunning) {
      mapRendererRef.current?.stopSpiralAnimation("stopped");
      return;
    }
    if (!spiralCanStart) {
      setSpiralMessage("Подождите готовности Imagery, Terrain, OSM Buildings и свежей проверки writable append-only TileCache.");
      return;
    }
    const started = mapRendererRef.current?.startSpiralAnimation({
      heightAboveGroundMeters: spiralHeightMeters,
      speedMetersPerSecond: spiralSpeedMetersPerSecond,
      pitchMetersPerTurn: spiralPitchMetersPerTurn,
      targetRadiusMeters: spiralTargetRadiusMeters,
      viewPitchRadians: -Math.PI / 2 + 0.01,
      waitForTiles: true,
    }) ?? false;
    if (!started) setSpiralMessage("Карта ещё не готова к запуску режима анимации.");
  };

  useImperativeHandle(ref, () => ({
    getLayout: () => ({
      schemaVersion: 1,
      pageId: "map",
      // Survey-only layer overrides never leak into a saved page layout.
      settings: animationSettingsSnapshotRef.current
        ? { ...mapSettings, ...animationSettingsSnapshotRef.current }
        : mapSettings,
      mapHeight: Math.round(mapHeight),
      camera: mapRendererRef.current?.getCameraView() ?? mapCameraRef.current ?? mapCamera,
      pinBindings,
      presentationProfiles,
      subjectDetailProfiles,
      dataProductBindings,
      referenceLayers,
      inspectorOpenSections,
      subjectStates: dataProductBindings.map((binding) => subjectStates[binding.id] ?? {
        bindingId: binding.id,
        visible: true,
        filters: {},
        window: defaultSubjectWindowState(0),
      }).map((state) => {
        const summary = presentationSummaries.find((candidate) => candidate.bindingId === state.bindingId);
        return summary && !hasSubjectWindowControls(summary.profile)
          ? { ...state, window: { ...state.window, open: false } }
          : state;
      }),
    }),
  }), [dataProductBindings, inspectorOpenSections, mapCamera, mapHeight, mapSettings, pinBindings, presentationProfiles, presentationSummaries, referenceLayers, subjectDetailProfiles, subjectStates]);

  const updateSubjectState = (bindingId: string, update: (state: MapSubjectState) => MapSubjectState) => {
    setSubjectStates((current) => {
      const index = dataProductBindings.findIndex((binding) => binding.id === bindingId);
      const state = current[bindingId] ?? {
        bindingId,
        visible: true,
        filters: {},
        window: defaultSubjectWindowState(Math.max(0, index)),
      };
      return { ...current, [bindingId]: update(state) };
    });
  };

  const togglePresentationFilter = (bindingId: string, field: string, value: string) => {
    updateSubjectState(bindingId, (state) => {
      const filters = state.visible ? state.filters : {};
      return {
        ...state,
        visible: true,
        filters: toggleMapPresentationFacetSelection(filters, field, value),
      };
    });
  };

  const toggleSubjectVisibility = (bindingId: string) => {
    updateSubjectState(bindingId, (state) => ({ ...state, visible: !state.visible }));
  };

  const openSubjectWindow = (bindingId: string) => {
    const nextZIndex = Math.max(20, layersWindowZIndex, subjectCardZIndex, ...Object.values(subjectStates).map((state) => state.window.zIndex)) + 1;
    updateSubjectState(bindingId, (state) => ({
      ...state,
      window: { ...state.window, open: true, zIndex: nextZIndex },
    }));
    setLayersWindowActive(false);
    setSubjectCardActive(false);
    setActiveSubjectBindingId(bindingId);
  };

  const closeSubjectWindow = (bindingId: string) => {
    updateSubjectState(bindingId, (state) => ({ ...state, window: { ...state.window, open: false } }));
    setActiveSubjectBindingId((current) => current === bindingId ? undefined : current);
  };

  const activateLayersWindow = () => {
    const nextZIndex = Math.max(20, layersWindowZIndex, subjectCardZIndex, ...Object.values(subjectStates).map((state) => state.window.zIndex)) + 1;
    setLayersWindowZIndex(nextZIndex);
    setLayersWindowActive(true);
    setSubjectCardActive(false);
    setActiveSubjectBindingId(undefined);
  };

  const toggleLayersWindow = () => {
    if (layersOpen) {
      setLayersOpen(false);
      setLayersWindowActive(false);
      return;
    }
    setLayersOpen(true);
    activateLayersWindow();
  };

  const updatePresentationProfile = (
    profileId: string,
    update: (profile: MapPresentationProfile) => MapPresentationProfile,
  ) => setPresentationProfiles((current) => current.map((profile) => (
    profile.id === profileId ? update(profile) : profile
  )));

  const updatePresentationStyle = (profileId: string, styleId: string, patch: Partial<MapPresentationProfile["styles"][number]>) => {
    updatePresentationProfile(profileId, (profile) => ({
      ...profile,
      styles: profile.styles.map((style) => style.id === styleId ? { ...style, ...patch } : style),
    }));
  };

  const handleSelect = useCallback((entityId: string) => {
    if (!selectable.some((entity) => entity.id === entityId)) return;
    setSelectedId(entityId);
    const entity = selectable.find((candidate) => candidate.id === entityId);
    const binding = dataProductBindings.find((candidate) => candidate.id === entity?.bindingId);
    const profile = subjectDetailProfiles.find((candidate) => candidate.id === binding?.subjectDetailProfileId)
      ?? subjectDetailProfiles.find((candidate) => candidate.semanticTypes.includes(entity?.fact.semanticType ?? ""));
    setSubjectCardTabId((current) => (
      profile?.tabs.some((tab) => tab.id === current)
        ? current
        : (profile?.defaultTabId ?? "overview")
    ));
    setSubjectCardOpen(true);
    setSubjectCardActive(true);
    setLayersWindowActive(false);
    setActiveSubjectBindingId(undefined);
    setSubjectCardZIndex((current) => Math.max(current, layersWindowZIndex, ...Object.values(subjectStates).map((state) => state.window.zIndex)) + 1);
  }, [dataProductBindings, layersWindowZIndex, selectable, subjectDetailProfiles, subjectStates]);

  const handleSelectAndFocus = useCallback((entityId: string) => {
    handleSelect(entityId);
    mapRendererRef.current?.focusRuntimeEntity(entityId);
  }, [handleSelect]);

  const rememberGatewayHealth = useCallback((health: MapGatewayHealth) => {
    gatewayHealthRef.current = health;
    setGatewayHealth(health);
    setGatewayLastVerifiedAt(new Date());
  }, []);

  const beginGatewayHealthVerification = useCallback(() => {
    return beginGatewayHealthEpoch(gatewayHealthOrderRef.current);
  }, []);

  const isLatestGatewayHealthRequest = useCallback((epoch: number) => (
    isLatestGatewayHealthEpoch(gatewayHealthOrderRef.current, epoch)
  ), []);

  const handleRendererGatewayHealth = useCallback((health: MapGatewayHealth | null) => {
    if (!isLatestGatewayHealthRequest(RENDERER_GATEWAY_HEALTH_EPOCH)) return;
    if (health?.cache?.persistent === true) {
      rememberGatewayHealth(health);
      setGatewayCheckError(null);
      setGatewayCheckState("ready");
      return;
    }
    const stale = Boolean(gatewayHealthRef.current);
    const code = health ? "persistent_cache_unavailable" : "gateway_health_unavailable";
    setGatewayCheckError(gatewayCheckMessage(code, stale));
    setGatewayCheckState(stale ? "stale" : "error");
  }, [isLatestGatewayHealthRequest, rememberGatewayHealth]);

  const verifyGateway = useCallback(() => {
    const pending = gatewayCheckRequestRef.current;
    if (pending) return pending.promise;
    const epoch = beginGatewayHealthVerification();
    const id = Symbol("gateway-check");
    const controller = new AbortController();
    let timedOut = false;
    let stage: "runtime" | "gateway_health" = "runtime";
    setGatewayCheckState("checking");
    setGatewayCheckError(null);
    const timeout = window.setTimeout(() => {
      timedOut = true;
      controller.abort();
    }, 10_000);
    const promise = (async () => {
      try {
        const runtimeResponse = await fetch("/api/map/runtime-config", { cache: "no-store", signal: controller.signal });
        if (!runtimeResponse.ok) throw new Error(`runtime_http_${runtimeResponse.status}`);
        let runtime: MapRuntimeConfig;
        try {
          runtime = await runtimeResponse.json() as MapRuntimeConfig;
        } catch {
          throw new Error("runtime_invalid_response");
        }
        if (!runtime?.gatewayHealthUrl) throw new Error("gateway_not_configured");
        stage = "gateway_health";
        const healthResponse = await fetch(runtime.gatewayHealthUrl, { cache: "no-store", signal: controller.signal });
        if (!healthResponse.ok) throw new Error(`gateway_health_http_${healthResponse.status}`);
        let health: MapGatewayHealth;
        try {
          health = await healthResponse.json() as MapGatewayHealth;
        } catch {
          throw new Error("gateway_health_invalid_response");
        }
        if (health.cache?.persistent !== true) throw new Error("persistent_cache_unavailable");
        if (!isLatestGatewayHealthRequest(epoch)) return;
        rememberGatewayHealth(health);
        // Runtime configuration deliberately exposes a same-origin relative
        // route. Resolve it against the current browser origin before displaying
        // the connection; new URL("/api/…") without a base throws and used to
        // turn a healthy Gateway into a false "unavailable" state.
        setGatewayEndpoint(new URL(runtime.gatewayHealthUrl, window.location.origin).origin);
        setGatewayCheckState("ready");
      } catch (error) {
        if (controller.signal.aborted && !timedOut) return;
        if (!isLatestGatewayHealthRequest(epoch)) return;
        const code = timedOut
          ? "gateway_check_timeout"
          : error instanceof TypeError
            ? `${stage}_network_error`
            : safeGatewayCheckCode(error);
        const stale = Boolean(gatewayHealthRef.current);
        if (!stale) setGatewayEndpoint(null);
        setGatewayCheckError(gatewayCheckMessage(code, stale));
        setGatewayCheckState(stale ? "stale" : "error");
      } finally {
        window.clearTimeout(timeout);
        if (gatewayCheckRequestRef.current?.id === id) gatewayCheckRequestRef.current = null;
      }
    })();
    gatewayCheckRequestRef.current = { id, controller, promise };
    return promise;
  }, [beginGatewayHealthVerification, isLatestGatewayHealthRequest, rememberGatewayHealth]);

  useEffect(() => {
    if (!inspectorOpen && !layersOpen && !animationModeEnabled) return;
    void verifyGateway();
    const interval = window.setInterval(() => void verifyGateway(), 15_000);
    return () => {
      window.clearInterval(interval);
      const pending = gatewayCheckRequestRef.current;
      if (pending) {
        gatewayCheckRequestRef.current = null;
        pending.controller.abort();
      }
    };
  }, [animationModeEnabled, inspectorOpen, layersOpen, verifyGateway]);

  const liveCacheStatus = gatewayHealth?.cache;
  const liveCacheSummary = liveCacheStatus
    ? `${liveCacheStatus.entries ?? 0} объектов · ${Math.round((liveCacheStatus.bytes ?? 0) / 1024 / 1024)} / ${Math.round((liveCacheStatus.maxBytes ?? 0) / 1024 / 1024) || "?"} MB`
    : "индекс ещё не получен";
  const transportDiagnostic = gatewayHealth?.diagnostics?.lastFailure
    ? `Последняя transport-ошибка: ${gatewayHealth.diagnostics.lastFailure}${gatewayHealth.diagnostics.lastFailureAt ? ` · ${new Date(gatewayHealth.diagnostics.lastFailureAt).toLocaleTimeString()}` : ""}`
    : null;
  const gatewayHealthAge = gatewayCheckState === "stale" && gatewayLastVerifiedAt
    ? `Последняя успешная проверка: ${gatewayLastVerifiedAt.toLocaleTimeString()}`
    : null;

  const startResize = (event: PointerEvent<HTMLButtonElement>) => {
    event.preventDefault();
    const startY = event.clientY;
    const startHeight = mapHeight;
    const onMove = (move: globalThis.PointerEvent) => {
      const maximum = Math.max(380, Math.round(window.innerHeight * 0.78));
      setMapHeight(Math.max(360, Math.min(maximum, startHeight + move.clientY - startY)));
    };
    const onEnd = () => {
      window.removeEventListener("pointermove", onMove);
      window.removeEventListener("pointerup", onEnd);
      window.removeEventListener("pointercancel", onEnd);
    };
    window.addEventListener("pointermove", onMove);
    window.addEventListener("pointerup", onEnd);
    window.addEventListener("pointercancel", onEnd);
  };

  const inspectorSections = [
    {
      id: "map-base",
      label: "Подложка и terrain",
      description: "provider-neutral surface",
      group: "Карта",
      content: <>
        <ControlRow label="Подложка"><strong>Cesium World Imagery</strong></ControlRow>
        <small className="catalog-map-inspector__note">Текущий официальный provider. Другие provider-слои появятся только после отдельного asset-контракта Platform.</small>
        <ControlRow label="Live providers"><span>Imagery: {providerStateLabel[providerStatus.imagery]} · Terrain: {providerStateLabel[providerStatus.terrain]} · 3D: {providerStateLabel[providerStatus.buildings]}</span></ControlRow>
        {Object.entries(providerStatus.errors).map(([provider, error]) => <small key={provider} className="catalog-map-inspector__note catalog-map-inspector__note--error" role="alert">{provider}: {error}</small>)}
        <small className="catalog-map-inspector__note">Рельеф — отдельный слой под imagery.</small>
        <Checker checked={mapSettings.terrainEnabled} label="Terrain" onChange={(terrainEnabled) => updateMapSettings({ terrainEnabled })} />
        <RangeControl label="Вертикальное преувеличение рельефа" value={mapSettings.terrainExaggeration * 100} min={25} max={300} formatValue={(value) => `${(value / 100).toFixed(2)}×`} onChange={(value) => updateMapSettings({ terrainExaggeration: value / 100 })} />
        <Checker checked={mapSettings.monochrome} label="Монохромная поверхность" onChange={(monochrome) => updateMapSettings({ monochrome })} />
        <ControlRow label="Цвет монохрома"><ColorField label="Цвет монохромной поверхности" value={mapSettings.monochromeColor} onChange={(monochromeColor) => updateMapSettings({ monochromeColor })} /></ControlRow>
        <RangeControl label="Яркость" value={mapSettings.imageryBrightness} min={0} max={200} formatValue={(value) => `${value}%`} onChange={(imageryBrightness) => updateMapSettings({ imageryBrightness })} />
        <RangeControl label="Контраст" value={mapSettings.imageryContrast} min={0} max={200} formatValue={(value) => `${value}%`} onChange={(imageryContrast) => updateMapSettings({ imageryContrast })} />
        <RangeControl label="Насыщенность" value={mapSettings.imagerySaturation} min={0} max={200} formatValue={(value) => `${value}%`} onChange={(imagerySaturation) => updateMapSettings({ imagerySaturation })} />
        <RangeControl label="Гамма" value={mapSettings.imageryGamma} min={0} max={300} formatValue={(value) => `${value}%`} onChange={(imageryGamma) => updateMapSettings({ imageryGamma })} />
        <RangeControl label="Оттенок" value={mapSettings.imageryHue} min={-180} max={180} formatValue={(value) => `${value}°`} onChange={(imageryHue) => updateMapSettings({ imageryHue })} />
        <RangeControl label="Прозрачность imagery" value={mapSettings.imageryAlpha} min={0} max={100} formatValue={(value) => `${value}%`} onChange={(imageryAlpha) => updateMapSettings({ imageryAlpha })} />
        <ControlRow label="Цвет планеты"><ColorField label="Цвет terrain без imagery" value={mapSettings.globeColor} onChange={(globeColor) => updateMapSettings({ globeColor })} /></ControlRow>
        <ControlRow label="Фон сцены"><ColorField label="Цвет фона сцены" value={mapSettings.backgroundColor} onChange={(backgroundColor) => updateMapSettings({ backgroundColor })} /></ControlRow>
      </>,
    },
    {
      id: "map-atmosphere",
      label: "Атмосфера и освещение",
      description: "scene / color correction",
      group: "Карта",
      content: <>
        <Checker checked={mapSettings.atmosphereEnabled} label="Показывать атмосферу" onChange={(atmosphereEnabled) => updateMapSettings({ atmosphereEnabled })} />
        <RangeControl label="Атмосфера: оттенок" value={mapSettings.atmosphereHue} min={-100} max={100} formatValue={(value) => `${value}%`} onChange={(atmosphereHue) => updateMapSettings({ atmosphereHue })} />
        <RangeControl label="Атмосфера: насыщенность" value={mapSettings.atmosphereSaturation} min={-100} max={100} formatValue={(value) => `${value}%`} onChange={(atmosphereSaturation) => updateMapSettings({ atmosphereSaturation })} />
        <RangeControl label="Атмосфера: яркость" value={mapSettings.atmosphereBrightness} min={-100} max={100} formatValue={(value) => `${value}%`} onChange={(atmosphereBrightness) => updateMapSettings({ atmosphereBrightness })} />
        <Checker checked={mapSettings.fogEnabled} label="Туман" onChange={(fogEnabled) => updateMapSettings({ fogEnabled })} />
        <RangeControl label="Плотность тумана" value={mapSettings.fogDensity} min={0} max={100} formatValue={(value) => `${(value / 10000).toFixed(4)}`} onChange={(fogDensity) => updateMapSettings({ fogDensity })} />
        <Checker checked={mapSettings.sunEnabled} label="Солнечное освещение" onChange={(sunEnabled) => updateMapSettings({ sunEnabled })} />
        <RangeControl label="Час солнца" value={mapSettings.sunHour} min={0} max={24} formatValue={(value) => `${value}:00 UTC`} onChange={(sunHour) => updateMapSettings({ sunHour })} />
        <RangeControl label="Интенсивность света" value={mapSettings.sunIntensity} min={0} max={200} formatValue={(value) => `${value}%`} onChange={(sunIntensity) => updateMapSettings({ sunIntensity })} />
        <Checker checked={mapSettings.shadowsEnabled} label="Тени" onChange={(shadowsEnabled) => updateMapSettings({ shadowsEnabled })} />
      </>,
    },
    {
      id: "map-buildings",
      label: "3D здания",
      description: "3D Tiles / detail",
      group: "Карта",
      content: <>
        <Checker checked={mapSettings.buildingsVisible} label="Показывать 3D здания" onChange={(buildingsVisible) => updateMapSettings({ buildingsVisible })} />
        <ControlRow label="Цвет"><ColorField label="Цвет зданий" value={mapSettings.buildingsColor} onChange={(buildingsColor) => updateMapSettings({ buildingsColor })} /></ControlRow>
        <RangeControl label="Прозрачность" value={Math.round(mapSettings.buildingsOpacity * 100)} min={0} max={100} formatValue={(value) => `${value}%`} onChange={(value) => updateMapSettings({ buildingsOpacity: value / 100 })} />
        <RangeControl label="Детализация" value={mapSettings.buildingsDetail} min={4} max={32} formatValue={(value) => `SSE ${value}`} onChange={(buildingsDetail) => updateMapSettings({ buildingsDetail })} />
      </>,
    },
    ...presentationProfiles.flatMap((profile) => {
      const referenceProfile = isMapReferencePresentationProfile(profile);
      const referenceLayer = referenceLayers.find((layer) => layer.presentationProfileId === profile.id);
      return [
      {
        id: `map-target-${profile.id}`,
        label: referenceProfile ? profile.title : profile.target.variant === "surface-fill" ? "HGeoZone" : "Таргет",
        description: profile.target.variant === "surface-fill" ? `проекция · ${profile.title}` : profile.title,
        group: referenceProfile ? "Станции" : profile.target.variant === "surface-fill" ? "Слои" : "Таргеты",
        content: <>
          <small className="catalog-map-inspector__note">Профиль принадлежит этой странице Application и управляется тем же provider-neutral MCP-контрактом. Исходный API в настройках отсутствует.</small>
          {referenceLayer ? (
            <Checker
              checked={referenceLayer.visible}
              label={`Показывать слой «${profile.title}»`}
              onChange={(visible) => setReferenceLayers((current) => current.map((layer) => (
                layer.id === referenceLayer.id ? { ...layer, visible } : layer
              )))}
            />
          ) : null}
          {profile.target.variant === "surface-fill" && <>
            <ControlRow label="Тип слоя"><strong>HGeoZone · ground projection</strong></ControlRow>
            {profile.styles.map((style) => {
              const classLabels = profile.classes.filter((item) => item.styleId === style.id).map((item) => item.label);
              const label = classLabels.length ? classLabels.join(" · ") : style.id;
              return <div className="catalog-map-inspector__style" key={style.id}>
                <ControlRow label={`Заливка · ${label}`}><ColorField label={`Цвет заливки: ${label}`} value={style.color} onChange={(color) => updatePresentationStyle(profile.id, style.id, { color })} /></ControlRow>
                <RangeControl label={`Прозрачность заливки · ${label}`} value={Math.round(style.opacity * 100)} min={0} max={100} step={1} formatValue={(value) => `${value}%`} onChange={(value) => updatePresentationStyle(profile.id, style.id, { opacity: value / 100 })} />
              </div>;
            })}
            <ControlRow label="Граница"><ColorField label="Цвет границы HGeoZone" value={profile.target.outlineColor} onChange={(outlineColor) => updatePresentationProfile(profile.id, (current) => current.target.variant === "surface-fill" ? ({ ...current, target: { ...current.target, outlineColor } }) : current)} /></ControlRow>
            <RangeControl label="Прозрачность границы" value={Math.round(profile.target.outlineOpacity * 100)} min={0} max={100} step={1} formatValue={(value) => `${value}%`} onChange={(value) => updatePresentationProfile(profile.id, (current) => current.target.variant === "surface-fill" ? ({ ...current, target: { ...current.target, outlineOpacity: value / 100 } }) : current)} />
            <RangeControl label="Толщина границы" value={profile.target.outlineWidthPx} min={0} max={8} step={0.5} formatValue={(value) => `${value} px`} onChange={(outlineWidthPx) => updatePresentationProfile(profile.id, (current) => current.target.variant === "surface-fill" ? ({ ...current, target: { ...current.target, outlineWidthPx } }) : current)} />
          </>}
          {profile.target.variant === "elevated-spike" && <>
            <RangeControl label="Высота таргета" value={profile.target.stemHeightMeters} min={100} max={10_000} step={50} formatValue={(value) => `${value} м`} onChange={(stemHeightMeters) => updatePresentationProfile(profile.id, (current) => current.target.variant === "elevated-spike" ? ({ ...current, target: { ...current.target, stemHeightMeters } }) : current)} />
            <RangeControl label="Размер головки" value={profile.target.headSizePx} min={1} max={32} step={1} formatValue={(value) => `${value} px`} onChange={(headSizePx) => updatePresentationProfile(profile.id, (current) => current.target.variant === "elevated-spike" ? ({ ...current, target: { ...current.target, headSizePx } }) : current)} />
            <RangeControl label="Толщина стержня" value={profile.target.stemWidthPx} min={0.25} max={12} step={0.25} formatValue={(value) => `${value} px`} onChange={(stemWidthPx) => updatePresentationProfile(profile.id, (current) => current.target.variant === "elevated-spike" ? ({ ...current, target: { ...current.target, stemWidthPx } }) : current)} />
          </>}
          <InspectorSelectField
            label="Подпись"
            value={profile.label.mode}
            options={[
              { value: "subject_id", label: "ID", description: "Стабильный идентификатор сущности" },
              { value: "attributes", label: "Имя", description: "Первое доступное display-поле" },
              { value: "none", label: "Нет", description: "Не показывать плашку" },
            ]}
            onChange={(mode) => updatePresentationProfile(profile.id, (current) => ({ ...current, label: { ...current.label, mode } }))}
          />
          <RangeControl label="Размер подписи" value={profile.label.sizePx} min={8} max={32} step={1} formatValue={(value) => `${value} px`} onChange={(sizePx) => updatePresentationProfile(profile.id, (current) => ({ ...current, label: { ...current.label, sizePx } }))} />
          <RangeControl label="Смещение подписи X" value={profile.label.offsetX} min={-100} max={100} step={1} formatValue={(value) => `${value} px`} onChange={(offsetX) => updatePresentationProfile(profile.id, (current) => ({ ...current, label: { ...current.label, offsetX } }))} />
          <RangeControl label="Смещение подписи Y" value={profile.label.offsetY} min={-100} max={100} step={1} formatValue={(value) => `${value} px`} onChange={(offsetY) => updatePresentationProfile(profile.id, (current) => ({ ...current, label: { ...current.label, offsetY } }))} />
          <RangeControl label="Скрывать подпись выше" value={profile.label.hideCameraHeightMeters} min={1_000} max={500_000} step={1_000} formatValue={(value) => `${Math.round(value / 1_000)} км`} onChange={(hideCameraHeightMeters) => updatePresentationProfile(profile.id, (current) => ({ ...current, label: { ...current.label, hideCameraHeightMeters } }))} />
          <RangeControl label={profile.target.variant === "surface-fill" ? "Скрывать HGeoZone выше" : "Скрывать таргет выше"} value={profile.target.hideCameraHeightMeters} min={1_000} max={500_000} step={1_000} formatValue={(value) => `${Math.round(value / 1_000)} км`} onChange={(hideCameraHeightMeters) => updatePresentationProfile(profile.id, (current) => ({ ...current, target: { ...current.target, hideCameraHeightMeters } }))} />
          <ControlRow label="Фон плашки"><ColorField label="Цвет фона подписи" value={profile.label.backgroundColor} onChange={(backgroundColor) => updatePresentationProfile(profile.id, (current) => ({ ...current, label: { ...current.label, backgroundColor } }))} /></ControlRow>
          <RangeControl label="Прозрачность плашки" value={Math.round(profile.label.backgroundOpacity * 100)} min={0} max={100} step={1} formatValue={(value) => `${value}%`} onChange={(value) => updatePresentationProfile(profile.id, (current) => ({ ...current, label: { ...current.label, backgroundOpacity: value / 100 } }))} />
        </>,
      },
      ...(profile.target.variant === "surface-fill" || referenceProfile ? [] : [{
        id: `map-state-classes-${profile.id}`,
        label: "Классы состояния",
        description: "нормализованные фасеты онтологии",
        group: "Таргеты",
        content: <>
          <small className="catalog-map-inspector__note">Цвета назначены семантическим классам после нормализации данных. Здесь нет названий provider-статусов и привязки к транспорту.</small>
          {profile.styles.map((style) => {
            const classLabels = profile.classes.filter((item) => item.styleId === style.id).map((item) => item.label);
            const label = classLabels.length ? classLabels.join(" · ") : style.id;
            return <div className="catalog-map-inspector__style" key={style.id}>
              <ControlRow label={label}><ColorField label={`Цвет: ${label}`} value={style.color} onChange={(color) => updatePresentationStyle(profile.id, style.id, { color })} /></ControlRow>
              <RangeControl label={`${label}: прозрачность`} value={Math.round(style.opacity * 100)} min={0} max={100} step={1} formatValue={(value) => `${value}%`} onChange={(value) => updatePresentationStyle(profile.id, style.id, { opacity: value / 100 })} />
            </div>;
          })}
        </>,
      }]),
    ];
    }),
    {
      id: "map-grid",
      label: "Сетка и LOD",
      description: "first adapter control",
      group: "Слои",
      content: <>
        <small className="catalog-map-inspector__note">Сетка размещается над поверхностью и меняет шаг по высоте камеры.</small>
        <Checker checked={mapSettings.gridVisible} label="3D-сетка" onChange={(gridVisible) => updateMapSettings({ gridVisible })} />
        <Checker checked={mapSettings.gridLodEnabled} label="LOD по высоте камеры" onChange={(gridLodEnabled) => updateMapSettings({ gridLodEnabled })} />
        <RangeControl label="Высота над поверхностью" value={mapSettings.gridHeightMeters} min={0} max={1000} formatValue={(value) => `${value} м`} onChange={(gridHeightMeters) => updateMapSettings({ gridHeightMeters })} />
        <RangeControl label="LOD 1: до высоты" value={mapSettings.gridLod1MaxHeightKm} min={1} max={50} formatValue={(value) => `${value} км`} onChange={(gridLod1MaxHeightKm) => updateMapSettings({ gridLod1MaxHeightKm })} />
        <RangeControl label="LOD 1: шаг" value={mapSettings.gridLod1StepKm} min={1} max={10} formatValue={(value) => `${value} км`} onChange={(gridLod1StepKm) => updateMapSettings({ gridLod1StepKm })} />
        <RangeControl label="LOD 2: до высоты" value={mapSettings.gridLod2MaxHeightKm} min={10} max={200} formatValue={(value) => `${value} км`} onChange={(gridLod2MaxHeightKm) => updateMapSettings({ gridLod2MaxHeightKm })} />
        <RangeControl label="LOD 2: шаг" value={mapSettings.gridLod2StepKm} min={1} max={25} formatValue={(value) => `${value} км`} onChange={(gridLod2StepKm) => updateMapSettings({ gridLod2StepKm })} />
        <RangeControl label="LOD 3: шаг" value={mapSettings.gridLod3StepKm} min={5} max={100} formatValue={(value) => `${value} км`} onChange={(gridLod3StepKm) => updateMapSettings({ gridLod3StepKm })} />
        <RangeControl label="Радиус сетки" value={mapSettings.gridRadiusKm} min={5} max={150} formatValue={(value) => `${value} км`} onChange={(gridRadiusKm) => updateMapSettings({ gridRadiusKm })} />
        <ControlRow label="Цвет линий"><ColorField label="Цвет линий сетки" value={mapSettings.gridColor} onChange={(gridColor) => updateMapSettings({ gridColor })} /></ControlRow>
        <RangeControl label="Толщина линий" value={mapSettings.gridLineWidth} min={1} max={8} formatValue={(value) => `${value} px`} onChange={(gridLineWidth) => updateMapSettings({ gridLineWidth })} />
        <RangeControl label="Прозрачность сетки" value={mapSettings.gridOpacity} min={0} max={100} formatValue={(value) => `${value}%`} onChange={(gridOpacity) => updateMapSettings({ gridOpacity })} />
        <Checker checked={mapSettings.gridDotsEnabled} label="Точки в пересечениях" onChange={(gridDotsEnabled) => updateMapSettings({ gridDotsEnabled })} />
        <RangeControl label="Размер точки" value={mapSettings.gridDotsSize} min={2} max={28} formatValue={(value) => `${value} px`} onChange={(gridDotsSize) => updateMapSettings({ gridDotsSize })} />
        <ControlRow label="Цвет точек"><ColorField label="Цвет точек сетки" value={mapSettings.gridDotsColor} onChange={(gridDotsColor) => updateMapSettings({ gridDotsColor })} /></ControlRow>
        <RangeControl label="Прозрачность точек" value={mapSettings.gridDotsOpacity} min={0} max={100} formatValue={(value) => `${value}%`} onChange={(gridDotsOpacity) => updateMapSettings({ gridDotsOpacity })} />
      </>,
    },
    {
      id: "map-camera-animation",
      label: "Анимация камеры",
      description: "geodesic spiral survey",
      group: "Камера",
      content: <>
        <Checker checked={animationModeEnabled} label="Режим анимации" onChange={setAnimationMode} />
        {animationModeEnabled ? <>
          <small className="catalog-map-inspector__note">Стартовая точка берётся из текущей позиции камеры. Камера смотрит почти в надир, а маршрут ждёт текущие tiles перед продолжением. Движение идёт по региональной геодезической спирали WGS84 до выбранного радиуса.</small>
          <InspectorSelectField
            label="Профиль покрытия"
            value={spiralPresetId}
            options={spiralPresetOptions}
            disabled={spiralRunning}
            onChange={selectSpiralPreset}
          />
          <small className="catalog-map-inspector__note">У текущего OSM Buildings подтверждено {OSM_BUILDINGS_OBSERVED_BAND_COUNT} иерархических bands. Десять профилей управляют высотой и покрытием; фактический LOD Cesium выбирает по SSE, viewport и расстоянию.</small>
          <ControlRow label="Слои прохода"><small>Imagery · Terrain · OSM Buildings</small></ControlRow>
          <RangeControl
            label="Высота над землёй"
            value={logarithmicControlValue(spiralHeightMeters)}
            min={logarithmicControlValue(10)}
            max={logarithmicControlValue(100_000)}
            step={0.01}
            disabled={spiralRunning}
            formatValue={(value) => formatMetricDistance(10 ** value)}
            onChange={(value) => {
              setSpiralPresetId("custom");
              setSpiralHeightMeters(valueFromLogarithmicControl(value));
            }}
          />
          <RangeControl
            label="Скорость камеры"
            value={logarithmicControlValue(spiralSpeedMetersPerSecond)}
            min={logarithmicControlValue(1)}
            max={logarithmicControlValue(5_000)}
            step={0.01}
            disabled={spiralRunning}
            formatValue={(value) => formatMetricSpeed(10 ** value)}
            onChange={(value) => {
              setSpiralPresetId("custom");
              setSpiralSpeedMetersPerSecond(valueFromLogarithmicControl(value));
            }}
          />
          <RangeControl
            label="Шаг спирали"
            value={logarithmicControlValue(spiralPitchMetersPerTurn)}
            min={logarithmicControlValue(20)}
            max={logarithmicControlValue(100_000)}
            step={0.01}
            disabled={spiralRunning}
            formatValue={(value) => formatMetricDistance(10 ** value)}
            onChange={(value) => {
              setSpiralPresetId("custom");
              setSpiralPitchMetersPerTurn(valueFromLogarithmicControl(value));
            }}
          />
          <RangeControl
            label="Радиус прохода"
            value={logarithmicControlValue(spiralTargetRadiusMeters)}
            min={logarithmicControlValue(1_000)}
            max={logarithmicControlValue(250_000)}
            step={0.01}
            disabled={spiralRunning}
            formatValue={(value) => formatMetricDistance(10 ** value)}
            onChange={(value) => {
              setSpiralPresetId("custom");
              setSpiralTargetRadiusMeters(valueFromLogarithmicControl(value));
            }}
          />
          <small className="catalog-map-inspector__note">Расчётное движение без ожидания сети: {formatDuration(cameraSurveySpiralDistance(spiralTargetRadiusMeters, spiralPitchMetersPerTurn) / spiralSpeedMetersPerSecond)}. Tile waits и автоматическое сужение шага под viewport увеличат фактическое время.</small>
          {!spiralCanStart && !spiralRunning ? <small className="catalog-map-inspector__note" role="status">Подготовка: imagery — {providerStateLabel[providerStatus.imagery]}, terrain — {providerStateLabel[providerStatus.terrain]}, OSM Buildings — {providerStateLabel[providerStatus.buildings]}, TileCache — {spiralTileCacheReady ? "готов" : gatewayHealth?.cache?.atCapacity ? "заполнен" : gatewayCheckState === "checking" ? "проверяется" : "недоступен для записи"}.</small> : null}
          <Button variant="secondary" shape="pill" onClick={toggleSpiralAnimation} disabled={!spiralRunning && !spiralCanStart}>{spiralRunning ? "Остановить" : "Запустить режим анимации"}</Button>
          {spiralRunning ? <small className="catalog-map-inspector__note">Камера движется от исходной точки. Выключение режима, уход со страницы или reload остановят сессию.</small> : null}
          {spiralMessage ? <small className="catalog-map-inspector__note catalog-map-inspector__note--error" role="status">{spiralMessage}</small> : null}
        </> : null}
      </>,
    },
    {
      id: "map-cache",
      label: "TileCache",
      description: "Platform Map Gateway",
      group: "Хранение",
      content: <>
        <small className="catalog-map-inspector__note">Общий persistent cache Platform: он не принадлежит приложению, странице или пользователю.</small>
        <Checker checked={mapSettings.cacheEnabled} label="Кэшировать live-данные" onChange={setCacheEnabled} />
        <small className="catalog-map-inspector__note">Cache hit отдаётся как есть; новый tile записывается только при miss.</small>
        <Checker checked={mapSettings.cacheNoOverwrite} disabled={!mapSettings.cacheEnabled} label="Не перезаписывать уже полученное" onChange={setCacheNoOverwrite} />
        <ControlRow className="catalog-map-inspector__cache-fact" label="Режим"><span>{mapSettings.cacheEnabled ? mapSettings.cacheNoOverwrite ? "Live + Cache · append-only" : "Live + Cache · обновление разрешено" : "Live без persistent cache"}</span></ControlRow>
        <ControlRow className="catalog-map-inspector__cache-fact" label="Хранилище"><span>Platform Map Gateway</span></ControlRow>
        <ControlRow className="catalog-map-inspector__cache-fact" label="Подключение"><span>{gatewayEndpoint ?? "runtime profile · не проверено"}</span></ControlRow>
        <ControlRow className="catalog-map-inspector__cache-fact" label="Записано"><span>{liveCacheSummary}</span></ControlRow>
        <ControlRow className="catalog-map-inspector__cache-fact" label="Политика"><span>{gatewayHealth?.cache?.writePolicy ?? "append-only · проверяется"}</span></ControlRow>
        <Button variant="secondary" shape="pill" icon={<Icon name="refresh" />} onClick={refreshCurrentViewport} disabled={!mapSettings.cacheEnabled || cacheRefresh}> {cacheRefresh ? "Обновляем viewport…" : "Обновить текущий viewport"}</Button>
        <Button variant="secondary" shape="pill" icon={<Icon name="refresh" />} onClick={() => void verifyGateway()} disabled={gatewayCheckState === "checking"}>{gatewayCheckState === "checking" ? "Проверяем Gateway…" : "Проверить подключение"}</Button>
        <small className="catalog-map-inspector__note">Live Cache: {liveCacheSummary} · {gatewayHealth?.cache?.mode ?? "проверяется"}</small>
        {transportDiagnostic ? <small className="catalog-map-inspector__note catalog-map-inspector__note--error" role="alert">{transportDiagnostic}</small> : null}
        {gatewayHealthAge ? <small className="catalog-map-inspector__note">{gatewayHealthAge}</small> : null}
        <small className="catalog-map-inspector__note">{mapSettings.cacheEnabled ? mapSettings.cacheNoOverwrite ? "Новые miss дописываются; при заполнении объёма Gateway продолжит live-маршрут без удаления прежних tiles." : "Новые запросы этого Application могут явно обновлять уже записанные tiles." : "Real-time: provider остаётся официальным, чтение и запись persistent cache выключены."}</small>
        {gatewayHealth?.cache?.atCapacity ? <small className="catalog-map-inspector__note catalog-map-inspector__note--error" role="alert">TileCache заполнен: новые tiles показываются live, но не записываются. Существующий cache не удаляется.</small> : null}
        {gatewayCheckState === "error" || gatewayCheckState === "stale" ? <small className="catalog-map-inspector__note catalog-map-inspector__note--error" role="alert">{gatewayCheckError}</small> : null}
      </>,
    },
    {
      id: "map-selection",
      label: "Выбранная сущность",
      description: "selection contract",
      group: "Данные",
      content: <>
        <ControlRow label="Сущность"><strong>{selected?.title ?? "Нет выбора"}</strong></ControlRow>
        <ControlRow label="Тип"><span>{selected?.kind ?? "—"}{selected?.status ? ` · ${selected.status}` : ""}</span></ControlRow>
      </>,
    },
  ];

  return (
    <div
      ref={workspaceRef}
      className={`catalog-map-fixture${expanded ? " catalog-map-fixture--expanded" : ""}`}
      style={{ "--catalog-map-height": `${mapHeight}px` } as CSSProperties}
      aria-label="Map Page Cesium adapter"
    >
      <Suspense fallback={<div className="catalog-map-fixture__loading">Загрузка карты…</div>}>
        <CesiumMapRenderer
          key={rendererRevision}
          ref={mapRendererRef}
          onSelect={handleSelect}
          onGatewayHealth={handleRendererGatewayHealth}
          onProviderStatus={setProviderStatus}
          onCameraChange={handleCameraChange}
          onCacheRefreshConsumed={handleCacheRefreshConsumed}
          onReadyChange={setMapRendererReady}
          onSpiralStateChange={handleSpiralStateChange}
          initialCamera={mapCamera ?? undefined}
          presentation={presentation}
          runtimeBindings={[...primaryRuntimeBindings, ...referenceRuntimeBindings]}
          presentationProfiles={presentationProfiles}
          presentationFilters={rendererPresentationFilters}
        />
      </Suspense>

      <div className="catalog-map-fixture__actions">
        <IconButton label="Настройки карты" aria-pressed={inspectorOpen} data-active={inspectorOpen || undefined} onClick={() => setInspectorOpen(true)}><Icon name="settings" /></IconButton>
        <IconButton label="Слои карты" aria-pressed={layersOpen} data-active={layersOpen || undefined} onClick={toggleLayersWindow}><Icon name="grid" /></IconButton>
        {features.toolbar ? <IconButton label="Toolbar" aria-pressed={toolbarOpen} data-active={toolbarOpen || undefined} onClick={() => setToolbarOpen((value) => !value)}><Icon name="panel" /></IconButton> : null}
        {features.assistant ? <IconButton label="Assistant" aria-pressed={assistantOpen} data-active={assistantOpen || undefined} onClick={() => setAssistantOpen((value) => !value)}><Icon name="apps" /></IconButton> : null}
      </div>

      {layersOpen ? (
        <WorkspaceWindow
          boundsRef={workspaceRef}
          rect={layersWindowRect}
          onRectChange={setLayersWindowRect}
          maximized={layersWindowMaximized}
          onMaximizedChange={setLayersWindowMaximized}
          onActivate={activateLayersWindow}
          onClose={() => {
            setLayersOpen(false);
            setLayersWindowActive(false);
          }}
          title="Слои карты"
          active={layersWindowActive}
          zIndex={layersWindowZIndex}
          minWidth={320}
          minHeight={360}
          className="catalog-map-fixture__layers catalog-map-fixture__map-glass-window"
          aria-label="Настройки слоёв карты"
        >
          <div className="catalog-map-fixture__layers-content">
            <div className="catalog-map-fixture__provider">
              <strong>Cesium World Imagery</strong>
              <small>официальный live provider · imagery: {providerStateLabel[providerStatus.imagery]} · terrain: {providerStateLabel[providerStatus.terrain]}</small>
            </div>
            {Object.entries(providerStatus.errors).map(([provider, error]) => <small key={provider} className="catalog-map-inspector__note catalog-map-inspector__note--error" role="alert">{provider}: {error}</small>)}
            <Checker checked={mapSettings.terrainEnabled} label="Terrain" onChange={(terrainEnabled) => updateMapSettings({ terrainEnabled })} />
            <Checker checked={mapSettings.buildingsVisible} label="3D здания" onChange={(buildingsVisible) => updateMapSettings({ buildingsVisible })} />
            <Checker checked={mapSettings.gridVisible} label="Планетарная сетка" onChange={(gridVisible) => updateMapSettings({ gridVisible })} />
            <small className="catalog-map-inspector__note">Live Cache: {liveCacheSummary}</small>
            {transportDiagnostic ? <small className="catalog-map-inspector__note catalog-map-inspector__note--error" role="alert">{transportDiagnostic}</small> : null}
            {gatewayHealthAge ? <small className="catalog-map-inspector__note">{gatewayHealthAge}</small> : null}
            {gatewayCheckState === "error" || gatewayCheckState === "stale" ? <small className="catalog-map-inspector__note catalog-map-inspector__note--error" role="alert">{gatewayCheckError}</small> : null}
            <Checker className="catalog-map-fixture__cache-toggle" checked={mapSettings.cacheEnabled} label="Кэшировать live-данные" onChange={setCacheEnabled} />
            <Checker className="catalog-map-fixture__cache-toggle" checked={mapSettings.cacheNoOverwrite} disabled={!mapSettings.cacheEnabled} label="Не перезаписывать cache" onChange={setCacheNoOverwrite} />
          </div>
        </WorkspaceWindow>
      ) : null}

      {toolbarOpen ? (
        <div className="catalog-map-fixture__toolbar" aria-label="Map toolbar">
          <Dropdown
            placement="top-start"
            width={320}
            minWidth={240}
            offset={10}
            surfaceRole="menu"
            surfaceClassName="catalog-map-fixture__objects-menu nodedc-map-glass"
            trigger={({ open, toggle, setTriggerRef, surfaceId }) => (
              <IconButton ref={setTriggerRef} label="Объекты" aria-controls={surfaceId} aria-expanded={open} aria-pressed={open} data-active={open || undefined} onClick={toggle}><Icon name="target" /></IconButton>
            )}
          >
            {({ close }) => (
              <div className="catalog-map-fixture__objects-menu-list">
                <div className="catalog-map-fixture__objects-menu-head">
                  <strong>Объекты</strong>
                  <small>{objectLayerCount} {objectLayerCount === 1 ? "группа" : "групп"}</small>
                </div>
                {presentationSummaries.map((summary) => {
                  const state = subjectStates[summary.bindingId];
                  const visibleCount = filteredTargets.filter((target) => target.bindingId === summary.bindingId && target.renderable).length;
                  const visible = state?.visible !== false;
                  const hasControls = hasSubjectWindowControls(summary.profile);
                  return (
                    <div
                      className="catalog-map-fixture__objects-menu-item"
                      key={summary.bindingId}
                      data-visible={visible || undefined}
                      data-open={hasControls && state?.window.open || undefined}
                    >
                      <button
                        type="button"
                        role={hasControls ? "menuitem" : "menuitemcheckbox"}
                        aria-checked={hasControls ? undefined : visible}
                        className="catalog-map-fixture__objects-menu-toggle"
                        onClick={() => {
                          if (hasControls) {
                            openSubjectWindow(summary.bindingId);
                            close();
                            return;
                          }
                          toggleSubjectVisibility(summary.bindingId);
                        }}
                      >
                        <span>{summary.displayName}</span>
                        <small>{visible ? `на карте: ${visibleCount}` : `слой скрыт · ${summary.total} объектов`}</small>
                      </button>
                    </div>
                  );
                })}
                {referenceObjectSummaries.map(({ layer, displayName, total }) => (
                  <div
                    className="catalog-map-fixture__objects-menu-item"
                    key={layer.id}
                    data-visible={layer.visible || undefined}
                  >
                    <button
                      type="button"
                      role="menuitemcheckbox"
                      aria-checked={layer.visible}
                      className="catalog-map-fixture__objects-menu-toggle"
                      onClick={() => setReferenceLayers((current) => current.map((candidate) => (
                        candidate.id === layer.id ? { ...candidate, visible: !candidate.visible } : candidate
                      )))}
                    >
                      <span>{displayName}</span>
                      <small>{layer.visible ? `на карте: ${total}` : `слой скрыт · ${total} объектов`}</small>
                    </button>
                  </div>
                ))}
                {!objectLayerCount ? <small className="catalog-map-fixture__objects-menu-empty">Нет подключённых объектов.</small> : null}
              </div>
            )}
          </Dropdown>
          <IconButton label="Обзор объектов" onClick={() => mapRendererRef.current?.fitRuntimeEntities(visibleTargetEntityIds)}><Icon name="globe" /></IconButton>
          <IconButton label="Поиск"><Icon name="search" /></IconButton>
        </div>
      ) : null}

      {presentationSummaries.map((summary) => {
        const state = subjectStates[summary.bindingId];
        if (!state?.window.open || !hasSubjectWindowControls(summary.profile)) return null;
        const visibleCount = filteredTargets.filter((target) => target.bindingId === summary.bindingId && target.renderable).length;
        return (
          <WorkspaceWindow
            key={summary.bindingId}
            boundsRef={workspaceRef}
            rect={state.window.rect}
            onRectChange={(rect) => updateSubjectState(summary.bindingId, (current) => ({
              ...current,
              window: { ...current.window, rect },
            }))}
            maximized={state.window.maximized}
            onMaximizedChange={(maximized) => updateSubjectState(summary.bindingId, (current) => ({
              ...current,
              window: { ...current.window, maximized },
            }))}
            onActivate={() => openSubjectWindow(summary.bindingId)}
            onClose={() => closeSubjectWindow(summary.bindingId)}
            title={summary.displayName}
            subtitle={`${summary.total} всего · ${visibleCount} на карте`}
            active={activeSubjectBindingId === summary.bindingId}
            zIndex={state.window.zIndex}
            minWidth={240}
            minHeight={220}
            autoHeight
            className="catalog-map-fixture__subject-window catalog-map-fixture__map-glass-window"
          >
            <div className="catalog-map-fixture__target-filters">
              <section aria-label={`${summary.displayName}: фильтры и счётчики`}>
                <div className="catalog-map-fixture__target-filter-list">
                  {summary.profile.facets.filter((facet) => facet.counter || facet.filterable).flatMap((facet) => (
                    facet.values.map((item) => {
                        const active = state.filters[facet.field]?.includes(item.value) ?? false;
                        const rowId = `${summary.bindingId}:${facet.field}:${item.value}`;
                        const expanded = Boolean(expandedFacetRows[rowId]);
                        const matchingEntities = selectable.filter((entity) => (
                          entity.bindingId === summary.bindingId
                          && mapFactMatchesFilters(
                            entity.fact,
                            summary.profile,
                            {
                              [summary.bindingId]: {
                                visible: true,
                                facets: { [facet.field]: [item.value] },
                              },
                            },
                            summary.bindingId,
                          )
                        ));
                        return (
                          <div className="catalog-map-fixture__target-filter-branch" key={`${facet.field}:${item.value}`}>
                            <div className="catalog-map-fixture__target-filter-row" data-active={active || undefined}>
                              <button
                                type="button"
                                className="catalog-map-fixture__target-filter-body"
                                aria-pressed={active}
                                disabled={!facet.filterable}
                                onClick={() => togglePresentationFilter(summary.bindingId, facet.field, item.value)}
                              >
                                <span className="catalog-map-fixture__target-filter-label">{item.label}</span>
                                <span className="catalog-map-fixture__target-filter-count">{summary.counts[facet.field]?.[item.value] ?? 0}</span>
                              </button>
                              <button
                                type="button"
                                className="catalog-map-fixture__target-filter-expander"
                                aria-label={`${expanded ? "Свернуть" : "Развернуть"} ${item.label}`}
                                aria-expanded={expanded}
                                onClick={() => setExpandedFacetRows((current) => ({ ...current, [rowId]: !current[rowId] }))}
                              >
                                <Icon name="chevron-right" size={14} />
                              </button>
                            </div>
                            {expanded ? (
                              <div className="catalog-map-fixture__target-filter-children">
                                {matchingEntities.map((entity) => (
                                  <button
                                    type="button"
                                    className="catalog-map-fixture__target-filter-entity"
                                    key={entity.id}
                                    data-selected={entity.id === selectedId || undefined}
                                    onClick={() => handleSelectAndFocus(entity.id)}
                                  >
                                    <span>{entity.title}</span>
                                    {entity.status ? <small>{entity.status}</small> : null}
                                  </button>
                                ))}
                                {!matchingEntities.length ? <small>Нет объектов в группе.</small> : null}
                              </div>
                            ) : null}
                          </div>
                        );
                    })
                  ))}
                </div>
              </section>
            </div>
          </WorkspaceWindow>
        );
      })}

      {subjectCardOpen && selectedSubjectCard ? (
        <WorkspaceWindow
          boundsRef={workspaceRef}
          rect={subjectCardRect}
          onRectChange={setSubjectCardRect}
          maximized={subjectCardMaximized}
          onMaximizedChange={setSubjectCardMaximized}
          onActivate={() => {
            const nextZIndex = Math.max(subjectCardZIndex, layersWindowZIndex, ...Object.values(subjectStates).map((state) => state.window.zIndex)) + 1;
            setSubjectCardZIndex(nextZIndex);
            setSubjectCardActive(true);
            setLayersWindowActive(false);
            setActiveSubjectBindingId(undefined);
          }}
          onClose={() => {
            setSubjectCardOpen(false);
            setSubjectCardActive(false);
          }}
          title={selectedSubjectCard.title}
          subtitle={selectedSubjectCard.sourceId}
          active={subjectCardActive}
          zIndex={subjectCardZIndex}
          minWidth={320}
          minHeight={320}
          className="catalog-map-fixture__subject-card catalog-map-fixture__map-glass-window"
          aria-label={`Карточка объекта: ${selectedSubjectCard.title}`}
        >
          <div className="catalog-map-subject-card">
            <div className="catalog-map-subject-card__tabs">
              <SegmentedControl
                value={selectedSubjectCard.tabs.some((tab) => tab.id === subjectCardTabId) ? subjectCardTabId : selectedSubjectCard.defaultTabId}
                items={selectedSubjectCard.tabs.map((tab) => ({ value: tab.id, label: tab.label }))}
                label="Разделы карточки объекта"
                onChange={setSubjectCardTabId}
              />
            </div>
            {selectedSubjectCard.tabs.filter((tab) => tab.id === (
              selectedSubjectCard.tabs.some((candidate) => candidate.id === subjectCardTabId)
                ? subjectCardTabId
                : selectedSubjectCard.defaultTabId
            )).map((tab) => (
              <div key={tab.id} className="catalog-map-subject-card__tab-panel" role="tabpanel">
                {tab.empty ? <div className="catalog-map-subject-card__empty">{tab.emptyMessage}</div> : null}
                {tab.sections.map((section) => (
                  <section key={section.id} className="catalog-map-subject-card__section" aria-labelledby={`subject-card-${tab.id}-${section.id}`}>
                    <h3 id={`subject-card-${tab.id}-${section.id}`}>{section.label}</h3>
                    {section.rows.length ? (
                      <dl>
                        {section.rows.map((row) => (
                          <div key={row.key} className="catalog-map-subject-card__row">
                            <dt>{row.label}</dt>
                            <dd>{row.value}</dd>
                          </div>
                        ))}
                      </dl>
                    ) : null}
                    {section.readings.length ? (
                      <div className="catalog-map-subject-card__readings">
                        {section.readings.map((reading) => (
                          <div className="catalog-map-subject-card__reading" key={reading.id}>
                            <span>{reading.label}</span>
                            <strong>{reading.value}</strong>
                            {reading.observedAt ? <time dateTime={reading.observedAt}>{new Date(reading.observedAt).toLocaleString("ru-RU")}</time> : null}
                          </div>
                        ))}
                      </div>
                    ) : null}
                  </section>
                ))}
              </div>
            ))}
          </div>
        </WorkspaceWindow>
      ) : null}

      {assistantOpen ? <div className="catalog-map-fixture__assistant"><strong>NODE.DC Assistant</strong><span>Контекст выбранной сущности готов к передаче.</span></div> : null}
      <button type="button" className="catalog-map-fixture__resize" aria-label="Изменить высоту карты" onPointerDown={startResize}><span /></button>

      <Window
        open={inspectorOpen && Boolean(features.inspector)}
        title="Настройки карты"
        subtitle="MAP / draggable inspector"
        placement="end"
        draggable
        closeOnBackdrop={false}
        lockBodyScroll={false}
        trapFocus={false}
        className="catalog-map-fixture__map-settings-window"
        onClose={() => setInspectorOpen(false)}
      >
        <Inspector
          sections={inspectorSections}
          openSections={inspectorOpenSections}
          singleOpen
          onOpenSectionsChange={setInspectorOpenSections}
        />
      </Window>
    </div>
  );
});
