import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { drawBaseMap } from "./canvas/drawBaseMap";
import { drawComplexPlan } from "./canvas/drawComplexPlan";
import { drawCoverage } from "./canvas/drawCoverage";
import { drawHeatmap } from "./canvas/drawHeatmap";
import { drawPlanWorks } from "./canvas/drawPlanWorks";
import { drawRequests } from "./canvas/drawRequests";
import { drawStations } from "./canvas/drawStations";
import { drawSwath } from "./canvas/drawSwath";
import { drawTiles } from "./canvas/drawTiles";
import { drawTracks } from "./canvas/drawTracks";
import { createMapProjection, normalizeLon } from "./geometry/mapProjection";
import { createPolygonSpatialIndex, hitTestAllPolygons, hitTestPolygons, queryPolygonsByBBox } from "./geometry/spatialIndex";
import type { Tgu2DMapLayersState } from "./model/mapLayerTypes";
import type { GeoPoint, MapLine, MapMarker, MapPolygon, MapSize, MapViewState, ScreenPoint, Tgu2DMapScene } from "./model/mapTypes";
import type { Tgu2DMapSelection } from "./model/mapSelectionTypes";

type Tgu2DMapViewProps = {
  scene: Tgu2DMapScene;
  layers: Tgu2DMapLayersState;
  redrawToken: number;
  onObjectSelect: (selection?: Tgu2DMapSelection) => void;
  onRequestsAtPoint?: (polygons: MapPolygon[]) => void;
  hoveredRequestPolygonId?: string;
  selectedRequestPolygonId?: string;
  /** Внешняя подсветка объекта (контур съёмки или линия сброса) по его id — «как будто кликнули на карте». */
  selectedObjectId?: string;
  /** Отмеченные объекты (постоянная подсветка независимо от выбора) — напр. слоты, отобранные под бронь. */
  markedObjectIds?: ReadonlySet<string>;
  /** Приглушённые объекты — напр. слоты, конфликтующие с текущим выбором (подсказка «недоступно»). */
  dimmedObjectIds?: ReadonlySet<string>;
  /** Точка, на которую центрировать карту; перецентрирование срабатывает при смене focusToken. */
  focusPoint?: GeoPoint;
  focusToken?: number;
  /**
   * Включает выбор объекта под курсором: если в точке клика лежит несколько объектов
   * (например, заявка и маршрут съёмки), их список отдаётся через onObjectsAtPoint вместо
   * «молчаливого» выбора верхнего по z-индексу. Также делает кликабельными линии сброса.
   */
  enableObjectChooser?: boolean;
  /** Несколько объектов под точкой клика — список передаётся наверх (выбор делает оператор справа). */
  onObjectsAtPoint?: (selections: Tgu2DMapSelection[]) => void;
  /**
   * Жест «выбор полосы» мышью: Shift+перетаскивание по слотам. Карта остаётся «глупой» — отдаёт
   * наверх ключи слотов под курсором (id вида `slot:${key}`), а группировку в полосу и диапазон
   * считает владелец выбора (TguSlotsTab). onStripAnchor — Shift+нажатие на слот (начало жеста),
   * onStripDrag — движение (текущие слоты под курсором), onStripCommit — отпускание (конец жеста).
   * Если жест не задан или Shift зажат над пустым местом — работает обычная панорама.
   */
  onStripAnchor?: (slotKeysUnderCursor: string[]) => void;
  onStripDrag?: (slotKeysUnderCursor: string[]) => void;
  onStripCommit?: () => void;
  pickMode?: boolean;
  onPickPoint?: (point: { lat: number; lon: number }) => void;
  targets?: Tgu2DMapTarget[];
  pickTarget?: Tgu2DMapTarget;
  selectedTargetId?: string;
  onTargetSelect?: (id: string) => void;
  drawMode?: boolean;
  draftPolygon?: GeoPoint[];
  onDrawAddPoint?: (point: GeoPoint) => void;
};

export type Tgu2DMapTarget = {
  id: string;
  name: string;
  lat: number;
  lon: number;
};

type DragState = {
  startX: number;
  startY: number;
  centerAtStart: {
    lon: number;
    lat: number;
  };
};

type TooltipState = {
  x: number;
  y: number;
  title: string;
  subtitle: string;
};

const INITIAL_VIEW: MapViewState = {
  centerLon: 45,
  centerLat: 30,
  zoom: 2
};

