import { MapboxOverlay } from '@deck.gl/mapbox';
import maplibregl from 'maplibre-gl';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { AisTarget } from '../../entities/aisTarget/model/types';
import { MaplibreDeckCustomLayer } from './MaplibreDeckCustomLayer';
import type { BaseMapId, Map3DProps, MapProjectionId } from './types';
import type { DashSeg, PairRangeCircle } from './types';
import {
  mergeNumberSets,
  makeSetSignature,
  isFiniteNumber,
  toIntMmsi,
  makeUniqueSorted,
  equalNumberArrays,
} from './lib/setUtils';
import { dashifyLine } from './lib/dashifyLine';
import { useHoverState } from './hooks/useHoverState';
import { useMapInit } from './hooks/useMapInit';
import { useProjectionToggle } from './hooks/useProjectionToggle';
import { useBaseMapToggle } from './hooks/useBaseMapToggle';
import { useFlyTo } from './hooks/useFlyTo';
import { useZonesLayer } from './hooks/useZonesLayer';
import { usePredictionVectors } from './hooks/usePredictionVectors';
import { useGlobeShips } from './hooks/useGlobeShips';
import { useGlobeOverlays } from './hooks/useGlobeOverlays';
import { useGlobeInteraction } from './hooks/useGlobeInteraction';
import { useDeckLayers } from './hooks/useDeckLayers';
import { useSubcablesLayer } from './hooks/useSubcablesLayer';
import { useTrackReplayLayer } from './hooks/useTrackReplayLayer';
import { useMapStyleSettings } from './hooks/useMapStyleSettings';
import { useOceanMapSettings } from '../../features/oceanMap/hooks/useOceanMapSettings';
import { useShipLabelColor } from './hooks/useShipLabelColor';
import { VesselContextMenu } from './components/VesselContextMenu';
import { useLiveShipAdapter } from '../../features/liveRenderer/hooks/useLiveShipAdapter';
import { useLiveShipBatchRender } from '../../features/liveRenderer/hooks/useLiveShipBatchRender';
import { useTrackQueryStore } from '../../features/trackReplay/stores/trackQueryStore';
import { useTrackReplayDeckLayers } from '../../features/trackReplay/hooks/useTrackReplayDeckLayers';

export type { Map3DSettings, BaseMapId, MapProjectionId } from './types';

type Props = Map3DProps;


