import { forwardRef, lazy, Suspense, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, type CSSProperties, type PointerEvent } from "react";
import { Button, Checker, ColorField, ControlRow, GlassSurface, Icon, IconButton, Inspector, RangeControl, Window } from "@nodedc/ui-react";
import type {
  CameraSpiralState,
  CesiumMapRendererHandle,
  MapCameraView,
  MapGatewayHealth,
  MapPresentation,
  MapProviderStatus,
} from "./CesiumMapRenderer.js";
import { mapRuntimeEntityId, useMapDataProductRuntime } from "./useMapDataProductRuntime.js";
import sceneFixture from "../../../registry/fixtures/map/map-operational-v0.1.json";

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 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;

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}`;
}

/**
 * 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;
  dataProductId: string;
  slotId: string;
  delivery: "snapshot+patch";
  semanticTypes: string[];
  fieldProjection: string[];
};

export type MapPageLayout = {
  schemaVersion: 1;
  pageId: "map";
  settings: MapPageSettings;
  mapHeight: number;
  camera: MapCameraView;
  pinBindings: MapPinBinding[];
  dataProductBindings: MapDataProductBinding[];
  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: 100,
  imageryHue: 0,
  imageryAlpha: 100,
  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: 0.82,
  buildingsDetail: 16,
  imageryBrightness: 100,
  imageryContrast: 100,
  imagerySaturation: 100,
  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: sceneFixture.viewport.center[0],
  latitude: sceneFixture.viewport.center[1],
  height: sceneFixture.viewport.range,
  heading: (sceneFixture.viewport.heading * Math.PI) / 180,
  pitch: (sceneFixture.viewport.pitch * Math.PI) / 180,
  roll: 0,
};

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

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

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)} м/с`;

function spiralStopMessage(reason: CameraSpiralState["reason"]) {
  if (reason === "spiral_extent_limit") return "Режим остановлен у безопасной границы геодезического маршрута.";
  if (reason === "terrain_sampling_error") return "Режим остановлен: не удалось получить высоту terrain для следующего участка.";
  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 fixtureSelectable = useMemo(() => [
    ...sceneFixture.scene.movingObjects.map((entity) => ({ id: entity.id, title: entity.label.text, kind: entity.objectType, status: entity.status })),
    ...sceneFixture.scene.stations.map((entity) => ({ id: entity.id, title: entity.label.text, kind: `${entity.stationType} station`, status: undefined })),
  ], []);
  const [selectedId, setSelectedId] = useState(sceneFixture.selection.entityId ?? fixtureSelectable[0]?.id);
  const [inspectorOpen, setInspectorOpen] = useState(false);
  const [layersOpen, setLayersOpen] = 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 [spiralRunning, setSpiralRunning] = useState(false);
  const [spiralHeightMeters, setSpiralHeightMeters] = useState(1500);
  const [spiralSpeedMetersPerSecond, setSpiralSpeedMetersPerSecond] = useState(250);
  const [spiralPitchMetersPerTurn, setSpiralPitchMetersPerTurn] = useState(10_000);
  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 ?? []);
  // 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 runtimeBindings = useMapDataProductRuntime({
    applicationId,
    pageId,
    bindings: dataProductBindings,
    enabled: Boolean(applicationId && pageId),
  });
  const selectable = useMemo(() => [
    ...fixtureSelectable,
    ...runtimeBindings.flatMap((binding) => binding.facts.map((fact) => {
      const attributes = fact.attributes;
      const label = [attributes.label, attributes.name, attributes.title, attributes.subject_id]
        .find((value) => typeof value === "string" && value.trim());
      const status = typeof attributes.status === "string" ? attributes.status : undefined;
      return {
        id: mapRuntimeEntityId(binding.bindingId, fact),
        title: typeof label === "string" ? label : fact.sourceId,
        kind: fact.semanticType,
        status,
      };
    })),
  ], [fixtureSelectable, runtimeBindings]);
  // 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 selected = selectable.find((entity) => entity.id === selectedId) ?? selectable[0];
  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 setAnimationMode = (enabled: boolean) => {
    setAnimationModeEnabled(enabled);
    setSpiralMessage(null);
    if (!enabled) {
      mapRendererRef.current?.stopSpiralAnimation("mode_disabled");
      setSpiralRunning(false);
    }
  };

  const toggleSpiralAnimation = () => {
    if (spiralRunning) {
      mapRendererRef.current?.stopSpiralAnimation("stopped");
      return;
    }
    const started = mapRendererRef.current?.startSpiralAnimation({
      heightAboveGroundMeters: spiralHeightMeters,
      speedMetersPerSecond: spiralSpeedMetersPerSecond,
      pitchMetersPerTurn: spiralPitchMetersPerTurn,
    }) ?? false;
    if (!started) setSpiralMessage("Карта ещё не готова к запуску режима анимации.");
  };

  useImperativeHandle(ref, () => ({
    getLayout: () => ({
      schemaVersion: 1,
      pageId: "map",
      settings: mapSettings,
      mapHeight: Math.round(mapHeight),
      camera: mapRendererRef.current?.getCameraView() ?? mapCameraRef.current ?? mapCamera,
      pinBindings,
      dataProductBindings,
    }),
  }), [dataProductBindings, mapCamera, mapHeight, mapSettings, pinBindings]);

  const handleSelect = useCallback((entityId: string) => {
    if (!selectable.some((entity) => entity.id === entityId)) return;
    setSelectedId(entityId);
  }, [selectable]);

  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) 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();
      }
    };
  }, [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 })} />
      </>,
    },
    {
      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">Стартовая точка берётся из текущей позиции камеры. Высота считается над terrain; при догрузке следующего участка маршрут ждёт данные, а не подменяет поверхность нулём. Движение идёт по региональной геодезической спирали WGS84 до 250 км от старта.</small>
          <RangeControl
            label="Высота над землёй"
            value={logarithmicControlValue(spiralHeightMeters)}
            min={logarithmicControlValue(10)}
            max={logarithmicControlValue(100_000)}
            step={0.01}
            disabled={spiralRunning}
            formatValue={(value) => formatMetricDistance(10 ** value)}
            onChange={(value) => 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) => 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) => setSpiralPitchMetersPerTurn(valueFromLogarithmicControl(value))}
          />
          <Button variant="secondary" shape="pill" onClick={toggleSpiralAnimation} disabled={!mapRendererReady}>{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
      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={runtimeBindings}
        />
      </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={() => setLayersOpen((value) => !value)}><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 ? (
        <GlassSurface className="catalog-map-fixture__layers" tone="strong" radius="card" padding="sm" aria-label="Настройки слоёв карты">
          <div className="catalog-map-fixture__layers-head"><strong>Слои карты</strong><IconButton label="Закрыть слои" onClick={() => setLayersOpen(false)}><Icon name="close" /></IconButton></div>
          <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} />
        </GlassSurface>
      ) : null}

      {toolbarOpen ? (
        <div className="catalog-map-fixture__toolbar" aria-label="Map toolbar">
          <IconButton label="Обзор"><Icon name="globe" /></IconButton>
          <IconButton label="Поиск"><Icon name="search" /></IconButton>
        </div>
      ) : 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}
        onClose={() => setInspectorOpen(false)}
      >
        <Inspector sections={inspectorSections} defaultOpen={["map-base"]} singleOpen />
      </Window>
    </div>
  );
});