export function Tgu2DMapView({
  scene,
  layers,
  redrawToken,
  onObjectSelect,
  onRequestsAtPoint,
  hoveredRequestPolygonId,
  selectedRequestPolygonId,
  selectedObjectId,
  markedObjectIds,
  dimmedObjectIds,
  focusPoint,
  focusToken,
  enableObjectChooser = false,
  onObjectsAtPoint,
  onStripAnchor,
  onStripDrag,
  onStripCommit,
  pickMode = false,
  onPickPoint,
  targets = [],
  pickTarget,
  selectedTargetId,
  onTargetSelect,
  drawMode = false,
  draftPolygon = [],
  onDrawAddPoint
}: Tgu2DMapViewProps) {
  const wrapperRef = useRef<HTMLDivElement | null>(null);
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
  const dragRef = useRef<DragState | undefined>(undefined);
  const hasDraggedRef = useRef(false);
  // Активен жест «выбор полосы» (Shift+drag по слотам): пока true — панорама и обычный клик подавлены.
  const stripGestureRef = useRef(false);
  const hoverRef = useRef<MapPolygon | undefined>(undefined);
  const hoverMarkerRef = useRef<MapMarker | undefined>(undefined);
  const selectedMarkerRef = useRef<MapMarker | undefined>(undefined);
  const [size, setSize] = useState<MapSize>({ width: 1000, height: 560 });
  const [view, setView] = useState<MapViewState>(INITIAL_VIEW);
  const [tileVersion, setTileVersion] = useState(0);
  const handleTileLoad = useCallback(() => setTileVersion((v) => v + 1), []);
  const [tooltip, setTooltip] = useState<TooltipState>();
  // Гео-координаты под курсором — показываем по центру внизу карты.
  const [cursorGeo, setCursorGeo] = useState<GeoPoint>();
  // Линейка: включается кнопкой в правом нижнем углу; в активном режиме клик по карте
  // добавляет вершину замера, а наложение рисует ломаную с накопленным расстоянием.
  const [rulerActive, setRulerActive] = useState(false);
  const [rulerPoints, setRulerPoints] = useState<GeoPoint[]>([]);
  const selectedPolygonRef = useRef<MapPolygon | undefined>(undefined);
  const selectedLineRef = useRef<MapLine | undefined>(undefined);
  const activePolygons = useMemo(
    () => scene.polygons.filter((polygon) => layers[polygon.layer]),
    [scene.polygons, layers]
  );
  const polygonIndex = useMemo(() => createPolygonSpatialIndex(activePolygons), [activePolygons]);
  const activeMarkers = useMemo(
    () => scene.markers.filter((marker) => layers[marker.layer]),
    [scene.markers, layers]
  );
  const projection = useMemo(() => createMapProjection(view, size), [view, size]);
  const visiblePolygons = useMemo(() => {
    // When the canvas is wider than the world tile, the entire globe is visible —
    // bbox filtering would produce a nonsensical range, so skip it.
    // When the viewport crosses the antimeridian, topLeft.lon ends up east of
    // bottomRight.lon — same situation, show everything.
    const topLeft = projection.unproject({ x: 0, y: 0 });
    const bottomRight = projection.unproject({ x: size.width, y: size.height });
    if (size.width >= projection.worldSize || topLeft.lon >= bottomRight.lon) {
      return activePolygons;
    }

    return queryPolygonsByBBox(polygonIndex, {
      minLon: topLeft.lon,
      maxLon: bottomRight.lon,
      minLat: Math.min(topLeft.lat, bottomRight.lat),
      maxLat: Math.max(topLeft.lat, bottomRight.lat)
    });
  }, [activePolygons, polygonIndex, projection, size.height, size.width]);

  useEffect(() => {
    const element = wrapperRef.current;
    if (!element) return;

    const resizeObserver = new ResizeObserver(() => {
      setSize({
        width: Math.max(320, element.clientWidth),
        height: Math.max(320, element.clientHeight)
      });
    });
    resizeObserver.observe(element);
    setSize({
      width: Math.max(320, element.clientWidth),
      height: Math.max(320, element.clientHeight)
    });

    return () => resizeObserver.disconnect();
  }, []);

  // Перецентрирование по внешней команде (клик по включению в редакторе): центрируем карту
  // на объекте и подтягиваем зум, чтобы он был хорошо виден. Триггер — смена focusToken,
  // поэтому повторный клик по тому же включению снова возвращает карту к объекту.
  useEffect(() => {
    if (!focusPoint || focusToken === undefined) return;
    setView((current) => ({
      centerLon: focusPoint.lon,
      centerLat: Math.max(-82, Math.min(82, focusPoint.lat)),
      zoom: Math.max(current.zoom, 4.5)
    }));
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [focusToken]);

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;

    const frameId = window.requestAnimationFrame(() => {
      const dpr = Math.min(2, window.devicePixelRatio || 1);
      canvas.width = Math.floor(size.width * dpr);
      canvas.height = Math.floor(size.height * dpr);
      canvas.style.width = `${size.width}px`;
      canvas.style.height = `${size.height}px`;

      const context = canvas.getContext("2d");
      if (!context) return;

      context.setTransform(dpr, 0, 0, dpr, 0, 0);
      context.clearRect(0, 0, size.width, size.height);
      drawTiles(context, view, projection, size, handleTileLoad);
      drawBaseMap(context, projection);

      if (layers.heatmap) {
        drawHeatmap(context, visiblePolygons.filter((polygon) => polygon.layer === "heatmap"), projection, size);
      }
      if (layers.requests) {
        drawRequests(context, visiblePolygons.filter((polygon) => polygon.layer === "requests"), projection, size);
      }
      if (layers.swath) {
        drawSwath(context, visiblePolygons.filter((polygon) => polygon.layer === "swath"), projection, size);
      }
      if (layers.complexPlan) {
        drawComplexPlan(context, visiblePolygons.filter((polygon) => polygon.layer === "complexPlan"), projection, size);
      }
      // След покрытия (вкладка «Слоты») — отдельный пасс поверх полос слотов; кольца приходят
      // через scene.coverageRings (тумблер на стороне TguSlotsTab очищает их, когда слой выключен).
      if (scene.coverageRings && scene.coverageRings.length > 0) {
        drawCoverage(context, scene.coverageRings, projection, size);
      }
      if (layers.planWorks) {
        drawPlanWorks(context, visiblePolygons.filter((polygon) => polygon.layer === "planWorks"), projection, size);
      }
      if (layers.tracks) {
        drawTracks(context, scene.lines.filter((line) => layers[line.layer]), projection);
      }
      if (layers.stations) {
        drawStations(context, scene.markers.filter((marker) => marker.layer === "stations"), projection);
      }
    });

    return () => window.cancelAnimationFrame(frameId);
  }, [handleTileLoad, layers, projection, redrawToken, scene.coverageRings, scene.lines, scene.markers, size, tileVersion, view, visiblePolygons]);

  const screenPointFromEvent = useCallback((event: React.PointerEvent | React.MouseEvent): ScreenPoint => {
    const bounds = wrapperRef.current?.getBoundingClientRect();
    return {
      x: bounds ? event.clientX - bounds.left : 0,
      y: bounds ? event.clientY - bounds.top : 0
    };
  }, []);

  const updateHover = useCallback(
    (event: React.PointerEvent) => {
      const screenPoint = screenPointFromEvent(event);
      setCursorGeo(projection.unproject(screenPoint));
      const marker = hitTestMarkers(activeMarkers, screenPoint, projection);
      const polygon = marker ? undefined : hitTestPolygons(polygonIndex, projection.unproject(screenPoint));

      if (marker?.id === hoverMarkerRef.current?.id && polygon?.id === hoverRef.current?.id) return;

      hoverMarkerRef.current = marker;
      hoverRef.current = polygon;

      if (!marker && !polygon) {
        setTooltip(undefined);
        return;
      }

      setTooltip({
        x: screenPoint.x + 12,
        y: screenPoint.y + 12,
        title: marker ? marker.label : polygon!.name,
        subtitle: marker ? "Станция" : layerTitle(polygon!.layer)
      });
    },
    [activeMarkers, polygonIndex, projection, screenPointFromEvent]
  );

  // Ключи слотов (id `slot:${key}`) под экранной точкой — для жеста выбора полосы. Брони вне окна
  // (`booked:${key}`) и прочие слои не берём: полосой выбираем только кандидатов-слоты.
  const slotKeysAtPoint = useCallback(
    (screenPoint: ScreenPoint): string[] => {
      const geoPoint = projection.unproject(screenPoint);
      const keys: string[] = [];
      for (const polygon of hitTestAllPolygons(polygonIndex, geoPoint)) {
        if (polygon.id.startsWith("slot:")) keys.push(polygon.id.slice("slot:".length));
      }
      return keys;
    },
    [polygonIndex, projection]
  );

  const onPointerDown = (event: React.PointerEvent) => {
    // Нажатие на кнопки управления картой (зум, линейка, «Сброс») не должно запускать перетаскивание
    // и захват указателя: setPointerCapture на контейнере иначе перехватывает жест, и кнопки внутри
    // карты перестают «кликаться» (родная причина того, что +/−/□ и «Сброс» не срабатывали).
    if ((event.target as HTMLElement).closest("button")) return;
    const screenPoint = screenPointFromEvent(event);
    // Shift+нажатие на слот → жест выбора полосы (вместо панорамы). Над пустым местом Shift не мешает.
    if (event.shiftKey && onStripAnchor) {
      const keys = slotKeysAtPoint(screenPoint);
      if (keys.length > 0) {
        event.preventDefault(); // не даём Shift+drag начать выделение текста / перетаскивание
        stripGestureRef.current = true;
        hasDraggedRef.current = false;
        dragRef.current = undefined;
        wrapperRef.current?.setPointerCapture(event.pointerId);
        onStripAnchor(keys);
        return;
      }
    }
    hasDraggedRef.current = false;
    dragRef.current = {
      startX: screenPoint.x,
      startY: screenPoint.y,
      centerAtStart: { lon: view.centerLon, lat: view.centerLat }
    };
    wrapperRef.current?.setPointerCapture(event.pointerId);
  };

  const DRAG_THRESHOLD = 4;

  const onPointerMove = (event: React.PointerEvent) => {
    if (stripGestureRef.current) {
      // Жест выбора полосы: тянем — отдаём слоты под курсором наверх (там клампится по границе полосы).
      hasDraggedRef.current = true;
      const screenPoint = screenPointFromEvent(event);
      setCursorGeo(projection.unproject(screenPoint));
      onStripDrag?.(slotKeysAtPoint(screenPoint));
      return;
    }
    const drag = dragRef.current;
    if (!drag) {
      updateHover(event);
      return;
    }

    const screenPoint = screenPointFromEvent(event);
    setCursorGeo(projection.unproject(screenPoint));
    const dx = screenPoint.x - drag.startX;
    const dy = screenPoint.y - drag.startY;
    if (!hasDraggedRef.current && Math.sqrt(dx * dx + dy * dy) > DRAG_THRESHOLD) {
      hasDraggedRef.current = true;
    }
    const dragProjection = createMapProjection(
      {
        centerLon: drag.centerAtStart.lon,
        centerLat: drag.centerAtStart.lat,
        zoom: view.zoom
      },
      size
    );
    const nextCenter = dragProjection.unproject({ x: size.width / 2 - dx, y: size.height / 2 - dy });
    setView((current) => ({
      ...current,
      centerLon: nextCenter.lon,
      centerLat: Math.max(-82, Math.min(82, nextCenter.lat))
    }));
  };

  const onPointerUp = (event: React.PointerEvent) => {
    if (stripGestureRef.current) {
      stripGestureRef.current = false;
      // hasDraggedRef оставляем true — иначе следующий onClick выбрал бы слот «сверху» жеста.
      hasDraggedRef.current = true;
      wrapperRef.current?.releasePointerCapture(event.pointerId);
      onStripCommit?.();
      return;
    }
    dragRef.current = undefined;
    wrapperRef.current?.releasePointerCapture(event.pointerId);
  };

  // Линейка: переключаем режим; при выключении замер очищаем.
  const toggleRuler = () => {
    setRulerActive((active) => {
      if (active) setRulerPoints([]);
      return !active;
    });
  };

  // Применить выбор конкретного объекта (из меню выбора либо при единственном попадании):
  // выставляем внутреннюю подсветку и сообщаем наверх через onObjectSelect.
  const applyEditorHit = useCallback((hit: EditorHit) => {
    selectedPolygonRef.current = hit.kind === "polygon" ? hit.polygon : undefined;
    selectedMarkerRef.current = hit.kind === "marker" ? hit.marker : undefined;
    selectedLineRef.current = hit.kind === "line" ? hit.line : undefined;
    onObjectSelect(hit.selection);
  }, [onObjectSelect]);

  // Клик в редакторе: собираем ВСЕ интерактивные объекты под точкой (заявки, маршруты съёмки,
  // сбросы, станции). 0 → смотрим на полосы обзора; 1 → выбираем сразу;
  // ≥2 → отдаём список наверх (оператор выбирает нужный в правой панели).
  const handleEditorClick = useCallback((screenPoint: ScreenPoint) => {
    const geoPoint = projection.unproject(screenPoint);
    const hits: EditorHit[] = [];
    // Полосы обзора собираем отдельно: они почти всегда лежат под кликом и не должны
    // превращать выбор заявки/маршрута в список. Их список показываем, только когда под точкой
    // нет другого интерактивного объекта, а самих полос несколько (наложение соседних витков).
    const swathHits: EditorHit[] = [];

    const marker = hitTestMarkers(activeMarkers, screenPoint, projection);
    if (marker) hits.push({ kind: "marker", marker, selection: markerToSelection(marker) });

    for (const polygon of hitTestAllPolygons(polygonIndex, geoPoint)) {
      if (polygon.layer === "swath") {
        swathHits.push({ kind: "polygon", polygon, selection: polygonToSelection(polygon) });
        continue;
      }
      if (
        polygon.layer !== "requests" &&
        polygon.layer !== "planWorks" &&
        polygon.layer !== "complexPlan" &&
        polygon.layer !== "heatmap"
      ) {
        continue;
      }
      hits.push({ kind: "polygon", polygon, selection: polygonToSelection(polygon) });
    }

    const dropLines = scene.lines.filter((line) => line.kind === "drop" && layers[line.layer]);
    for (const line of hitTestLines(dropLines, screenPoint, projection)) {
      hits.push({ kind: "line", line, selection: lineToSelection(line) });
    }

    if (hits.length >= 2 && onObjectsAtPoint) {
      // Несколько интерактивных объектов — показываем список. В него добавляем и полосы обзора,
      // лежащие под точкой, чтобы их данные тоже были видны. Порядок: маршрут → заявка → полоса.
      const combined = [...hits, ...swathHits].sort((a, b) => editorHitRank(a) - editorHitRank(b));
      // Внутреннюю подсветку снимаем (её ведёт правая панель через selectedObjectId).
      selectedPolygonRef.current = undefined;
      selectedMarkerRef.current = undefined;
      selectedLineRef.current = undefined;
      onObjectsAtPoint(combined.map((hit) => hit.selection));
      return;
    }
    if (hits.length === 1) {
      // Единственный интерактивный объект — выбираем сразу (полосы обзора лежат под кликом почти
      // всегда, поэтому ради одного объекта список не показываем — иначе любой клик стал бы списком).
      applyEditorHit(hits[0]);
      return;
    }

    // Другого интерактивного объекта под точкой нет — работаем с полосами обзора:
    // ≥2 (наложение витков) → список справа; ровно одна → выбираем её.
    if (swathHits.length >= 2 && onObjectsAtPoint) {
      selectedPolygonRef.current = undefined;
      selectedMarkerRef.current = undefined;
      selectedLineRef.current = undefined;
      onObjectsAtPoint(swathHits.map((hit) => hit.selection));
      return;
    }
    if (swathHits.length === 1) {
      applyEditorHit(swathHits[0]);
      return;
    }

    // Совсем ничего интерактивного — выбираем верхний прочий полигон либо снимаем выбор.
    const fallback = hitTestPolygons(polygonIndex, geoPoint);
    selectedPolygonRef.current = fallback;
    selectedMarkerRef.current = undefined;
    selectedLineRef.current = undefined;
    onObjectSelect(fallback ? polygonToSelection(fallback) : undefined);
  }, [activeMarkers, applyEditorHit, layers, onObjectSelect, onObjectsAtPoint, polygonIndex, projection, scene.lines]);

  const onClick = (event: React.MouseEvent) => {
    if (hasDraggedRef.current) return;
    const screenPoint = screenPointFromEvent(event);
    if (rulerActive) {
      // Режим линейки: клик добавляет вершину замера, выбор объектов не трогаем.
      setRulerPoints((points) => [...points, projection.unproject(screenPoint)]);
      return;
    }
    if (drawMode) {
      onDrawAddPoint?.(projection.unproject(screenPoint));
      return;
    }
    if (pickMode) {
      onPickPoint?.(projection.unproject(screenPoint));
      onObjectSelect(undefined);
      return;
    }

    if (enableObjectChooser) {
      handleEditorClick(screenPoint);
      return;
    }

    const marker = hitTestMarkers(activeMarkers, screenPoint, projection);
    if (marker) {
      selectedMarkerRef.current = marker;
      selectedPolygonRef.current = undefined;
      onRequestsAtPoint?.([]);
      onObjectSelect({
        type: "marker",
        id: marker.id,
        name: marker.label,
        layer: marker.layer,
        stationNumber: marker.stationNumber,
        lat: marker.point.lat,
        lon: marker.point.lon,
        altitude: marker.altitude,
        elevationMin: marker.elevationMin,
        elevationMax: marker.elevationMax
      });
      return;
    }

    const geoPoint = projection.unproject(screenPoint);
    const polygon = hitTestPolygons(polygonIndex, geoPoint);

    if (polygon?.layer === "requests" && onRequestsAtPoint) {
      const allRequests = hitTestAllPolygons(polygonIndex, geoPoint).filter((p) => p.layer === "requests");
      onRequestsAtPoint(allRequests);
      selectedPolygonRef.current = undefined;
      selectedMarkerRef.current = undefined;
      onObjectSelect(undefined);
      return;
    }

    onRequestsAtPoint?.([]);
    selectedPolygonRef.current = polygon;
    selectedMarkerRef.current = undefined;

    if (!polygon) {
      onObjectSelect(undefined);
      return;
    }

    onObjectSelect({
      type: "polygon",
      id: polygon.id,
      name: polygon.name,
      layer: polygon.layer,
      planId: polygon.planId,
      spacecraftId: polygon.spacecraftId,
      requestId: polygon.requestId,
      surveyType: polygon.surveyType,
      status: polygon.status,
      modeId: polygon.modeId
    });
  };

  const onWheel = (event: React.WheelEvent) => {
    event.preventDefault();
    const screenPoint = screenPointFromEvent(event);
    const before = projection.unproject(screenPoint);
    const nextZoom = Math.max(0.8, Math.min(8, view.zoom + (event.deltaY < 0 ? 0.35 : -0.35)));
    const nextProjection = createMapProjection({ ...view, zoom: nextZoom }, size);
    const after = nextProjection.unproject(screenPoint);

    setView((current) => ({
      ...current,
      zoom: nextZoom,
      centerLon: current.centerLon + before.lon - after.lon,
      centerLat: Math.max(-82, Math.min(82, current.centerLat + before.lat - after.lat))
    }));
  };

  // Внутренние ref-выделения (клик по карте) могут указывать на объект, которого уже нет в сцене —
  // например, контур съёмки, скрытый при входе в режим правки (sceneModes исключает редактируемый
  // режим). SVG-оверлей рисуется на каждый рендер из ref, поэтому без сверки с живой сценой старая
  // подсветка «висит» поверх карты, пока не кликнешь в другое место. Рисуем только то, что есть в сцене.
  const selectedPolygon =
    selectedPolygonRef.current && activePolygons.some((p) => p.id === selectedPolygonRef.current!.id)
      ? selectedPolygonRef.current
      : undefined;
  const selectedLine =
    selectedLineRef.current && scene.lines.some((l) => l.id === selectedLineRef.current!.id)
      ? selectedLineRef.current
      : undefined;
  const selectedMarker =
    selectedMarkerRef.current && activeMarkers.some((m) => m.id === selectedMarkerRef.current!.id)
      ? selectedMarkerRef.current
      : undefined;
  const externalSelectedPolygon = useMemo(() => {
    const id = selectedRequestPolygonId ?? selectedObjectId;
    return id ? activePolygons.find((p) => p.id === id) : undefined;
  }, [selectedRequestPolygonId, selectedObjectId, activePolygons]);
  // Подсветка линии (сброс/трасса) по тому же selectedObjectId — линии живут вне activePolygons.
  const externalSelectedLine = useMemo(
    () => (selectedObjectId ? scene.lines.find((line) => line.id === selectedObjectId) : undefined),
    [selectedObjectId, scene.lines]
  );
  // Линии под подсветкой: выбранная кликом на карте (внутренняя) и заданная извне (по selectedObjectId).
  const overlayLines = uniqueLines([selectedLine, externalSelectedLine]);
  const externalHoveredPolygon = useMemo(
    () => hoveredRequestPolygonId ? activePolygons.find((p) => p.id === hoveredRequestPolygonId) : undefined,
    [hoveredRequestPolygonId, activePolygons]
  );
  const overlayPolygons = uniquePolygons([externalSelectedPolygon, selectedPolygon, externalHoveredPolygon, hoverRef.current]);
  // Отмеченные объекты (напр. отобранные под бронь слоты) — постоянная подсветка под обычным выбором.
  const markedPolygons = useMemo(
    () =>
      markedObjectIds && markedObjectIds.size > 0
        ? activePolygons.filter((p) => markedObjectIds.has(p.id))
        : [],
    [markedObjectIds, activePolygons]
  );
  // Приглушённые (конфликтующие с выбором) объекты — рисуем под отмеченными/выбранными.
  const dimmedPolygons = useMemo(
    () =>
      dimmedObjectIds && dimmedObjectIds.size > 0
        ? activePolygons.filter((p) => dimmedObjectIds.has(p.id))
        : [],
    [dimmedObjectIds, activePolygons]
  );
  const overlayMarkers = uniqueMarkers([selectedMarker, hoverMarkerRef.current]);

  // Накопленное расстояние (км) от первой вершины линейки до каждой — по дуге большого круга.
  const rulerCumKm = useMemo(() => {
    const out: number[] = [];
    let acc = 0;
    rulerPoints.forEach((point, i) => {
      if (i > 0) acc += haversineKm(rulerPoints[i - 1], point);
      out.push(acc);
    });
    return out;
  }, [rulerPoints]);
  const rulerTotalKm = rulerCumKm.length > 0 ? rulerCumKm[rulerCumKm.length - 1] : 0;

  return (
    <div
      className={`tgu-2d-map ${pickMode || drawMode || rulerActive ? "is-picking" : ""}`}
      ref={wrapperRef}
      onClick={onClick}
      onPointerDown={onPointerDown}
      onPointerMove={onPointerMove}
      onPointerUp={onPointerUp}
      onPointerCancel={onPointerUp}
      onPointerLeave={() => {
        dragRef.current = undefined;
        hoverRef.current = undefined;
        hoverMarkerRef.current = undefined;
        setTooltip(undefined);
        setCursorGeo(undefined);
      }}
      onWheel={onWheel}
    >
      <canvas className="tgu-2d-map__canvas" ref={canvasRef} />
      <svg className="tgu-2d-map__overlay" width={size.width} height={size.height} aria-hidden="true">
        {dimmedPolygons.flatMap((polygon) => {
          const raw = polygon.points.map((point) => projection.project(point));
          const pts = unwrapX(raw, projection.worldSize);
          return ([0, projection.worldSize, -projection.worldSize] as const).flatMap((dx) => {
            const visible = pts.some((p) => {
              const x = p.x + dx;
              return x >= -64 && x <= size.width + 64 && p.y >= -64 && p.y <= size.height + 64;
            });
            if (!visible) return [];
            return (
              <polygon
                key={`dimmed:${polygon.id}:${dx}`}
                points={pts.map((p) => `${p.x + dx},${p.y}`).join(" ")}
                className="is-dimmed"
              />
            );
          });
        })}
        {markedPolygons.flatMap((polygon) => {
          const raw = polygon.points.map((point) => projection.project(point));
          const pts = unwrapX(raw, projection.worldSize);
          return ([0, projection.worldSize, -projection.worldSize] as const).flatMap((dx) => {
            const visible = pts.some((p) => {
              const x = p.x + dx;
              return x >= -64 && x <= size.width + 64 && p.y >= -64 && p.y <= size.height + 64;
            });
            if (!visible) return [];
            return (
              <polygon
                key={`marked:${polygon.id}:${dx}`}
                points={pts.map((p) => `${p.x + dx},${p.y}`).join(" ")}
                className="is-marked"
              />
            );
          });
        })}
        {overlayPolygons.flatMap((polygon) => {
          const raw = polygon.points.map((point) => projection.project(point));
          const pts = unwrapX(raw, projection.worldSize);
          const cls =
            polygon.id === selectedPolygon?.id || polygon.id === externalSelectedPolygon?.id
              ? "is-selected"
              : "is-hovered";
          return ([0, projection.worldSize, -projection.worldSize] as const).flatMap((dx) => {
            const visible = pts.some((p) => {
              const x = p.x + dx;
              return x >= -64 && x <= size.width + 64 && p.y >= -64 && p.y <= size.height + 64;
            });
            if (!visible) return [];
            return (
              <polygon
                key={`${polygon.id}:${dx}`}
                points={pts.map((p) => `${p.x + dx},${p.y}`).join(" ")}
                className={cls}
              />
            );
          });
        })}
        {overlayLines.flatMap((line) => {
          if (line.points.length < 2) return [];
          const pts = unwrapX(line.points.map((point) => projection.project(point)), projection.worldSize);
          return ([0, projection.worldSize, -projection.worldSize] as const).flatMap((dx) => {
            const visible = pts.some((p) => {
              const x = p.x + dx;
              return x >= -64 && x <= size.width + 64 && p.y >= -64 && p.y <= size.height + 64;
            });
            if (!visible) return [];
            return (
              <polyline
                key={`line:${line.id}:${dx}`}
                points={pts.map((p) => `${p.x + dx},${p.y}`).join(" ")}
                className="is-selected"
                fill="none"
              />
            );
          });
        })}
        {overlayMarkers.map((marker) => {
          const pt = projection.project(marker.point);
          const cls = marker.id === selectedMarker?.id ? "is-selected" : "is-hovered";
          return <circle key={marker.id} cx={pt.x} cy={pt.y} r={10} className={cls} />;
        })}
        {targets.map((t) => {
          const screen = projection.project(t);
          const isSelected = t.id === selectedTargetId;
          return (
            <g
              className={`tgu-2d-map__target tgu-2d-map__target--confirmed${isSelected ? " is-selected" : ""}`}
              key={t.id}
              transform={`translate(${screen.x} ${screen.y})`}
              onClick={(e) => { e.stopPropagation(); onTargetSelect?.(t.id); }}
              style={{ cursor: "pointer" }}
            >
              <circle r="6" />
              <line x1="-10" x2="10" y1="0" y2="0" />
              <line x1="0" x2="0" y1="-10" y2="10" />
              <text x="10" y="-8">{t.name}</text>
            </g>
          );
        })}
        {pickTarget && (() => {
          const screen = projection.project(pickTarget);
          return (
            <g className="tgu-2d-map__target tgu-2d-map__target--pending" key="pick" transform={`translate(${screen.x} ${screen.y})`}>
              <circle r="6" />
              <line x1="-10" x2="10" y1="0" y2="0" />
              <line x1="0" x2="0" y1="-10" y2="10" />
            </g>
          );
        })()}
        {draftPolygon.length > 0 && (() => {
          const pts = unwrapX(draftPolygon.map((point) => projection.project(point)), projection.worldSize);
          const path = pts.map((p) => `${p.x},${p.y}`).join(" ");
          return (
            <g className="tgu-2d-map__draft">
              {pts.length >= 2 && (
                pts.length >= 3
                  ? <polygon className="tgu-2d-map__draft-fill" points={path} />
                  : <polyline className="tgu-2d-map__draft-line" points={path} />
              )}
              {pts.map((p, i) => (
                <circle key={i} cx={p.x} cy={p.y} r={4} className="tgu-2d-map__draft-vertex" />
              ))}
            </g>
          );
        })()}
        {rulerPoints.length > 0 && (() => {
          const pts = unwrapX(rulerPoints.map((point) => projection.project(point)), projection.worldSize);
          return (
            <g className="tgu-2d-map__ruler">
              {pts.length >= 2 && (
                <polyline
                  className="tgu-2d-map__ruler-line"
                  points={pts.map((p) => `${p.x},${p.y}`).join(" ")}
                  fill="none"
                />
              )}
              {pts.map((p, i) => (
                <g key={i}>
                  <circle cx={p.x} cy={p.y} r={4} className="tgu-2d-map__ruler-vertex" />
                  <text x={p.x + 8} y={p.y - 8} className="tgu-2d-map__ruler-label">
                    {i === 0 ? "0" : formatDistanceKm(rulerCumKm[i])}
                  </text>
                </g>
              ))}
            </g>
          );
        })()}
      </svg>
      {tooltip && (
        <div className="tgu-2d-map__tooltip" style={{ left: tooltip.x, top: tooltip.y }}>
          <div>{tooltip.title}</div>
          <span>{tooltip.subtitle}</span>
        </div>
      )}
      {cursorGeo && <div className="tgu-2d-map__coords">{formatLatLon(cursorGeo)}</div>}
      <div className="tgu-2d-map__attribution">© <a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener noreferrer">OpenStreetMap</a> contributors · © <a href="https://carto.com/attributions" target="_blank" rel="noopener noreferrer">CARTO</a></div>
      {rulerActive && (
        <div className="tgu-2d-map__ruler-total">
          <span className="tgu-2d-map__ruler-total-label">Линейка</span>
          <span className="tgu-2d-map__ruler-total-value">
            {rulerPoints.length < 2 ? "кликайте по карте" : formatDistanceKm(rulerTotalKm)}
          </span>
          {rulerPoints.length > 0 && (
            <button
              type="button"
              className="tgu-2d-map__ruler-reset"
              onClick={(event) => {
                event.stopPropagation();
                setRulerPoints([]);
              }}
            >
              Сброс
            </button>
          )}
        </div>
      )}
      <div className="tgu-2d-map__zoom">
        <button
          type="button"
          className={`tgu-2d-map__zoom-ruler${rulerActive ? " is-active" : ""}`}
          title="Линейка — измерение расстояния по карте"
          aria-pressed={rulerActive}
          onClick={(event) => {
            event.stopPropagation();
            toggleRuler();
          }}
        >
          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
            <rect x="2.5" y="8.5" width="19" height="7" rx="1" />
            <path d="M6.5 8.5v3M10 8.5v4M13.5 8.5v3M17 8.5v4" />
          </svg>
        </button>
        <button type="button" onClick={(event) => { event.stopPropagation(); setView((current) => ({ ...current, zoom: Math.min(8, current.zoom + 0.5) })); }}>+</button>
        <button type="button" onClick={(event) => { event.stopPropagation(); setView((current) => ({ ...current, zoom: Math.max(0.8, current.zoom - 0.5) })); }}>-</button>
        <button type="button" title="Вернуть обзор" onClick={(event) => { event.stopPropagation(); setView(INITIAL_VIEW); }}>□</button>
      </div>
    </div>
  );
}