export function Map3D({
  targets,
  zones,
  selectedMmsi,
  hoveredMmsiSet = [],
  hoveredFleetMmsiSet = [],
  hoveredPairMmsiSet = [],
  hoveredFleetOwnerKey = null,
  highlightedMmsiSet = [],
  settings,
  baseMap,
  projection,
  overlays,
  onSelectMmsi,
  onToggleHighlightMmsi,
  onViewBboxChange,
  legacyHits,
  pairLinks,
  fcLinks,
  fleetCircles,
  onProjectionLoadingChange,
  fleetFocus,
  onHoverFleet,
  onClearFleetHover,
  onHoverMmsi,
  onClearMmsiHover,
  onHoverPair,
  onClearPairHover,
  subcableGeo = null,
  hoveredCableId = null,
  onHoverCable,
  onClickCable,
  mapStyleSettings,
  initialView,
  onViewStateChange,
  onGlobeShipsReady,
  activeTrack = null,
  trackContextMenu = null,
  onRequestTrack,
  onCloseTrackMenu,
  onOpenTrackMenu,
  onMapReady,
  alarmMmsiMap,
  onClickShipPhoto,
  freeCamera = true,
  oceanMapSettings,
  encMapSettings,
}: Props) {
  // ── Shared refs ──────────────────────────────────────────────────────
  const containerRef = useRef<HTMLDivElement | null>(null);
  const mapRef = useRef<maplibregl.Map | null>(null);
  const overlayRef = useRef<MapboxOverlay | null>(null);
  const overlayInteractionRef = useRef<MapboxOverlay | null>(null);
  const globeDeckLayerRef = useRef<MaplibreDeckCustomLayer | null>(null);
  const baseMapRef = useRef<BaseMapId>(baseMap);
  const projectionRef = useRef<MapProjectionId>(projection);
  const projectionBusyRef = useRef(false);
  const deckHoverRafRef = useRef<number | null>(null);
  const deckHoverHasHitRef = useRef(false);
  const mapInitiatedSelectRef = useRef(false);

  useEffect(() => { baseMapRef.current = baseMap; }, [baseMap]);
  useEffect(() => { projectionRef.current = projection; }, [projection]);

  // ── Hover state ──────────────────────────────────────────────────────
  const {
    hoveredDeckMmsiSet: hoveredDeckMmsiArr,
    setHoveredDeckMmsiSet,
    setHoveredDeckPairMmsiSet,
    setHoveredDeckFleetOwnerKey,
    setHoveredDeckFleetMmsiSet,
    hoveredZoneId, setHoveredZoneId,
    hoveredMmsiSetRef, hoveredFleetMmsiSetRef, hoveredPairMmsiSetRef,
    externalHighlightedSetRef,
    hoveredDeckMmsiSetRef, hoveredDeckPairMmsiSetRef, hoveredDeckFleetMmsiSetRef,
    hoveredFleetOwnerKeys,
  } = useHoverState({
    hoveredMmsiSet, hoveredFleetMmsiSet, hoveredPairMmsiSet,
    hoveredFleetOwnerKey, highlightedMmsiSet,
  });

  const fleetFocusId = fleetFocus?.id;
  const fleetFocusLon = fleetFocus?.center?.[0];
  const fleetFocusLat = fleetFocus?.center?.[1];
  const fleetFocusZoom = fleetFocus?.zoom;

  // ── Highlight memos ──────────────────────────────────────────────────
  const effectiveHoveredPairMmsiSet = useMemo(
    () => mergeNumberSets(hoveredPairMmsiSetRef, hoveredDeckPairMmsiSetRef),
    [hoveredPairMmsiSetRef, hoveredDeckPairMmsiSetRef],
  );
  const effectiveHoveredFleetMmsiSet = useMemo(
    () => mergeNumberSets(hoveredFleetMmsiSetRef, hoveredDeckFleetMmsiSetRef),
    [hoveredFleetMmsiSetRef, hoveredDeckFleetMmsiSetRef],
  );
  const [mapSyncEpoch, setMapSyncEpoch] = useState(0);
  const highlightedMmsiSetCombined = useMemo(
    () =>
      mergeNumberSets(
        hoveredMmsiSetRef,
        hoveredDeckMmsiSetRef,
        externalHighlightedSetRef,
        effectiveHoveredFleetMmsiSet,
        effectiveHoveredPairMmsiSet,
      ),
    [
      hoveredMmsiSetRef,
      hoveredDeckMmsiSetRef,
      externalHighlightedSetRef,
      effectiveHoveredFleetMmsiSet,
      effectiveHoveredPairMmsiSet,
    ],
  );
  const highlightedMmsiSetForShips = useMemo(
    () => (projection === 'globe' ? mergeNumberSets(hoveredMmsiSetRef, externalHighlightedSetRef) : highlightedMmsiSetCombined),
    [projection, hoveredMmsiSetRef, externalHighlightedSetRef, highlightedMmsiSetCombined],
  );
  const hoveredShipSignature = useMemo(
    () =>
      `${makeSetSignature(hoveredMmsiSetRef)}|${makeSetSignature(externalHighlightedSetRef)}|${makeSetSignature(
        hoveredDeckMmsiSetRef,
      )}|${makeSetSignature(effectiveHoveredFleetMmsiSet)}|${makeSetSignature(effectiveHoveredPairMmsiSet)}`,
    [
      hoveredMmsiSetRef,
      externalHighlightedSetRef,
      hoveredDeckMmsiSetRef,
      effectiveHoveredFleetMmsiSet,
      effectiveHoveredPairMmsiSet,
    ],
  );
  void hoveredShipSignature;
  const hoveredPairMmsiList = useMemo(() => makeUniqueSorted(Array.from(effectiveHoveredPairMmsiSet)), [effectiveHoveredPairMmsiSet]);
  const hoveredFleetOwnerKeyList = useMemo(() => Array.from(hoveredFleetOwnerKeys).sort(), [hoveredFleetOwnerKeys]);
  const hoveredFleetMmsiList = useMemo(() => makeUniqueSorted(Array.from(effectiveHoveredFleetMmsiSet)), [effectiveHoveredFleetMmsiSet]);

  const isHighlightedMmsi = useCallback(
    (mmsi: number) => highlightedMmsiSetCombined.has(mmsi),
    [highlightedMmsiSetCombined],
  );
  const baseHighlightedMmsiSet = useMemo(() => {
    const out = new Set<number>();
    if (selectedMmsi != null) out.add(selectedMmsi);
    externalHighlightedSetRef.forEach((value) => { out.add(value); });
    return out;
  }, [selectedMmsi, externalHighlightedSetRef]);
  const isBaseHighlightedMmsi = useCallback(
    (mmsi: number) => baseHighlightedMmsiSet.has(mmsi),
    [baseHighlightedMmsiSet],
  );
  const isHighlightedPair = useCallback(
    (aMmsi: number, bMmsi: number) =>
      effectiveHoveredPairMmsiSet.size === 2 &&
      effectiveHoveredPairMmsiSet.has(aMmsi) &&
      effectiveHoveredPairMmsiSet.has(bMmsi),
    [effectiveHoveredPairMmsiSet],
  );
  const isHighlightedFleet = useCallback(
    (ownerKey: string, vesselMmsis: number[]) => {
      if (hoveredFleetOwnerKeys.has(ownerKey)) return true;
      return vesselMmsis.some((x) => isHighlightedMmsi(x));
    },
    [hoveredFleetOwnerKeys, isHighlightedMmsi],
  );

  // ── Ship data memos ──────────────────────────────────────────────────
  const rawShipData = useMemo(() => {
    return targets.filter((t) => isFiniteNumber(t.lat) && isFiniteNumber(t.lon) && isFiniteNumber(t.mmsi));
  }, [targets]);

  const hideLiveShips = useTrackQueryStore((state) => state.hideLiveShips);

  const liveShipFeatures = useLiveShipAdapter(rawShipData, legacyHits ?? null);
  const { renderedTargets: batchRenderedTargets } = useLiveShipBatchRender(
    mapRef,
    liveShipFeatures,
    rawShipData,
    mapSyncEpoch,
  );

  const shipData = useMemo(
    () => (hideLiveShips ? [] : rawShipData),
    [hideLiveShips, rawShipData],
  );

  const shipByMmsi = useMemo(() => {
    const byMmsi = new Map<number, AisTarget>();
    for (const t of rawShipData) byMmsi.set(t.mmsi, t);
    return byMmsi;
  }, [rawShipData]);

  const shipLayerData = useMemo(() => {
    if (hideLiveShips) return [];
    // Fallback to raw targets when batch result is temporarily empty
    // (e.g. overlay update race or viewport sync delay).
    if (batchRenderedTargets.length === 0) return rawShipData;
    return [...batchRenderedTargets];
  }, [hideLiveShips, batchRenderedTargets, rawShipData]);

  const shipHighlightSet = useMemo(() => {
    const out = new Set(highlightedMmsiSetForShips);
    if (selectedMmsi) out.add(selectedMmsi);
    return out;
  }, [highlightedMmsiSetForShips, selectedMmsi]);

  // Globe: 직접 호버/선택된 선박만 hover overlay에 포함
  // 선단/쌍 멤버는 feature-state(outline 색상)로 하이라이트 → hover overlay 불필요
  // → alarm badge 레이어 가림 방지
  const shipHoverOverlaySet = useMemo(
    () =>
      projection === 'globe'
        ? mergeNumberSets(shipHighlightSet, hoveredDeckMmsiSetRef)
        : shipHighlightSet,
    [projection, shipHighlightSet, hoveredDeckMmsiSetRef],
  );

  const shipOverlayLayerData = useMemo(() => {
    if (shipLayerData.length === 0) return [];
    if (shipHighlightSet.size === 0) return [];
    return shipLayerData.filter((target) => shipHighlightSet.has(target.mmsi));
  }, [shipHighlightSet, shipLayerData]);

  const trackReplayRenderState = useTrackReplayDeckLayers();

  // ── Deck hover management ────────────────────────────────────────────
  const hasAuxiliarySelectModifier = useCallback(
    (ev?: { shiftKey?: boolean; ctrlKey?: boolean; metaKey?: boolean } | null): boolean => {
      if (!ev) return false;
      return !!(ev.shiftKey || ev.ctrlKey || ev.metaKey);
    },
    [],
  );

  const toFleetMmsiList = useCallback((value: unknown) => {
    if (!Array.isArray(value)) return [];
    const out: number[] = [];
    for (const item of value) {
      const v = toIntMmsi(item);
      if (v != null) out.push(v);
    }
    return out;
  }, []);

  // 지도 내부 클릭에서의 선택 — fly-to 비활성화 플래그 설정
  // eslint-disable-next-line react-hooks/preserve-manual-memoization
  const onMapSelectMmsi = useCallback((mmsi: number | null) => {
    mapInitiatedSelectRef.current = true;
    onSelectMmsi(mmsi);
  }, [onSelectMmsi]);

  const onDeckSelectOrHighlight = useCallback(
    (info: unknown, allowMultiSelect = false) => {
      const obj = info as {
        mmsi?: unknown;
        srcEvent?: { shiftKey?: boolean; ctrlKey?: boolean; metaKey?: boolean } | null;
      };
      const mmsi = toIntMmsi(obj.mmsi);
      if (mmsi == null) return;
      const evt = obj.srcEvent ?? null;
      const isAux = hasAuxiliarySelectModifier(evt);
      if (onToggleHighlightMmsi && isAux) {
        onToggleHighlightMmsi(mmsi);
        return;
      }
      if (!allowMultiSelect && selectedMmsi === mmsi) {
        onMapSelectMmsi(null);
        return;
      }
      onMapSelectMmsi(mmsi);
    },
    [hasAuxiliarySelectModifier, onMapSelectMmsi, onToggleHighlightMmsi, selectedMmsi],
  );

  // eslint-disable-next-line react-hooks/preserve-manual-memoization
  const setHoveredDeckFleetMmsis = useCallback((next: number[]) => {
    const normalized = makeUniqueSorted(next);
    setHoveredDeckFleetMmsiSet((prev) => (equalNumberArrays(prev, normalized) ? prev : normalized));
  }, []);

  // eslint-disable-next-line react-hooks/preserve-manual-memoization
  const setHoveredDeckFleetOwner = useCallback((ownerKey: string | null) => {
    setHoveredDeckFleetOwnerKey((prev) => (prev === ownerKey ? prev : ownerKey));
  }, []);

  const mapDeckMmsiHoverRef = useRef<number[]>([]);
  const mapDeckPairHoverRef = useRef<number[]>([]);
  const mapFleetHoverStateRef = useRef<{ ownerKey: string | null; vesselMmsis: number[] }>({
    ownerKey: null,
    vesselMmsis: [],
  });
  const mapDrivenMmsiHoverRef = useRef(false);
  const mapDrivenPairHoverRef = useRef(false);
  const mapDrivenFleetHoverRef = useRef(false);

  const clearMapFleetHoverState = useCallback(() => {
    const prev = mapFleetHoverStateRef.current;
    mapFleetHoverStateRef.current = { ownerKey: null, vesselMmsis: [] };
    setHoveredDeckFleetOwner(null);
    setHoveredDeckFleetMmsis([]);
    if (
      mapDrivenFleetHoverRef.current &&
      (prev.ownerKey != null || prev.vesselMmsis.length > 0) &&
      hoveredFleetOwnerKey === prev.ownerKey &&
      equalNumberArrays(hoveredFleetMmsiSet, prev.vesselMmsis)
    ) {
      onClearFleetHover?.();
    }
    mapDrivenFleetHoverRef.current = false;
  }, [
    setHoveredDeckFleetOwner,
    setHoveredDeckFleetMmsis,
    onClearFleetHover,
    hoveredFleetOwnerKey,
    hoveredFleetMmsiSet,
  ]);

  const clearDeckHoverPairs = useCallback(() => {
    const prev = mapDeckPairHoverRef.current;
    mapDeckPairHoverRef.current = [];
    setHoveredDeckPairMmsiSet((prevState) => (prevState.length === 0 ? prevState : []));
    if (mapDrivenPairHoverRef.current && prev.length > 0 && equalNumberArrays(hoveredPairMmsiSet, prev)) {
      onClearPairHover?.();
    }
    mapDrivenPairHoverRef.current = false;
  }, [setHoveredDeckPairMmsiSet, onClearPairHover, hoveredPairMmsiSet]);

  const clearDeckHoverMmsi = useCallback(() => {
    const prev = mapDeckMmsiHoverRef.current;
    mapDeckMmsiHoverRef.current = [];
    setHoveredDeckMmsiSet((prevState) => (prevState.length === 0 ? prevState : []));
    if (mapDrivenMmsiHoverRef.current && prev.length > 0 && equalNumberArrays(hoveredMmsiSet, prev)) {
      onClearMmsiHover?.();
    }
    mapDrivenMmsiHoverRef.current = false;
  }, [setHoveredDeckMmsiSet, onClearMmsiHover, hoveredMmsiSet]);

  const scheduleDeckHoverResolve = useCallback(() => {
    if (deckHoverRafRef.current != null) return;
    deckHoverRafRef.current = window.requestAnimationFrame(() => {
      deckHoverRafRef.current = null;
      if (!deckHoverHasHitRef.current) {
        clearDeckHoverMmsi();
        clearDeckHoverPairs();
        clearMapFleetHoverState();
      }
      deckHoverHasHitRef.current = false;
    });
  }, [clearDeckHoverMmsi, clearDeckHoverPairs, clearMapFleetHoverState]);

  const touchDeckHoverState = useCallback(
    (isHover: boolean) => {
      if (isHover) deckHoverHasHitRef.current = true;
      scheduleDeckHoverResolve();
    },
    [scheduleDeckHoverResolve],
  );

  const setDeckHoverMmsi = useCallback(
    (next: number[]) => {
      const normalized = makeUniqueSorted(next);
      const prev = mapDeckMmsiHoverRef.current;
      touchDeckHoverState(normalized.length > 0);
      setHoveredDeckMmsiSet((prev) => (equalNumberArrays(prev, normalized) ? prev : normalized));
      mapDeckMmsiHoverRef.current = normalized;
      if (!equalNumberArrays(prev, normalized)) {
        if (normalized.length > 0) {
          mapDrivenMmsiHoverRef.current = true;
          onHoverMmsi?.(normalized);
        } else if (mapDrivenMmsiHoverRef.current && prev.length > 0) {
          onClearMmsiHover?.();
          mapDrivenMmsiHoverRef.current = false;
        }
      }
    },
    [setHoveredDeckMmsiSet, touchDeckHoverState, onHoverMmsi, onClearMmsiHover],
  );

  const setDeckHoverPairs = useCallback(
    (next: number[]) => {
      const normalized = makeUniqueSorted(next);
      const prev = mapDeckPairHoverRef.current;
      touchDeckHoverState(normalized.length > 0);
      setHoveredDeckPairMmsiSet((prev) => (equalNumberArrays(prev, normalized) ? prev : normalized));
      mapDeckPairHoverRef.current = normalized;
      if (!equalNumberArrays(prev, normalized)) {
        if (normalized.length > 0) {
          mapDrivenPairHoverRef.current = true;
          onHoverPair?.(normalized);
        } else if (mapDrivenPairHoverRef.current && prev.length > 0) {
          onClearPairHover?.();
          mapDrivenPairHoverRef.current = false;
        }
      }
    },
    [setHoveredDeckPairMmsiSet, touchDeckHoverState, onHoverPair, onClearPairHover],
  );

  const setMapFleetHoverState = useCallback(
    (ownerKey: string | null, vesselMmsis: number[]) => {
      const normalized = makeUniqueSorted(vesselMmsis);
      const prev = mapFleetHoverStateRef.current;
      if (prev.ownerKey === ownerKey && equalNumberArrays(prev.vesselMmsis, normalized)) {
        return;
      }
      touchDeckHoverState(!!ownerKey || normalized.length > 0);
      setHoveredDeckFleetOwner(ownerKey);
      setHoveredDeckFleetMmsis(normalized);
      mapFleetHoverStateRef.current = { ownerKey, vesselMmsis: normalized };
      if (ownerKey != null || normalized.length > 0) {
        mapDrivenFleetHoverRef.current = true;
        onHoverFleet?.(ownerKey, normalized);
      } else if (mapDrivenFleetHoverRef.current) {
        onClearFleetHover?.();
        mapDrivenFleetHoverRef.current = false;
      }
    },
    [
      setHoveredDeckFleetOwner,
      setHoveredDeckFleetMmsis,
      touchDeckHoverState,
      onHoverFleet,
      onClearFleetHover,
    ],
  );

  // hover RAF cleanup
  useEffect(() => {
    return () => {
      if (deckHoverRafRef.current != null) {
        window.cancelAnimationFrame(deckHoverRafRef.current);
        deckHoverRafRef.current = null;
      }
      deckHoverHasHitRef.current = false;
    };
  }, []);

  // sync external fleet hover state
  useEffect(() => {
    mapFleetHoverStateRef.current = {
      ownerKey: hoveredFleetOwnerKey,
      vesselMmsis: hoveredFleetMmsiSet,
    };
  }, [hoveredFleetOwnerKey, hoveredFleetMmsiSet]);

  // ── Overlay data memos ───────────────────────────────────────────────
  const fcDashed = useMemo(() => {
    const segs: DashSeg[] = [];
    for (const l of fcLinks || []) {
      segs.push(...dashifyLine(l.from, l.to, l.suspicious, l.distanceNm, l.fcMmsi, l.otherMmsi));
    }
    return segs;
  }, [fcLinks]);

  const pairRanges = useMemo(() => {
    const out: PairRangeCircle[] = [];
    for (const p of pairLinks || []) {
      const center: [number, number] = [(p.from[0] + p.to[0]) / 2, (p.from[1] + p.to[1]) / 2];
      out.push({
        center,
        radiusNm: Math.max(0.05, p.distanceNm / 2),
        warn: p.warn,
        aMmsi: p.aMmsi,
        bMmsi: p.bMmsi,
        distanceNm: p.distanceNm,
      });
    }
    return out;
  }, [pairLinks]);

  const pairLinksInteractive = useMemo(() => {
    if ((pairLinks?.length ?? 0) === 0) return [];
    if (effectiveHoveredPairMmsiSet.size < 2) return [];
    const links = pairLinks || [];
    return links.filter((link) =>
      effectiveHoveredPairMmsiSet.has(link.aMmsi) && effectiveHoveredPairMmsiSet.has(link.bMmsi),
    );
  }, [pairLinks, effectiveHoveredPairMmsiSet]);

  const pairRangesInteractive = useMemo(() => {
    if (pairRanges.length === 0) return [];
    if (effectiveHoveredPairMmsiSet.size < 2) return [];
    return pairRanges.filter((range) =>
      effectiveHoveredPairMmsiSet.has(range.aMmsi) && effectiveHoveredPairMmsiSet.has(range.bMmsi),
    );
  }, [pairRanges, effectiveHoveredPairMmsiSet]);

  const fcLinesInteractive = useMemo(() => {
    if (fcDashed.length === 0) return [];
    if (highlightedMmsiSetCombined.size === 0) return [];
    return fcDashed.filter(
      (line) =>
        [line.fromMmsi, line.toMmsi].some((mmsi) => (mmsi == null ? false : highlightedMmsiSetCombined.has(mmsi))),
    );
  }, [fcDashed, highlightedMmsiSetCombined]);

  const fleetCirclesInteractive = useMemo(() => {
    if ((fleetCircles?.length ?? 0) === 0) return [];
    if (hoveredFleetOwnerKeys.size === 0 && highlightedMmsiSetCombined.size === 0) return [];
    const circles = fleetCircles || [];
    return circles.filter((item) => isHighlightedFleet(item.ownerKey, item.vesselMmsis));
  }, [fleetCircles, hoveredFleetOwnerKeys, isHighlightedFleet, highlightedMmsiSetCombined]);

  // ── Hook orchestration ───────────────────────────────────────────────
  const { ensureMercatorOverlay, pulseMapSync } = useMapInit(
    containerRef, mapRef, overlayRef, overlayInteractionRef, globeDeckLayerRef,
    baseMapRef, projectionRef,
    { baseMap, projection, showSeamark: settings.showSeamark, onViewBboxChange, setMapSyncEpoch, initialView, onViewStateChange },
  );

  const reorderGlobeFeatureLayers = useProjectionToggle(
    mapRef, overlayRef, overlayInteractionRef, globeDeckLayerRef, projectionBusyRef,
    { projection, ensureMercatorOverlay, onProjectionLoadingChange, pulseMapSync, setMapSyncEpoch },
  );

  // freeCamera 토글에 따라 회전/틸트 제어 (모드별 독립 상태)
  // mapSyncEpoch: 맵 비동기 생성 후 최초 적용을 위해 의존
  useEffect(() => {
    const map = mapRef.current;
    if (!map) return;
    try {
      if (freeCamera) {
        map.dragRotate.enable();
        map.touchPitch.enable();
      } else {
        map.dragRotate.disable();
        map.touchPitch.disable();
        if (!projectionBusyRef.current && (map.getPitch() !== 0 || map.getBearing() !== 0)) {
          map.easeTo({ pitch: 0, bearing: 0, duration: 300 });
        }
      }
    } catch { /* ignore */ }
  }, [freeCamera, projection, mapSyncEpoch]);

  useBaseMapToggle(
    mapRef,
    { baseMap, projection, showSeamark: settings.showSeamark, mapSyncEpoch, pulseMapSync },
  );

  useMapStyleSettings(mapRef, mapStyleSettings, { baseMap, mapSyncEpoch });
  useOceanMapSettings(mapRef, oceanMapSettings, { baseMap, mapSyncEpoch });
  const shipLabelColors = useShipLabelColor(mapRef, baseMap, mapSyncEpoch, encMapSettings);

  useZonesLayer(
    mapRef, projectionBusyRef, reorderGlobeFeatureLayers,
    { zones, overlays, projection, baseMap, hoveredZoneId, mapSyncEpoch },
  );

  usePredictionVectors(
    mapRef, projectionBusyRef, reorderGlobeFeatureLayers,
    {
      overlays, settings, shipData, legacyHits, selectedMmsi,
      externalHighlightedSetRef, projection, baseMap, mapSyncEpoch,
    },
  );

  useGlobeShips(
    mapRef, projectionBusyRef, reorderGlobeFeatureLayers,
    {
      projection, settings, shipData: shipLayerData, shipHighlightSet, shipHoverOverlaySet,
      shipOverlayLayerData, shipLayerData, shipByMmsi, mapSyncEpoch,
      onSelectMmsi: onMapSelectMmsi, onToggleHighlightMmsi, targets: shipLayerData, overlays,
      legacyHits, selectedMmsi, isBaseHighlightedMmsi, hasAuxiliarySelectModifier,
      onGlobeShipsReady, alarmMmsiMap,
    },
  );

  useGlobeOverlays(
    mapRef, projectionBusyRef, reorderGlobeFeatureLayers,
    {
      overlays, pairLinks, fcLinks, fleetCircles, projection,
      mapSyncEpoch, hoveredFleetMmsiList, hoveredFleetOwnerKeyList, hoveredPairMmsiList,
    },
  );

  useGlobeInteraction(
    mapRef, projectionBusyRef,
    {
      projection, settings, overlays, targets, shipData, shipByMmsi, selectedMmsi,
      hoveredZoneId, legacyHits,
      clearDeckHoverPairs, clearDeckHoverMmsi, clearMapFleetHoverState,
      setDeckHoverPairs, setDeckHoverMmsi, setMapFleetHoverState, setHoveredZoneId,
    },
  );

  useDeckLayers(
    mapRef, overlayRef, globeDeckLayerRef, projectionBusyRef,
    {
      projection, settings, trackReplayDeckLayers: trackReplayRenderState.trackReplayDeckLayers, shipLayerData, shipData,
      legacyHits, pairLinks, fcLinks, fcDashed, fleetCircles, pairRanges,
      pairLinksInteractive, pairRangesInteractive, fcLinesInteractive, fleetCirclesInteractive,
      overlays, shipByMmsi, selectedMmsi, shipHighlightSet,
      isHighlightedFleet, isHighlightedPair, isHighlightedMmsi,
      clearDeckHoverPairs, clearDeckHoverMmsi, clearMapFleetHoverState,
      setDeckHoverPairs, setDeckHoverMmsi, setMapFleetHoverState,
      toFleetMmsiList, touchDeckHoverState, hasAuxiliarySelectModifier,
      onDeckSelectOrHighlight, onSelectMmsi: onMapSelectMmsi, onToggleHighlightMmsi,
      ensureMercatorOverlay, alarmMmsiMap,
      onClickShipPhoto,
      shipLabelColors,
    },
  );

  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  const noopCable = useCallback((_: string | null) => {}, []);

  useSubcablesLayer(
    mapRef, projectionBusyRef, reorderGlobeFeatureLayers,
    {
      subcableGeo: subcableGeo ?? null,
      overlays, projection, mapSyncEpoch,
      hoveredCableId: hoveredCableId ?? null,
      onHoverCable: onHoverCable ?? noopCable,
      onClickCable: onClickCable ?? noopCable,
    },
  );

  useTrackReplayLayer(
    mapRef, projectionBusyRef, reorderGlobeFeatureLayers,
    { activeTrack, projection, mapSyncEpoch, renderState: trackReplayRenderState },
  );

  // 우클릭 컨텍스트 메뉴 — 대상선박(legacyHits)만 허용
  // Mercator: Deck.gl hover 상태에서 MMSI 참조, Globe: queryRenderedFeatures
  const hoveredDeckMmsiRef = useRef(hoveredDeckMmsiArr);
  useEffect(() => { hoveredDeckMmsiRef.current = hoveredDeckMmsiArr; }, [hoveredDeckMmsiArr]);

  useEffect(() => {
    const container = containerRef.current;
    if (!container) return;
    const onContextMenu = (e: MouseEvent) => {
      e.preventDefault();
      if (!onOpenTrackMenu) return;
      const map = mapRef.current;
      if (!map || projectionBusyRef.current) return;

      let mmsi: number | null = null;

      // Globe/Mercator 공통: MapLibre 레이어에서 bbox 쿼리 (호버 상태 무관)
      let shipLayerIds: string[] = [];
      try {
        shipLayerIds = projectionRef.current === 'globe'
          ? [
              'ships-globe', 'ships-globe-lite', 'ships-globe-halo', 'ships-globe-outline',
              'ships-globe-alarm-pulse', 'ships-globe-alarm-badge',
            ].filter((id) => map.getLayer(id))
          : [];
      } catch { /* ignore */ }

      if (shipLayerIds.length > 0) {
        const tolerance = 8;
        const bbox: [maplibregl.PointLike, maplibregl.PointLike] = [
          [e.offsetX - tolerance, e.offsetY - tolerance],
          [e.offsetX + tolerance, e.offsetY + tolerance],
        ];
        let features: maplibregl.MapGeoJSONFeature[] = [];
        try {
          features = map.queryRenderedFeatures(bbox, { layers: shipLayerIds });
        } catch { /* ignore */ }

        if (features.length > 0) {
          const props = features[0].properties || {};
          const raw = typeof props.mmsi === 'number' ? props.mmsi : Number(props.mmsi);
          if (Number.isFinite(raw) && raw > 0) mmsi = raw;
        }
      }

      // Mercator fallback: Deck.gl 호버 상태에서 MMSI 참조
      if (mmsi == null && projectionRef.current !== 'globe') {
        const hovered = hoveredDeckMmsiRef.current;
        if (hovered.length > 0) mmsi = hovered[0];
      }

      if (mmsi == null) return;

      const target = shipByMmsi.get(mmsi);
      const vesselName = (target?.name || '').trim() || `MMSI ${mmsi}`;
      const isPermitted = legacyHits?.has(mmsi) ?? false;
      onOpenTrackMenu({ x: e.clientX, y: e.clientY, mmsi, vesselName, isPermitted });
    };
    container.addEventListener('contextmenu', onContextMenu);
    return () => container.removeEventListener('contextmenu', onContextMenu);
  }, [onOpenTrackMenu, legacyHits, shipByMmsi]);

  useFlyTo(
    mapRef, projectionRef,
    { selectedMmsi, shipData, mapInitiatedSelectRef, fleetFocusId, fleetFocusLon, fleetFocusLat, fleetFocusZoom },
  );

  // Map ready 콜백 — mapSyncEpoch 초기 증가 시 1회 호출
  const mapReadyFiredRef = useRef(false);
  useEffect(() => {
    if (mapReadyFiredRef.current || !onMapReady || !mapRef.current) return;
    mapReadyFiredRef.current = true;
    onMapReady(mapRef.current);
  }, [mapSyncEpoch, onMapReady]);

  return (
    <>
      <div ref={containerRef} style={{ width: '100%', height: '100%' }} />
      {trackContextMenu && onCloseTrackMenu && (
        <VesselContextMenu
          x={trackContextMenu.x}
          y={trackContextMenu.y}
          mmsi={trackContextMenu.mmsi}
          vesselName={trackContextMenu.vesselName}
          isPermitted={trackContextMenu.isPermitted}
          onRequestTrack={onRequestTrack ?? undefined}
          onClose={onCloseTrackMenu}
        />
      )}
    </>
  );
}