/** Расстояние между двумя гео-точками по дуге большого круга (haversine), км. */
function haversineKm(a: GeoPoint, b: GeoPoint): number {
  const R = 6371;
  const toRad = Math.PI / 180;
  const dLat = (b.lat - a.lat) * toRad;
  const dLon = (b.lon - a.lon) * toRad;
  const lat1 = a.lat * toRad;
  const lat2 = b.lat * toRad;
  const h = Math.sin(dLat / 2) ** 2 + Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLon / 2) ** 2;
  return 2 * R * Math.asin(Math.min(1, Math.sqrt(h)));
}

/** Человекочитаемое расстояние: метры до 1 км, иначе километры. */
function formatDistanceKm(km: number): string {
  if (km < 1) return `${(km * 1000).toFixed(0)} м`;
  if (km < 100) return `${km.toFixed(2)} км`;
  return `${km.toFixed(1)} км`;
}

/** Подпись координат под курсором: широта/долгота с полушариями. */
function formatLatLon(point: GeoPoint): string {
  const lat = point.lat;
  const lon = normalizeLon(point.lon);
  const latStr = `${Math.abs(lat).toFixed(4)}° ${lat >= 0 ? "с.ш." : "ю.ш."}`;
  const lonStr = `${Math.abs(lon).toFixed(4)}° ${lon >= 0 ? "в.д." : "з.д."}`;
  return `${latStr}, ${lonStr}`;
}

function layerTitle(layer: MapPolygon["layer"]): string {
  switch (layer) {
    case "planWorks":
      return "Работы плана";
    case "swath":
      return "Полоса обзора";
    case "complexPlan":
      return "Комплексный план";
    case "heatmap":
      return "Ячейка приоритета";
    case "tracks":
      return "Трасса КА";
    case "requests":
      return "Заявка на съёмку";
  }
}

function unwrapX(points: { x: number; y: number }[], worldSize: number): { x: number; y: number }[] {
  if (points.length === 0) return [];
  const out: { x: number; y: number }[] = [points[0]];
  let prevX = points[0].x;
  for (let i = 1; i < points.length; i++) {
    let x = points[i].x;
    while (x - prevX > worldSize / 2) x -= worldSize;
    while (prevX - x > worldSize / 2) x += worldSize;
    out.push({ x, y: points[i].y });
    prevX = x;
  }
  return out;
}

function hitTestMarkers(
  markers: MapMarker[],
  screenPoint: ScreenPoint,
  projection: ReturnType<typeof createMapProjection>,
  hitRadius = 10
): MapMarker | undefined {
  let best: MapMarker | undefined;
  let bestDist = hitRadius;
  for (const marker of markers) {
    const pt = projection.project(marker.point);
    const dist = Math.sqrt((pt.x - screenPoint.x) ** 2 + (pt.y - screenPoint.y) ** 2);
    if (dist <= bestDist) {
      bestDist = dist;
      best = marker;
    }
  }
  return best;
}

function uniqueMarkers(markers: Array<MapMarker | undefined>): MapMarker[] {
  const result: MapMarker[] = [];
  const ids = new Set<string>();
  for (const marker of markers) {
    if (!marker || ids.has(marker.id)) continue;
    ids.add(marker.id);
    result.push(marker);
  }
  return result;
}

function uniquePolygons(polygons: Array<MapPolygon | undefined>): MapPolygon[] {
  const result: MapPolygon[] = [];
  const ids = new Set<string>();
  for (const polygon of polygons) {
    if (!polygon || ids.has(polygon.id)) continue;
    ids.add(polygon.id);
    result.push(polygon);
  }
  return result;
}

function uniqueLines(lines: Array<MapLine | undefined>): MapLine[] {
  const result: MapLine[] = [];
  const ids = new Set<string>();
  for (const line of lines) {
    if (!line || ids.has(line.id)) continue;
    ids.add(line.id);
    result.push(line);
  }
  return result;
}

/** Один объект-кандидат под курсором: несёт исходный объект (для подсветки) и его выделение. */
type EditorHit =
  | { kind: "marker"; marker: MapMarker; selection: Tgu2DMapSelection }
  | { kind: "polygon"; polygon: MapPolygon; selection: Tgu2DMapSelection }
  | { kind: "line"; line: MapLine; selection: Tgu2DMapSelection };

// Порядок объектов в списке выбора под точкой: сначала маршрут съёмки, затем заявка,
// в конце — полоса обзора (как фон). Остальные объекты — между ними.
function editorHitRank(hit: EditorHit): number {
  if (hit.kind === "marker") return 0; // станции / маркеры КА
  if (hit.kind === "line") return 3; // сбросы
  switch (hit.polygon.layer) {
    case "planWorks": // маршрут съёмки
    case "complexPlan":
      return 1;
    case "requests": // заявка на съёмку
      return 2;
    case "heatmap":
      return 4;
    case "swath": // полоса обзора — последней
      return 5;
    default:
      return 6;
  }
}

function markerToSelection(marker: MapMarker): Tgu2DMapSelection {
  return {
    type: "marker",
    id: marker.id,
    name: marker.label,
    layer: marker.layer,
    stationNumber: marker.stationNumber,
    lat: marker.point.lat,
    lon: marker.point.lon,
    altitude: marker.altitude,
    elevationMin: marker.elevationMin,
    elevationMax: marker.elevationMax
  };
}

function polygonToSelection(polygon: MapPolygon): Tgu2DMapSelection {
  return {
    type: "polygon",
    id: polygon.id,
    name: polygon.name,
    layer: polygon.layer,
    planId: polygon.planId,
    spacecraftId: polygon.spacecraftId,
    requestId: polygon.requestId,
    surveyType: polygon.surveyType,
    status: polygon.status,
    modeId: polygon.modeId,
    revolution: polygon.revolution,
    importance: polygon.importance,
    cellNum: polygon.cellNum
  };
}

function lineToSelection(line: MapLine): Tgu2DMapSelection {
  return {
    type: "line",
    id: line.id,
    name: line.name ?? "Сброс",
    layer: line.layer,
    kind: line.kind,
    modeId: line.modeId
  };
}

/** Линии, проходящие в пределах допуска (px) от точки клика, отсортированные по близости. */
function hitTestLines(
  lines: MapLine[],
  screenPoint: ScreenPoint,
  projection: ReturnType<typeof createMapProjection>,
  tolerance = 7
): MapLine[] {
  const worldSize = projection.worldSize;
  const hits: Array<{ line: MapLine; dist: number }> = [];
  for (const line of lines) {
    if (line.points.length < 2) continue;
    const pts = unwrapX(line.points.map((point) => projection.project(point)), worldSize);
    let best = Number.POSITIVE_INFINITY;
    for (const dx of [0, worldSize, -worldSize]) {
      for (let i = 1; i < pts.length; i++) {
        const d = distToSegment(
          screenPoint,
          { x: pts[i - 1].x + dx, y: pts[i - 1].y },
          { x: pts[i].x + dx, y: pts[i].y }
        );
        if (d < best) best = d;
      }
    }
    if (best <= tolerance) hits.push({ line, dist: best });
  }
  return hits.sort((a, b) => a.dist - b.dist).map((h) => h.line);
}

function distToSegment(p: ScreenPoint, a: ScreenPoint, b: ScreenPoint): number {
  const dx = b.x - a.x;
  const dy = b.y - a.y;
  const lenSq = dx * dx + dy * dy;
  if (lenSq === 0) return Math.hypot(p.x - a.x, p.y - a.y);
  let t = ((p.x - a.x) * dx + (p.y - a.y) * dy) / lenSq;
  t = Math.max(0, Math.min(1, t));
  return Math.hypot(p.x - (a.x + t * dx), p.y - (a.y + t * dy));
}
