import { useState, useEffect, useMemo, useCallback, useRef } from 'react';
import { Source, Layer, Popup, useMap } from 'react-map-gl/maplibre';
import { FONT_MONO } from '../../styles/fonts';
import type { GeoJSON } from 'geojson';
import type { MapLayerMouseEvent } from 'maplibre-gl';
import type { Ship, VesselAnalysisDto } from '../../types';
import { fetchFleetCompanies, fetchGroupHistory } from '../../services/vesselAnalysis';
import type { FleetCompany, GroupPolygonDto } from '../../services/vesselAnalysis';
import type { UseGroupPolygonsResult } from '../../hooks/useGroupPolygons';

/**
 * Convex hull + buffer 기반 폴리곤 생성 (Python polygon_builder.py 동일 로직)
 * - 1점: 원형 버퍼 (GEAR_BUFFER_DEG=0.01 ≈ 1.1km)
 * - 2점: 두 점 잇는 직선 양쪽 버퍼
 * - 3점+: convex hull + 버퍼
 */
const GEAR_BUFFER_DEG = 0.01;
const CIRCLE_SEGMENTS = 16;

function buildInterpPolygon(points: [number, number][]): GeoJSON.Polygon | null {
  if (points.length === 0) return null;

  if (points.length === 1) {
    // Point.buffer → 원형
    const [cx, cy] = points[0];
    const ring: [number, number][] = [];
    for (let i = 0; i <= CIRCLE_SEGMENTS; i++) {
      const angle = (2 * Math.PI * i) / CIRCLE_SEGMENTS;
      ring.push([cx + GEAR_BUFFER_DEG * Math.cos(angle), cy + GEAR_BUFFER_DEG * Math.sin(angle)]);
    }
    return { type: 'Polygon', coordinates: [ring] };
  }

  if (points.length === 2) {
    // LineString.buffer → 캡슐 형태
    const [p1, p2] = points;
    const dx = p2[0] - p1[0];
    const dy = p2[1] - p1[1];
    const len = Math.sqrt(dx * dx + dy * dy) || 1e-10;
    const nx = (-dy / len) * GEAR_BUFFER_DEG;
    const ny = (dx / len) * GEAR_BUFFER_DEG;
    // 양쪽 오프셋 + 반원 엔드캡
    const ring: [number, number][] = [];
    const half = CIRCLE_SEGMENTS / 2;
    // p1→p2 오른쪽
    ring.push([p1[0] + nx, p1[1] + ny]);
    ring.push([p2[0] + nx, p2[1] + ny]);
    // p2 반원
    const a2 = Math.atan2(ny, nx);
    for (let i = 0; i <= half; i++) {
      const angle = a2 - Math.PI * i / half;
      ring.push([p2[0] + GEAR_BUFFER_DEG * Math.cos(angle), p2[1] + GEAR_BUFFER_DEG * Math.sin(angle)]);
    }
    // p2→p1 왼쪽
    ring.push([p1[0] - nx, p1[1] - ny]);
    // p1 반원
    const a1 = Math.atan2(-ny, -nx);
    for (let i = 0; i <= half; i++) {
      const angle = a1 - Math.PI * i / half;
      ring.push([p1[0] + GEAR_BUFFER_DEG * Math.cos(angle), p1[1] + GEAR_BUFFER_DEG * Math.sin(angle)]);
    }
    ring.push(ring[0]); // 닫기
    return { type: 'Polygon', coordinates: [ring] };
  }

  // 3점+: convex hull + buffer
  const hull = convexHull(points);
  return bufferPolygon(hull, GEAR_BUFFER_DEG);
}

/** 단순 convex hull (Graham scan) */
function convexHull(points: [number, number][]): [number, number][] {
  const pts = [...points].sort((a, b) => a[0] - b[0] || a[1] - b[1]);
  if (pts.length <= 2) return pts;

  const cross = (o: [number, number], a: [number, number], b: [number, number]) =>
    (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);

  const lower: [number, number][] = [];
  for (const p of pts) {
    while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], p) <= 0) lower.pop();
    lower.push(p);
  }
  const upper: [number, number][] = [];
  for (let i = pts.length - 1; i >= 0; i--) {
    while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], pts[i]) <= 0) upper.pop();
    upper.push(pts[i]);
  }
  lower.pop();
  upper.pop();
  return lower.concat(upper);
}

/** 폴리곤 외곽에 buffer 적용 (각 변의 오프셋 + 꼭짓점 라운드) */
function bufferPolygon(hull: [number, number][], buf: number): GeoJSON.Polygon {
  const ring: [number, number][] = [];
  const n = hull.length;
  for (let i = 0; i < n; i++) {
    const p = hull[i];
    const prev = hull[(i - 1 + n) % n];
    const next = hull[(i + 1) % n];
    // 이전 변과 다음 변의 외향 법선
    const a1 = Math.atan2(p[1] - prev[1], p[0] - prev[0]) - Math.PI / 2;
    const a2 = Math.atan2(next[1] - p[1], next[0] - p[0]) - Math.PI / 2;
    // 꼭짓점 라운딩 (a1 → a2)
    const startA = a1;
    let endA = a2;
    if (endA < startA) endA += 2 * Math.PI;
    const steps = Math.max(2, Math.round((endA - startA) / (Math.PI / 8)));
    for (let s = 0; s <= steps; s++) {
      const a = startA + (endA - startA) * s / steps;
      ring.push([p[0] + buf * Math.cos(a), p[1] + buf * Math.sin(a)]);
    }
  }
  ring.push(ring[0]);
  return { type: 'Polygon', coordinates: [ring] };
}

/**
 * 히스토리 데이터의 gap 구간에 보간 프레임을 삽입하여 완성 데이터셋 반환.
 * - gap ≤ 30분: 5분 간격 직선 보간 (이전 폴리곤 유지, 중심만 직선 이동)
 * - gap > 30분: 30분 간격으로 멤버 위치 보간 + 가상 폴리곤 생성
 */
function fillGapFrames(snapshots: GroupPolygonDto[]): HistoryFrame[] {
  if (snapshots.length < 2) return snapshots;
  const STEP_SHORT_MS = 300_000;   // 5분
  const STEP_LONG_MS = 1_800_000;  // 30분
  const THRESHOLD_MS = 1_800_000;  // 30분 경계
  const result: GroupPolygonDto[] = [];

  for (let i = 0; i < snapshots.length; i++) {
    result.push(snapshots[i]);
    if (i >= snapshots.length - 1) continue;

    const prev = snapshots[i];
    const next = snapshots[i + 1];
    const t0 = new Date(prev.snapshotTime).getTime();
    const t1 = new Date(next.snapshotTime).getTime();
    const gap = t1 - t0;
    if (gap <= STEP_SHORT_MS) continue;

    const nextMap = new Map(next.members.map(m => [m.mmsi, m]));
    const common = prev.members.filter(m => nextMap.has(m.mmsi));
    if (common.length === 0) continue;

    if (gap <= THRESHOLD_MS) {
      // ≤30분: 5분 간격 직선 보간 (중심만 이동, 폴리곤은 이전 것 유지)
      for (let t = t0 + STEP_SHORT_MS; t < t1; t += STEP_SHORT_MS) {
        const ratio = (t - t0) / gap;
        const cLon = prev.centerLon + (next.centerLon - prev.centerLon) * ratio;
        const cLat = prev.centerLat + (next.centerLat - prev.centerLat) * ratio;
        result.push({
          ...prev,
          snapshotTime: new Date(t).toISOString(),
          centerLon: cLon,
          centerLat: cLat,
          _interp: true,
        });
      }
    } else {
      // >30분: 30분 간격으로 멤버 위치 보간 + 가상 폴리곤
      for (let t = t0 + STEP_LONG_MS; t < t1; t += STEP_LONG_MS) {
        const ratio = (t - t0) / gap;
        const positions: [number, number][] = [];
        const members: MemberInfo[] = [];

        for (const pm of common) {
          const nm = nextMap.get(pm.mmsi)!;
          const lon = pm.lon + (nm.lon - pm.lon) * ratio;
          const lat = pm.lat + (nm.lat - pm.lat) * ratio;
          const dLon = nm.lon - pm.lon;
          const dLat = nm.lat - pm.lat;
          const cog = (dLon === 0 && dLat === 0) ? pm.cog : (Math.atan2(dLon, dLat) * 180 / Math.PI + 360) % 360;
          members.push({ mmsi: pm.mmsi, name: pm.name, lon, lat, sog: pm.sog, cog, role: pm.role, isParent: pm.isParent });
          positions.push([lon, lat]);
        }

        const cLon = positions.reduce((s, p) => s + p[0], 0) / positions.length;
        const cLat = positions.reduce((s, p) => s + p[1], 0) / positions.length;
        const polygon = buildInterpPolygon(positions);

        result.push({
          ...prev,
          snapshotTime: new Date(t).toISOString(),
          polygon,
          centerLon: cLon,
          centerLat: cLat,
          memberCount: members.length,
          members,
          _interp: true,
          _longGap: true,
        });
      }
    }
  }
  return result;
}

export interface SelectedGearGroupData {
  parent: Ship | null;
  gears: Ship[];
  groupName: string;
}

export interface SelectedFleetData {
  clusterId: number;
  ships: Ship[];
  companyName: string;
}

/** 히스토리 스냅샷 + 보간 플래그 */
type HistoryFrame = GroupPolygonDto & { _interp?: boolean; _longGap?: boolean };

interface Props {
  ships: Ship[];
  analysisMap?: Map<string, VesselAnalysisDto>;
  clusters?: Map<number, string[]>;
  onShipSelect?: (mmsi: string) => void;
  onFleetZoom?: (bounds: { minLat: number; maxLat: number; minLng: number; maxLng: number }) => void;
  onSelectedGearChange?: (data: SelectedGearGroupData | null) => void;
  onSelectedFleetChange?: (data: SelectedFleetData | null) => void;
  groupPolygons?: UseGroupPolygonsResult;
}

const EMPTY_ANALYSIS = new globalThis.Map<string, VesselAnalysisDto>();

export function FleetClusterLayer({ ships, analysisMap: analysisMapProp, onShipSelect, onFleetZoom, onSelectedGearChange, onSelectedFleetChange, groupPolygons }: Props) {
  const analysisMap = analysisMapProp ?? EMPTY_ANALYSIS;
  const [companies, setCompanies] = useState<Map<number, FleetCompany>>(new Map());
  const [expandedFleet, setExpandedFleet] = useState<number | null>(null);
  const [activeSection, setActiveSection] = useState<string | null>('fleet');
  const toggleSection = (key: string) => setActiveSection(prev => prev === key ? null : key);
  const [hoveredFleetId, setHoveredFleetId] = useState<number | null>(null);
  const [expandedGearGroup, setExpandedGearGroup] = useState<string | null>(null);
  const [selectedGearGroup, setSelectedGearGroup] = useState<string | null>(null);
  // 폴리곤 호버 툴팁
  const [hoverTooltip, setHoverTooltip] = useState<{ lng: number; lat: number; type: 'fleet' | 'gear'; id: number | string } | null>(null);
  // 어구 다중 선택 팝업
  const [gearPickerPopup, setGearPickerPopup] = useState<{
    lng: number; lat: number;
    candidates: { name: string; count: number; inZone: boolean; isFleet: boolean; clusterId?: number }[];
  } | null>(null);
  const [pickerHoveredGroup, setPickerHoveredGroup] = useState<string | null>(null);
  // 히스토리 애니메이션 — 12시간 실시간 타임라인
  const [historyData, setHistoryData] = useState<HistoryFrame[] | null>(null);
  const [, setHistoryGroupKey] = useState<string | null>(null);
  const [timelinePos, setTimelinePos] = useState(0); // 0~1 (12시간 내 위치)
  const [isPlaying, setIsPlaying] = useState(true);
  const animTimerRef = useRef<ReturnType<typeof setInterval>>();
  const historyStartRef = useRef(0); // 12시간 전 epoch ms
  const historyEndRef = useRef(0); // 현재 epoch ms
  const { current: mapRef } = useMap();
  const registeredRef = useRef(false);
  const dataRef = useRef<{ shipMap: Map<string, Ship>; groupPolygons: UseGroupPolygonsResult | undefined; onFleetZoom: Props['onFleetZoom'] }>({ shipMap: new Map(), groupPolygons, onFleetZoom });

  useEffect(() => {
    fetchFleetCompanies().then(setCompanies).catch(() => {});
  }, []);

  const TIMELINE_DURATION_MS = 12 * 60 * 60_000; // 12시간
  const PLAYBACK_CYCLE_SEC = 30; // 30초에 12시간 전체 재생
  const TICK_MS = 50; // 50ms 간격 업데이트

  const loadHistory = async (groupKey: string) => {
    setHistoryGroupKey(groupKey);
    setTimelinePos(0);
    setIsPlaying(true);
    const history = await fetchGroupHistory(groupKey, 12);
    const sorted = history.reverse(); // 시간 오름차순
    const filled = fillGapFrames(sorted); // 빈 구간 보간 삽입
    const now = Date.now();
    historyStartRef.current = now - TIMELINE_DURATION_MS;
    historyEndRef.current = now;
    setHistoryData(filled);
  };

  const closeHistory = useCallback(() => {
    setHistoryData(null);
    setHistoryGroupKey(null);
    setTimelinePos(0);
    setIsPlaying(true);
    clearInterval(animTimerRef.current);
  }, []);

  // 재생 타이머 — 50ms마다 timelinePos 진행
  useEffect(() => {
    if (!historyData || !isPlaying) {
      clearInterval(animTimerRef.current);
      return;
    }
    const step = TICK_MS / (PLAYBACK_CYCLE_SEC * 1000); // 1틱당 진행량
    animTimerRef.current = setInterval(() => {
      setTimelinePos(prev => {
        const next = prev + step;
        return next >= 1 ? 0 : next; // 순환
      });
    }, TICK_MS);
    return () => clearInterval(animTimerRef.current);
  }, [historyData, isPlaying]);

  // timelinePos → 현재 시각 + 가장 가까운 스냅샷 인덱스
  const currentTimeMs = historyStartRef.current + timelinePos * TIMELINE_DURATION_MS;
  const currentSnapIdx = useMemo(() => {
    if (!historyData || historyData.length === 0) return -1;
    let best = 0;
    let bestDiff = Infinity;
    for (let i = 0; i < historyData.length; i++) {
      const t = new Date(historyData[i].snapshotTime).getTime();
      const diff = Math.abs(t - currentTimeMs);
      if (diff < bestDiff) { bestDiff = diff; best = i; }
    }
    // 보간 프레임 포함 데이터셋에서 가장 가까운 프레임 매칭 (최대 gap = 30분)
    return bestDiff < 1_800_000 ? best : -1;
  }, [historyData, currentTimeMs]);

  // 스냅샷 존재 구간 맵 (프로그레스 바 갭 표시용)
  // 프로그레스 바: 원본 데이터만 표시 (보간 프레임 제외)
  const snapshotRanges = useMemo(() => {
    if (!historyData) return [];
    return historyData
      .filter(h => !h._interp)
      .map(h => {
        const t = new Date(h.snapshotTime).getTime();
        return (t - historyStartRef.current) / TIMELINE_DURATION_MS;
      });
  }, [historyData]);

  useEffect(() => {
    const onKeyDown = (e: KeyboardEvent) => {
      if (e.key === 'Escape') {
        if (historyData) closeHistory();
        setSelectedGearGroup(null);
        setExpandedFleet(null);
        setExpandedGearGroup(null);
      }
    };
    window.addEventListener('keydown', onKeyDown);
    return () => window.removeEventListener('keydown', onKeyDown);
  }, [historyData, closeHistory]);

  // ── 맵 폴리곤 클릭/호버 이벤트 등록
  useEffect(() => {
    const map = mapRef?.getMap();
    if (!map || registeredRef.current) return;

    const fleetLayers = ['fleet-cluster-fill-layer'];
    const gearLayers = ['gear-cluster-fill-layer'];
    const allLayers = [...fleetLayers, ...gearLayers];

    const setCursor = (cursor: string) => { map.getCanvas().style.cursor = cursor; };

    const onFleetEnter = (e: MapLayerMouseEvent) => {
      setCursor('pointer');
      const feat = e.features?.[0];
      if (!feat) return;
      const cid = feat.properties?.clusterId as number | undefined;
      if (cid != null) {
        setHoveredFleetId(cid);
        setHoverTooltip({ lng: e.lngLat.lng, lat: e.lngLat.lat, type: 'fleet', id: cid });
      }
    };
    const onFleetLeave = () => {
      setCursor('');
      setHoveredFleetId(null);
      setHoverTooltip(prev => prev?.type === 'fleet' ? null : prev);
    };
    const handleFleetSelect = (cid: number) => {
      const d = dataRef.current;
      setExpandedFleet(prev => prev === cid ? null : cid);
      setActiveSection('fleet');
      const group = d.groupPolygons?.fleetGroups.find(g => Number(g.groupKey) === cid);
      if (!group || group.members.length === 0) return;
      let minLat = Infinity, maxLat = -Infinity, minLng = Infinity, maxLng = -Infinity;
      for (const m of group.members) {
        if (m.lat < minLat) minLat = m.lat;
        if (m.lat > maxLat) maxLat = m.lat;
        if (m.lon < minLng) minLng = m.lon;
        if (m.lon > maxLng) maxLng = m.lon;
      }
      if (minLat !== Infinity) d.onFleetZoom?.({ minLat, maxLat, minLng, maxLng });
      loadHistory(String(cid));
    };

    // 통합 클릭 핸들러: 선단+어구 모든 폴리곤 겹침 판정
    const onPolygonClick = (e: MapLayerMouseEvent) => {
      const features = map.queryRenderedFeatures(e.point, { layers: allLayers });
      if (features.length === 0) return;

      // 후보 수집 (선단 + 어구 통합)
      const seen = new Set<string>();
      const candidates: { name: string; count: number; inZone: boolean; isFleet: boolean; clusterId?: number }[] = [];
      for (const f of features) {
        const cid = f.properties?.clusterId as number | undefined;
        const gearName = f.properties?.name as string | undefined;
        if (cid != null) {
          const key = `fleet-${cid}`;
          if (seen.has(key)) continue;
          seen.add(key);
          const d = dataRef.current;
          const g = d.groupPolygons?.fleetGroups.find(x => Number(x.groupKey) === cid);
          candidates.push({ name: g?.groupLabel ?? `선단 #${cid}`, count: g?.memberCount ?? 0, inZone: false, isFleet: true, clusterId: cid });
        } else if (gearName) {
          if (seen.has(gearName)) continue;
          seen.add(gearName);
          candidates.push({ name: gearName, count: f.properties?.gearCount ?? 0, inZone: f.properties?.inZone === 1, isFleet: false });
        }
      }

      if (candidates.length === 1) {
        // 단일 → 바로 선택
        const c = candidates[0];
        if (c.isFleet && c.clusterId != null) handleFleetSelect(c.clusterId);
        else handleGearGroupZoomFromMap(c.name);
      } else if (candidates.length > 1) {
        // 다중 → 선택 팝업
        setGearPickerPopup({ lng: e.lngLat.lng, lat: e.lngLat.lat, candidates });
      }
    };

    const onFleetClick = onPolygonClick;

    const onGearEnter = (e: MapLayerMouseEvent) => {
      setCursor('pointer');
      const feat = e.features?.[0];
      if (!feat) return;
      const name = feat.properties?.name as string | undefined;
      if (name) {
        setHoverTooltip({ lng: e.lngLat.lng, lat: e.lngLat.lat, type: 'gear', id: name });
      }
    };
    const onGearLeave = () => {
      setCursor('');
      setHoverTooltip(prev => prev?.type === 'gear' ? null : prev);
    };
    const handleGearGroupZoomFromMap = (name: string) => {
      const d = dataRef.current;
      setSelectedGearGroup(prev => prev === name ? null : name);
      setExpandedGearGroup(name);

      // 해당 어구가 속한 섹션으로 아코디언 전환
      const isInZone = d.groupPolygons?.gearInZoneGroups.some(g => g.groupKey === name);
      setActiveSection(isInZone ? 'inZone' : 'outZone');

      // 섹션 전환 후 스크롤
      requestAnimationFrame(() => {
        setTimeout(() => {
          document.getElementById(`gear-row-${name}`)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
        }, 50);
      });
      const allGroups = d.groupPolygons
        ? [...d.groupPolygons.gearInZoneGroups, ...d.groupPolygons.gearOutZoneGroups]
        : [];
      const group = allGroups.find(g => g.groupKey === name);
      if (!group || group.members.length === 0) return;
      let minLat = Infinity, maxLat = -Infinity, minLng = Infinity, maxLng = -Infinity;
      for (const m of group.members) {
        if (m.lat < minLat) minLat = m.lat;
        if (m.lat > maxLat) maxLat = m.lat;
        if (m.lon < minLng) minLng = m.lon;
        if (m.lon > maxLng) maxLng = m.lon;
      }
      if (minLat !== Infinity) d.onFleetZoom?.({ minLat, maxLat, minLng, maxLng });
      loadHistory(name);
    };

    const onGearClick = onPolygonClick;

    const register = () => {
      const ready = allLayers.every(id => map.getLayer(id));
      if (!ready) return;
      registeredRef.current = true;

      for (const id of fleetLayers) {
        map.on('mouseenter', id, onFleetEnter);
        map.on('mouseleave', id, onFleetLeave);
        map.on('click', id, onFleetClick);
      }
      for (const id of gearLayers) {
        map.on('mouseenter', id, onGearEnter);
        map.on('mouseleave', id, onGearLeave);
        map.on('click', id, onGearClick);
      }
    };

    register();
    if (!registeredRef.current) {
      const interval = setInterval(() => {
        register();
        if (registeredRef.current) clearInterval(interval);
      }, 500);
      return () => clearInterval(interval);
    }
  }, [mapRef]);

  // ships map (mmsi → Ship)
  const shipMap = useMemo(() => {
    const m = new Map<string, Ship>();
    for (const s of ships) m.set(s.mmsi, s);
    return m;
  }, [ships]);

  // stale closure 방지
  dataRef.current = { shipMap, groupPolygons, onFleetZoom };

  // 선택된 어구 그룹 데이터를 부모에 전달 (deck.gl 렌더링용) — 히스토리 모드에서는 null
  useEffect(() => {
    if (!selectedGearGroup || historyData) {
      onSelectedGearChange?.(null);
      if (historyData) return; // 히스토리 모드: 선택은 유지하되 부모 강조만 숨김
      return;
    }
    const allGroups = groupPolygons
      ? [...groupPolygons.gearInZoneGroups, ...groupPolygons.gearOutZoneGroups]
      : [];
    const group = allGroups.find(g => g.groupKey === selectedGearGroup);
    if (!group) {
      onSelectedGearChange?.(null);
      return;
    }
    const parent = group.members.find(m => m.isParent);
    const gears = group.members.filter(m => !m.isParent);
    const toShip = (m: typeof group.members[0]): Ship => ({
      mmsi: m.mmsi,
      name: m.name,
      lat: m.lat,
      lng: m.lon,
      heading: m.cog,
      speed: m.sog,
      course: m.cog,
      category: 'fishing',
      lastSeen: Date.now(),
    });
    onSelectedGearChange?.({
      parent: parent ? toShip(parent) : null,
      gears: gears.map(toShip),
      groupName: selectedGearGroup,
    });
  }, [selectedGearGroup, groupPolygons, onSelectedGearChange, historyData]);

  // 선택된 선단 데이터를 부모에 전달 (deck.gl 강조 렌더링용) — 히스토리 모드에서는 null
  useEffect(() => {
    if (expandedFleet === null || historyData) {
      onSelectedFleetChange?.(null);
      if (historyData) return;
      return;
    }
    const group = groupPolygons?.fleetGroups.find(g => Number(g.groupKey) === expandedFleet);
    const company = companies.get(expandedFleet);
    if (!group) {
      onSelectedFleetChange?.(null);
      return;
    }
    const fleetShips: Ship[] = group.members.map(m => ({
      mmsi: m.mmsi,
      name: m.name,
      lat: m.lat,
      lng: m.lon,
      heading: m.cog,
      speed: m.sog,
      course: m.cog,
      category: 'fishing',
      lastSeen: Date.now(),
    }));
    onSelectedFleetChange?.({
      clusterId: expandedFleet,
      ships: fleetShips,
      companyName: company?.nameCn || group.groupLabel || `선단 #${expandedFleet}`,
    });
  }, [expandedFleet, groupPolygons, companies, onSelectedFleetChange, historyData]);

  // API 기반 어구 그룹 분류
  const inZoneGearGroups = groupPolygons?.gearInZoneGroups ?? [];
  const outZoneGearGroups = groupPolygons?.gearOutZoneGroups ?? [];

  // 선단 폴리곤 GeoJSON (서버 제공)
  const fleetPolygonGeoJSON = useMemo((): GeoJSON => {
    const features: GeoJSON.Feature[] = [];
    if (!groupPolygons) return { type: 'FeatureCollection', features };
    for (const g of groupPolygons.fleetGroups) {
      if (!g.polygon) continue;
      features.push({
        type: 'Feature',
        properties: { clusterId: Number(g.groupKey), color: g.color },
        geometry: g.polygon,
      });
    }
    return { type: 'FeatureCollection', features };
  }, [groupPolygons?.fleetGroups]);

  // 2척 선단 라인은 서버에서 Shapely buffer로 이미 Polygon 처리되므로 빈 컬렉션
  const lineGeoJSON = useMemo((): GeoJSON => ({
    type: 'FeatureCollection', features: [],
  }), []);

  // 호버 하이라이트용 단일 폴리곤
  const hoveredGeoJSON = useMemo((): GeoJSON => {
    if (hoveredFleetId === null || !groupPolygons) return { type: 'FeatureCollection', features: [] };
    const g = groupPolygons.fleetGroups.find(f => Number(f.groupKey) === hoveredFleetId);
    if (!g?.polygon) return { type: 'FeatureCollection', features: [] };
    return {
      type: 'FeatureCollection',
      features: [{
        type: 'Feature',
        properties: { clusterId: hoveredFleetId, color: g.color },
        geometry: g.polygon,
      }],
    };
  }, [hoveredFleetId, groupPolygons?.fleetGroups]);

  // 어구 클러스터 GeoJSON (서버 제공)
  const gearClusterGeoJson = useMemo((): GeoJSON => {
    const features: GeoJSON.Feature[] = [];
    if (!groupPolygons) return { type: 'FeatureCollection', features };
    for (const g of [...groupPolygons.gearInZoneGroups, ...groupPolygons.gearOutZoneGroups]) {
      if (!g.polygon) continue;
      features.push({
        type: 'Feature',
        properties: {
          name: g.groupKey,
          gearCount: g.memberCount,
          inZone: g.groupType === 'GEAR_IN_ZONE' ? 1 : 0,
        },
        geometry: g.polygon,
      });
    }
    return { type: 'FeatureCollection', features };
  }, [groupPolygons?.gearInZoneGroups, groupPolygons?.gearOutZoneGroups]);

  // 가상 선박 마커 GeoJSON (API members + shipMap heading 보정)
  const memberMarkersGeoJson = useMemo((): GeoJSON => {
    const features: GeoJSON.Feature[] = [];
    if (!groupPolygons) return { type: 'FeatureCollection', features };

    const addMember = (m: { mmsi: string; name: string; lat: number; lon: number; cog: number; isParent: boolean; role: string }, groupKey: string, groupType: string, color: string) => {
      // shipMap에서 실제 heading 조회 (AIS 하드웨어 값, API cog보다 정확)
      const realShip = shipMap.get(m.mmsi);
      const heading = realShip?.heading ?? m.cog ?? 0;
      const lat = realShip?.lat ?? m.lat;
      const lon = realShip?.lng ?? m.lon;
      features.push({
        type: 'Feature',
        properties: { mmsi: m.mmsi, name: m.name, groupKey, groupType, role: m.role, isParent: m.isParent ? 1 : 0, isGear: (groupType !== 'FLEET' && !m.isParent) ? 1 : 0, color, cog: heading, baseSize: (groupType !== 'FLEET' && !m.isParent) ? 0.11 : m.isParent ? 0.18 : 0.14 },
        geometry: { type: 'Point', coordinates: [lon, lat] },
      });
    };

    // 선단 멤버
    for (const g of groupPolygons.fleetGroups) {
      for (const m of g.members) addMember(m, g.groupKey, 'FLEET', g.color);
    }
    // 어구 멤버
    for (const g of [...groupPolygons.gearInZoneGroups, ...groupPolygons.gearOutZoneGroups]) {
      const color = g.groupType === 'GEAR_IN_ZONE' ? '#dc2626' : '#f97316';
      for (const m of g.members) addMember(m, g.groupKey, g.groupType, color);
    }
    return { type: 'FeatureCollection', features };
  }, [groupPolygons, shipMap]);

  // picker 호버 하이라이트 (선단 + 어구 통합)
  const pickerHighlightGeoJson = useMemo((): GeoJSON => {
    if (!pickerHoveredGroup || !groupPolygons) return { type: 'FeatureCollection', features: [] };
    // 선단에서 찾기
    const fleet = groupPolygons.fleetGroups.find(x => String(x.groupKey) === pickerHoveredGroup);
    if (fleet?.polygon) return { type: 'FeatureCollection', features: [{ type: 'Feature', properties: {}, geometry: fleet.polygon }] };
    // 어구에서 찾기
    const all = [...groupPolygons.gearInZoneGroups, ...groupPolygons.gearOutZoneGroups];
    const g = all.find(x => x.groupKey === pickerHoveredGroup);
    if (!g?.polygon) return { type: 'FeatureCollection', features: [] };
    return { type: 'FeatureCollection', features: [{ type: 'Feature', properties: {}, geometry: g.polygon }] };
  }, [pickerHoveredGroup, groupPolygons]);

  // ── 히스토리 애니메이션 GeoJSON ──
  const EMPTY_HIST_FC: GeoJSON = { type: 'FeatureCollection', features: [] };

  const memberTrailsGeoJson = useMemo((): GeoJSON => {
    if (!historyData) return EMPTY_HIST_FC;
    const tracks = new Map<string, [number, number][]>();
    for (const snap of historyData) {
      for (const m of snap.members) {
        const arr = tracks.get(m.mmsi) ?? [];
        arr.push([m.lon, m.lat]);
        tracks.set(m.mmsi, arr);
      }
    }
    const features: GeoJSON.Feature[] = [];
    for (const [, coords] of tracks) {
      if (coords.length < 2) continue;
      features.push({ type: 'Feature', properties: {}, geometry: { type: 'LineString', coordinates: coords } });
    }
    return { type: 'FeatureCollection', features };
  }, [historyData]);

  // 현재 또는 마지막 유효 스냅샷 (신호없음 구간에서 이전 데이터 유지)
  const effectiveSnapIdx = useMemo(() => {
    if (!historyData || historyData.length === 0) return -1;
    if (currentSnapIdx >= 0) return currentSnapIdx;
    // 현재 시각 이전의 가장 가까운 스냅샷
    for (let i = historyData.length - 1; i >= 0; i--) {
      if (new Date(historyData[i].snapshotTime).getTime() <= currentTimeMs) return i;
    }
    return -1;
  }, [historyData, currentSnapIdx, currentTimeMs]);

  const isStale = currentSnapIdx < 0 && effectiveSnapIdx >= 0;

  const currentFrame = historyData && effectiveSnapIdx >= 0 ? historyData[effectiveSnapIdx] : null;
  const isLongGap = !!currentFrame?._longGap;
  const showGray = isLongGap || (isStale && !currentFrame?._interp);

  // center trail: historyData에 이미 보간 프레임 포함 → 전체 좌표 연결
  const centerTrailGeoJson = useMemo((): GeoJSON => {
    if (!historyData || historyData.length === 0) return EMPTY_HIST_FC;

    const features: GeoJSON.Feature[] = [];

    // 연속된 같은 타입끼리 세그먼트 분리
    let segStart = 0;
    for (let i = 1; i <= historyData.length; i++) {
      const curInterp = i < historyData.length && !!historyData[i]._longGap;
      const startInterp = !!historyData[segStart]._longGap;
      if (i < historyData.length && curInterp === startInterp) continue;

      const from = segStart > 0 ? segStart - 1 : segStart;
      const seg = historyData.slice(from, i);
      if (seg.length >= 2) {
        features.push({
          type: 'Feature',
          properties: { interpolated: startInterp ? 1 : 0 },
          geometry: { type: 'LineString', coordinates: seg.map(s => [s.centerLon, s.centerLat]) },
        });
      }
      segStart = i;
    }

    // 실데이터 도트만
    for (const h of historyData) {
      if (h.color === '#94a3b8') continue;
      features.push({
        type: 'Feature', properties: { interpolated: 0 },
        geometry: { type: 'Point', coordinates: [h.centerLon, h.centerLat] },
      });
    }

    return { type: 'FeatureCollection', features };
  }, [historyData]);

  // 현재 재생 위치 포인트
  const currentCenterGeoJson = useMemo((): GeoJSON => {
    if (!historyData || effectiveSnapIdx < 0) return EMPTY_HIST_FC;
    const snap = historyData[effectiveSnapIdx];
    if (!snap) return EMPTY_HIST_FC;
    return {
      type: 'FeatureCollection',
      features: [{
        type: 'Feature',
        properties: { interpolated: showGray ? 1 : 0 },
        geometry: { type: 'Point', coordinates: [snap.centerLon, snap.centerLat] },
      }],
    };
  }, [historyData, effectiveSnapIdx, showGray]);

  const animPolygonGeoJson = useMemo((): GeoJSON => {
    if (!historyData || effectiveSnapIdx < 0) return EMPTY_HIST_FC;
    const snap = historyData[effectiveSnapIdx];
    if (!snap?.polygon) return EMPTY_HIST_FC;
    return {
      type: 'FeatureCollection',
      features: [{ type: 'Feature', properties: { stale: isStale ? 1 : 0, interpolated: showGray ? 1 : 0 }, geometry: snap.polygon }],
    };
  }, [historyData, effectiveSnapIdx, isStale, showGray]);

  // 현재 프레임의 멤버 위치 (가상 아이콘)
  const animMembersGeoJson = useMemo((): GeoJSON => {
    if (!historyData || effectiveSnapIdx < 0) return EMPTY_HIST_FC;
    const snap = historyData[effectiveSnapIdx];
    if (!snap) return EMPTY_HIST_FC;
    return {
      type: 'FeatureCollection',
      features: snap.members.map(m => ({
        type: 'Feature' as const,
        properties: { mmsi: m.mmsi, name: m.name, cog: m.cog ?? 0, role: m.role, isGear: m.role === 'GEAR' ? 1 : 0, stale: isStale ? 1 : 0, interpolated: showGray ? 1 : 0 },
        geometry: { type: 'Point' as const, coordinates: [m.lon, m.lat] },
      })),
    };
  }, [historyData, effectiveSnapIdx, isStale, showGray]);

  // 선단 목록 (멤버 수 내림차순)
  const fleetList = useMemo(() => {
    if (!groupPolygons) return [];
    return groupPolygons.fleetGroups.map(g => ({
      id: Number(g.groupKey),
      mmsiList: g.members.map(m => m.mmsi),
      label: g.groupLabel,
      memberCount: g.memberCount,
      areaSqNm: g.areaSqNm,
      color: g.color,
      members: g.members,
    })).sort((a, b) => b.memberCount - a.memberCount);
  }, [groupPolygons?.fleetGroups]);

  const handleFleetZoom = useCallback((clusterId: number) => {
    const group = groupPolygons?.fleetGroups.find(g => Number(g.groupKey) === clusterId);
    if (!group || group.members.length === 0) return;
    let minLat = Infinity, maxLat = -Infinity, minLng = Infinity, maxLng = -Infinity;
    for (const m of group.members) {
      if (m.lat < minLat) minLat = m.lat;
      if (m.lat > maxLat) maxLat = m.lat;
      if (m.lon < minLng) minLng = m.lon;
      if (m.lon > maxLng) maxLng = m.lon;
    }
    if (minLat === Infinity) return;
    onFleetZoom?.({ minLat, maxLat, minLng, maxLng });
  }, [groupPolygons?.fleetGroups, onFleetZoom]);

  const handleGearGroupZoom = useCallback((parentName: string) => {
    setSelectedGearGroup(prev => prev === parentName ? null : parentName);
    setExpandedGearGroup(parentName);

    // 해당 어구가 속한 섹션으로 아코디언 전환
    const isInZone = groupPolygons?.gearInZoneGroups.some(g => g.groupKey === parentName);
    setActiveSection(isInZone ? 'inZone' : 'outZone');

    requestAnimationFrame(() => {
      setTimeout(() => {
        document.getElementById(`gear-row-${parentName}`)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
      }, 50);
    });
    const allGroups = groupPolygons
      ? [...groupPolygons.gearInZoneGroups, ...groupPolygons.gearOutZoneGroups]
      : [];
    const group = allGroups.find(g => g.groupKey === parentName);
    if (!group || group.members.length === 0) return;
    let minLat = Infinity, maxLat = -Infinity, minLng = Infinity, maxLng = -Infinity;
    for (const m of group.members) {
      if (m.lat < minLat) minLat = m.lat;
      if (m.lat > maxLat) maxLat = m.lat;
      if (m.lon < minLng) minLng = m.lon;
      if (m.lon > maxLng) maxLng = m.lon;
    }
    if (minLat === Infinity) return;
    onFleetZoom?.({ minLat, maxLat, minLng, maxLng });
    loadHistory(parentName);
  }, [groupPolygons, onFleetZoom]);

  // 패널 스타일 (AnalysisStatsPanel 패턴)
  const panelStyle: React.CSSProperties = {
    position: 'absolute',
    bottom: 60,
    left: 10,
    zIndex: 10,
    minWidth: 220,
    maxWidth: 300,
    backgroundColor: 'rgba(12, 24, 37, 0.92)',
    border: '1px solid rgba(99, 179, 237, 0.25)',
    borderRadius: 8,
    color: '#e2e8f0',
    fontFamily: FONT_MONO,
    fontSize: 11,
    boxShadow: '0 4px 16px rgba(0, 0, 0, 0.5)',
    overflow: 'hidden',
    display: 'flex',
    flexDirection: 'column',
    pointerEvents: 'auto',
    maxHeight: 'min(45vh, 400px)',
  };

  const headerStyle: React.CSSProperties = {
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'space-between',
    padding: '6px 10px',
    borderBottom: 'none',
    cursor: 'default',
    userSelect: 'none',
    flexShrink: 0,
  };

  const toggleButtonStyle: React.CSSProperties = {
    background: 'none',
    border: 'none',
    color: '#94a3b8',
    cursor: 'pointer',
    fontSize: 10,
    padding: '0 2px',
    lineHeight: 1,
  };

  return (
    <>
      {/* 선단 폴리곤 레이어 */}
      <Source id="fleet-cluster-fill" type="geojson" data={fleetPolygonGeoJSON}>
        <Layer
          id="fleet-cluster-fill-layer"
          type="fill"
          paint={{
            'fill-color': ['get', 'color'],
            'fill-opacity': 0.1,
          }}
        />
        <Layer
          id="fleet-cluster-line-layer"
          type="line"
          paint={{
            'line-color': ['get', 'color'],
            'line-opacity': 0.5,
            'line-width': 1.5,
          }}
        />
      </Source>

      {/* 2척 선단 라인 (서버측 Polygon 처리로 빈 컬렉션) */}
      <Source id="fleet-cluster-line" type="geojson" data={lineGeoJSON}>
        <Layer
          id="fleet-cluster-line-only"
          type="line"
          paint={{
            'line-color': ['get', 'color'],
            'line-opacity': 0.5,
            'line-width': 1.5,
            'line-dasharray': [4, 2],
          }}
        />
      </Source>

      {/* 호버 하이라이트 (별도 Source) */}
      <Source id="fleet-cluster-hovered" type="geojson" data={hoveredGeoJSON}>
        <Layer
          id="fleet-cluster-hovered-fill"
          type="fill"
          paint={{
            'fill-color': ['get', 'color'],
            'fill-opacity': 0.25,
          }}
        />
      </Source>

      {/* 선택된 어구 그룹 하이라이트 폴리곤 — 히스토리 모드에서는 숨김 */}
      {selectedGearGroup && !historyData && (() => {
        const allGroups = groupPolygons
          ? [...groupPolygons.gearInZoneGroups, ...groupPolygons.gearOutZoneGroups]
          : [];
        const group = allGroups.find(g => g.groupKey === selectedGearGroup);
        if (!group?.polygon) return null;
        const hlGeoJson: GeoJSON.FeatureCollection = {
          type: 'FeatureCollection',
          features: [{
            type: 'Feature',
            properties: {},
            geometry: group.polygon,
          }],
        };
        return (
          <Source id="gear-cluster-selected" type="geojson" data={hlGeoJson}>
            <Layer id="gear-selected-fill" type="fill" paint={{ 'fill-color': '#f97316', 'fill-opacity': 0.25 }} />
            <Layer id="gear-selected-line" type="line" paint={{ 'line-color': '#f97316', 'line-width': 3, 'line-opacity': 0.9 }} />
          </Source>
        );
      })()}

      {/* 비허가 어구 클러스터 폴리곤 */}
      <Source id="gear-clusters" type="geojson" data={gearClusterGeoJson}>
        <Layer
          id="gear-cluster-fill-layer"
          type="fill"
          paint={{
            'fill-color': ['case', ['==', ['get', 'inZone'], 1], 'rgba(220, 38, 38, 0.12)', 'rgba(249, 115, 22, 0.08)'],
          }}
        />
        <Layer
          id="gear-cluster-line-layer"
          type="line"
          paint={{
            'line-color': ['case', ['==', ['get', 'inZone'], 1], '#dc2626', '#f97316'],
            'line-opacity': 0.7,
            'line-width': ['case', ['==', ['get', 'inZone'], 1], 2, 1.5],
            'line-dasharray': [4, 2],
          }}
        />
      </Source>

      {/* 가상 선박 마커 — 히스토리 재생 모드에서는 숨김 (애니메이션 아이콘으로 대체) */}
      <Source id="group-member-markers" type="geojson" data={historyData ? ({ type: 'FeatureCollection', features: [] } as GeoJSON) : memberMarkersGeoJson}>
        <Layer
          id="group-member-icon"
          type="symbol"
          layout={{
            'icon-image': ['case', ['==', ['get', 'isGear'], 1], 'gear-diamond', 'ship-triangle'],
            'icon-size': ['interpolate', ['linear'], ['zoom'],
              4, ['*', ['get', 'baseSize'], 0.9],
              6, ['*', ['get', 'baseSize'], 1.2],
              8, ['*', ['get', 'baseSize'], 1.8],
              10, ['*', ['get', 'baseSize'], 2.6],
              12, ['*', ['get', 'baseSize'], 3.2],
              13, ['*', ['get', 'baseSize'], 4.0],
              14, ['*', ['get', 'baseSize'], 4.8],
            ],
            'icon-rotate': ['case', ['==', ['get', 'isGear'], 1], 0, ['get', 'cog']],
            'icon-rotation-alignment': 'map',
            'icon-allow-overlap': true,
            'icon-ignore-placement': true,
            'text-field': ['get', 'name'],
            'text-size': ['interpolate', ['linear'], ['zoom'], 4, 6, 8, 8, 12, 10],
            'text-offset': [0, 1.4],
            'text-anchor': 'top',
            'text-allow-overlap': false,
            'text-font': ['Open Sans Regular', 'Arial Unicode MS Regular'],
          }}
          paint={{
            'icon-color': ['get', 'color'],
            'icon-halo-color': 'rgba(0,0,0,0.6)',
            'icon-halo-width': 0.5,
            'text-color': ['get', 'color'],
            'text-halo-color': '#000000',
            'text-halo-width': 1,
          }}
        />
      </Source>

      {/* 어구 picker 호버 하이라이트 */}
      <Source id="gear-picker-highlight" type="geojson" data={pickerHighlightGeoJson}>
        <Layer id="gear-picker-highlight-fill" type="fill"
          paint={{ 'fill-color': '#ffffff', 'fill-opacity': 0.25 }} />
        <Layer id="gear-picker-highlight-line" type="line"
          paint={{ 'line-color': '#ffffff', 'line-width': 2, 'line-dasharray': [3, 2] }} />
      </Source>

      {/* 어구 다중 선택 팝업 */}
      {gearPickerPopup && (
        <Popup longitude={gearPickerPopup.lng} latitude={gearPickerPopup.lat}
          onClose={() => { setGearPickerPopup(null); setPickerHoveredGroup(null); }}
          closeOnClick={false} className="gl-popup" maxWidth="220px">
          <div style={{ fontSize: 10, fontFamily: FONT_MONO, padding: '4px 0' }}>
            <div style={{ fontWeight: 700, marginBottom: 4, color: '#e2e8f0', padding: '0 6px' }}>
              겹친 그룹 ({gearPickerPopup.candidates.length})
            </div>
            {gearPickerPopup.candidates.map(c => (
              <div key={c.isFleet ? `fleet-${c.clusterId}` : c.name}
                onMouseEnter={() => setPickerHoveredGroup(c.isFleet ? String(c.clusterId) : c.name)}
                onMouseLeave={() => setPickerHoveredGroup(null)}
                onClick={() => {
                  if (c.isFleet && c.clusterId != null) {
                    setExpandedFleet(prev => prev === c.clusterId! ? null : c.clusterId!);
                    setActiveSection('fleet');
                    handleFleetZoom(c.clusterId);
                  } else {
                    handleGearGroupZoom(c.name);
                  }
                  setGearPickerPopup(null);
                  setPickerHoveredGroup(null);
                }}
                style={{
                  cursor: 'pointer', padding: '3px 6px',
                  borderLeft: `3px solid ${c.isFleet ? '#63b3ed' : c.inZone ? '#dc2626' : '#f97316'}`,
                  marginBottom: 2, borderRadius: 2,
                  backgroundColor: pickerHoveredGroup === (c.isFleet ? String(c.clusterId) : c.name) ? 'rgba(255,255,255,0.1)' : 'transparent',
                }}>
                <span style={{ color: c.isFleet ? '#63b3ed' : '#e2e8f0', fontSize: 9 }}>{c.isFleet ? '⚓ ' : ''}{c.name}</span>
                <span style={{ color: '#64748b', marginLeft: 4 }}>({c.count}{c.isFleet ? '척' : '개'})</span>
              </div>
            ))}
          </div>
        </Popup>
      )}

      {/* 폴리곤 호버 툴팁 */}
      {hoverTooltip && (() => {
        if (hoverTooltip.type === 'fleet') {
          const cid = hoverTooltip.id as number;
          const group = groupPolygons?.fleetGroups.find(g => Number(g.groupKey) === cid);
          const company = companies.get(cid);
          const memberCount = group?.memberCount ?? 0;
          return (
            <Popup longitude={hoverTooltip.lng} latitude={hoverTooltip.lat}
              closeButton={false} closeOnClick={false} anchor="bottom"
              className="gl-popup" maxWidth="220px"
            >
              <div style={{ fontFamily: FONT_MONO, fontSize: 10, padding: 4, color: '#e2e8f0' }}>
                <div style={{ fontWeight: 700, color: group?.color ?? '#63b3ed', marginBottom: 3 }}>
                  {company?.nameCn || group?.groupLabel || `선단 #${cid}`}
                </div>
                <div style={{ color: '#94a3b8' }}>선박 {memberCount}척</div>
                {expandedFleet === cid && group?.members.slice(0, 5).map(m => {
                  const dto = analysisMap.get(m.mmsi);
                  const role = dto?.algorithms.fleetRole.role ?? m.role;
                  return (
                    <div key={m.mmsi} style={{ fontSize: 9, color: '#7ea8c4', marginTop: 2 }}>
                      {role === 'LEADER' ? '★' : '·'} {m.name || m.mmsi} <span style={{ color: '#4a6b82' }}>{m.sog.toFixed(1)}kt</span>
                    </div>
                  );
                })}
                <div style={{ fontSize: 8, color: '#4a6b82', marginTop: 3 }}>클릭하여 상세 보기</div>
              </div>
            </Popup>
          );
        }
        if (hoverTooltip.type === 'gear') {
          const name = hoverTooltip.id as string;
          const allGroups = groupPolygons
            ? [...groupPolygons.gearInZoneGroups, ...groupPolygons.gearOutZoneGroups]
            : [];
          const group = allGroups.find(g => g.groupKey === name);
          if (!group) return null;
          const parentMember = group.members.find(m => m.isParent);
          const gearMembers = group.members.filter(m => !m.isParent);
          return (
            <Popup longitude={hoverTooltip.lng} latitude={hoverTooltip.lat}
              closeButton={false} closeOnClick={false} anchor="bottom"
              className="gl-popup" maxWidth="220px"
            >
              <div style={{ fontFamily: FONT_MONO, fontSize: 10, padding: 4, color: '#e2e8f0' }}>
                <div style={{ fontWeight: 700, color: '#f97316', marginBottom: 3 }}>
                  {name} <span style={{ fontWeight: 400, color: '#94a3b8' }}>어구 {gearMembers.length}개</span>
                </div>
                {parentMember && (
                  <div style={{ fontSize: 9, color: '#fbbf24' }}>모선: {parentMember.name || parentMember.mmsi}</div>
                )}
                {selectedGearGroup === name && gearMembers.slice(0, 5).map(m => (
                  <div key={m.mmsi} style={{ fontSize: 9, color: '#7ea8c4', marginTop: 1 }}>
                    · {m.name || m.mmsi}
                  </div>
                ))}
                <div style={{ fontSize: 8, color: '#4a6b82', marginTop: 3 }}>클릭하여 선택/해제</div>
              </div>
            </Popup>
          );
        }
        return null;
      })()}

      {/* ── 히스토리 애니메이션 레이어 (최상위) ── */}
      {historyData && (
        <Source id="history-member-trails" type="geojson" data={memberTrailsGeoJson}>
          <Layer id="history-member-trails-line" type="line" paint={{
            'line-color': '#cbd5e1', 'line-width': 1.5, 'line-opacity': 0.65,
          }} />
        </Source>
      )}
      {historyData && (
        <Source id="history-center-trail" type="geojson" data={centerTrailGeoJson}>
          <Layer id="history-center-trail-line" type="line" paint={{
            'line-color': ['case', ['==', ['get', 'interpolated'], 1], '#f97316', '#fbbf24'],
            'line-width': 2,
            'line-dasharray': [4, 4],
            'line-opacity': ['case', ['==', ['get', 'interpolated'], 1], 0.8, 0.7],
          }} filter={['==', '$type', 'LineString']} />
          <Layer id="history-center-dots" type="circle" paint={{
            'circle-radius': 2.5, 'circle-color': '#fbbf24', 'circle-opacity': 0.6,
          }} filter={['==', '$type', 'Point']} />
        </Source>
      )}
      {/* 보간 경로는 centerTrailGeoJson에 통합됨 (interpolated=1 세그먼트) */}
      {/* 현재 재생 위치 포인트 (실데이터=빨강, 보간=주황) */}
      {historyData && effectiveSnapIdx >= 0 && (
        <Source id="history-current-center" type="geojson" data={currentCenterGeoJson}>
          <Layer id="history-current-center-dot" type="circle" paint={{
            'circle-radius': 7,
            'circle-color': ['case', ['==', ['get', 'interpolated'], 1], '#f97316', '#ef4444'],
            'circle-stroke-width': 2,
            'circle-stroke-color': '#ffffff',
          }} />
        </Source>
      )}
      {historyData && (
        <Source id="history-anim-polygon" type="geojson" data={animPolygonGeoJson}>
          <Layer id="history-anim-fill" type="fill" paint={{
            'fill-color': ['case', ['==', ['get', 'interpolated'], 1], '#94a3b8', isStale ? '#64748b' : '#fbbf24'],
            'fill-opacity': ['case', ['==', ['get', 'interpolated'], 1], 0.12, isStale ? 0.08 : 0.15],
          }} />
          <Layer id="history-anim-line" type="line" paint={{
            'line-color': ['case', ['==', ['get', 'interpolated'], 1], '#94a3b8', isStale ? '#64748b' : '#fbbf24'],
            'line-width': ['case', ['==', ['get', 'interpolated'], 1], 1.5, isStale ? 1 : 2],
            'line-opacity': ['case', ['==', ['get', 'interpolated'], 1], 0.5, isStale ? 0.4 : 0.7],
            'line-dasharray': isStale ? [3, 3] : [1, 0],
          }} />
        </Source>
      )}
      {/* 가상 아이콘 — 현재 프레임 멤버 위치 (최상위) */}
      {historyData && (
        <Source id="history-anim-members" type="geojson" data={animMembersGeoJson}>
          <Layer id="history-anim-members-icon" type="symbol" layout={{
            'icon-image': ['case', ['==', ['get', 'isGear'], 1], 'gear-diamond', 'ship-triangle'],
            'icon-size': ['case', ['==', ['get', 'isGear'], 1], 0.55, 0.7],
            'icon-rotate': ['case', ['==', ['get', 'isGear'], 1], 0, ['get', 'cog']],
            'icon-rotation-alignment': 'map',
            'icon-allow-overlap': true,
          }} paint={{
            'icon-color': ['case',
              ['==', ['get', 'interpolated'], 1], '#94a3b8',
              ['==', ['get', 'stale'], 1], '#64748b',
              ['==', ['get', 'isGear'], 0], '#fbbf24',
              '#a8b8c8',
            ],
            'icon-opacity': ['case',
              ['==', ['get', 'interpolated'], 1], 0.5,
              ['==', ['get', 'stale'], 1], 0.4,
              0.9,
            ],
          }} />
          <Layer id="history-anim-members-label" type="symbol" layout={{
            'text-field': ['get', 'name'],
            'text-size': 8,
            'text-offset': [0, 1.5],
            'text-allow-overlap': false,
          }} paint={{
            'text-color': ['case',
              ['==', ['get', 'interpolated'], 1], '#94a3b8',
              ['==', ['get', 'isGear'], 0], '#fbbf24',
              '#e2e8f0',
            ],
            'text-halo-color': 'rgba(0,0,0,0.8)',
            'text-halo-width': 1,
          }} />
        </Source>
      )}

      {/* 히스토리 재생 컨트롤러 */}
      {historyData && (() => {
        const curTime = new Date(currentTimeMs);
        const timeStr = curTime.toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit' });
        const hasSnap = currentSnapIdx >= 0;
        return (
          <div style={{
            position: 'absolute', bottom: 20, left: '50%', transform: 'translateX(-50%)',
            background: 'rgba(12,24,37,0.95)', border: '1px solid rgba(99,179,237,0.25)',
            borderRadius: 8, padding: '8px 14px', display: 'flex', flexDirection: 'column', gap: 4,
            zIndex: 20, fontFamily: FONT_MONO, fontSize: 10, color: '#e2e8f0',
            boxShadow: '0 4px 16px rgba(0,0,0,0.5)', pointerEvents: 'auto', minWidth: 360,
          }}>
            {/* 프로그레스 바 — 갭 표시 */}
            <div style={{ position: 'relative', height: 8, background: 'rgba(255,255,255,0.05)', borderRadius: 4, overflow: 'hidden' }}>
              {/* 스냅샷 존재 구간 표시 */}
              {snapshotRanges.map((pos, i) => (
                <div key={i} style={{
                  position: 'absolute', left: `${pos * 100}%`, top: 0, width: 2, height: '100%',
                  background: 'rgba(251,191,36,0.4)',
                }} />
              ))}
              {/* 현재 위치 */}
              <div style={{
                position: 'absolute', left: `${timelinePos * 100}%`, top: -1, width: 3, height: 10,
                background: hasSnap ? '#fbbf24' : '#ef4444', borderRadius: 1,
                transform: 'translateX(-50%)',
              }} />
            </div>
            {/* 컨트롤 행 */}
            <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
              <button type="button" onClick={() => setIsPlaying(p => !p)} style={{
                background: 'none', border: '1px solid rgba(99,179,237,0.3)', borderRadius: 4,
                color: '#e2e8f0', cursor: 'pointer', padding: '2px 6px', fontSize: 12, fontFamily: FONT_MONO,
              }}>
                {isPlaying ? '⏸' : '▶'}
              </button>
              <span style={{ color: hasSnap ? '#fbbf24' : '#ef4444', minWidth: 40, textAlign: 'center' }}>
                {timeStr}
              </span>
              <input type="range" min={0} max={1000} value={Math.round(timelinePos * 1000)}
                onChange={e => { setIsPlaying(false); setTimelinePos(Number(e.target.value) / 1000); }}
                style={{ flex: 1, cursor: 'pointer', accentColor: '#fbbf24' }}
                title="히스토리 타임라인" aria-label="히스토리 타임라인"
              />
              <span style={{ color: '#64748b', fontSize: 9 }}>
                {historyData.length}건
              </span>
              <button type="button" onClick={closeHistory} style={{
                background: 'none', border: '1px solid rgba(239,68,68,0.3)', borderRadius: 4,
                color: '#ef4444', cursor: 'pointer', padding: '2px 6px', fontSize: 11, fontFamily: FONT_MONO,
              }}>✕</button>
            </div>
          </div>
        );
      })()}

      {/* 선단 목록 패널 */}
      <div style={panelStyle}>
          {/* ── 선단 현황 섹션 ── */}
          <div style={{ ...headerStyle, cursor: 'pointer' }} onClick={() => toggleSection('fleet')}>
            <span style={{ fontWeight: 700, color: '#63b3ed', letterSpacing: 0.5 }}>
              선단 현황 ({fleetList.length}개)
            </span>
            <button type="button" style={toggleButtonStyle} aria-label="선단 현황 접기/펴기">
              {activeSection === 'fleet' ? '▲' : '▼'}
            </button>
          </div>
          {activeSection === 'fleet' && (
            <div style={{ padding: '4px 0', overflowY: 'auto', flex: 1 }}>
              {fleetList.length === 0 ? (
                <div style={{ color: '#64748b', textAlign: 'center', padding: '8px 10px' }}>
                  선단 데이터 없음
                </div>
              ) : (
                fleetList.map(({ id, mmsiList, label, color, members }) => {
                const company = companies.get(id);
                const companyName = company?.nameCn ?? label ?? `선단 #${id}`;
                const isOpen = expandedFleet === id;
                const isHovered = hoveredFleetId === id;

                const mainMembers = members.filter(m => {
                  const dto = analysisMap.get(m.mmsi);
                  return dto?.algorithms.fleetRole.role === 'LEADER' || dto?.algorithms.fleetRole.role === 'MEMBER';
                });
                const displayMembers = mainMembers.length > 0 ? mainMembers : members;

                return (
                  <div key={id}>
                    {/* 선단 행 */}
                    <div
                      onMouseEnter={() => setHoveredFleetId(id)}
                      onMouseLeave={() => setHoveredFleetId(null)}
                      style={{
                        display: 'flex',
                        alignItems: 'center',
                        gap: 4,
                        padding: '4px 10px',
                        cursor: 'pointer',
                        backgroundColor: isHovered ? 'rgba(255,255,255,0.06)' : 'transparent',
                        borderLeft: isOpen ? `2px solid ${color}` : '2px solid transparent',
                        transition: 'background-color 0.1s',
                      }}
                    >
                      {/* 펼침 토글 */}
                      <span
                        onClick={() => setExpandedFleet(prev => (prev === id ? null : id))}
                        style={{ color: '#94a3b8', fontSize: 9, flexShrink: 0, cursor: 'pointer' }}
                      >
                        {isOpen ? '▾' : '▸'}
                      </span>
                      {/* 색상 인디케이터 */}
                      <span style={{
                        width: 8, height: 8, borderRadius: '50%',
                        backgroundColor: color, flexShrink: 0,
                      }} />
                      {/* 회사명 */}
                      <span
                        onClick={() => setExpandedFleet(prev => (prev === id ? null : id))}
                        style={{
                          flex: 1,
                          color: '#e2e8f0',
                          overflow: 'hidden',
                          textOverflow: 'ellipsis',
                          whiteSpace: 'nowrap',
                          cursor: 'pointer',
                        }}
                        title={company ? `${company.nameCn} / ${company.nameEn}` : companyName}
                      >
                        {companyName}
                      </span>
                      {/* 선박 수 */}
                      <span style={{ color: '#64748b', fontSize: 10, flexShrink: 0 }}>
                        ({mmsiList.length}척)
                      </span>
                      {/* zoom 버튼 */}
                      <button
                        type="button"
                        onClick={e => { e.stopPropagation(); handleFleetZoom(id); }}
                        style={{
                          background: 'none',
                          border: '1px solid rgba(99,179,237,0.3)',
                          borderRadius: 3,
                          color: '#63b3ed',
                          fontSize: 9,
                          cursor: 'pointer',
                          padding: '1px 4px',
                          flexShrink: 0,
                        }}
                        title="이 선단으로 지도 이동"
                      >
                        zoom
                      </button>
                    </div>

                    {/* 선단 상세 */}
                    {isOpen && (
                      <div style={{
                        paddingLeft: 22,
                        paddingRight: 10,
                        paddingBottom: 6,
                        fontSize: 10,
                        color: '#94a3b8',
                        borderLeft: `2px solid ${color}33`,
                        marginLeft: 10,
                      }}>
                        {/* 선박 목록 */}
                        <div style={{ color: '#64748b', fontSize: 9, marginBottom: 3 }}>선박:</div>
                        {displayMembers.map(m => {
                          const dto = analysisMap.get(m.mmsi);
                          const role = dto?.algorithms.fleetRole.role ?? m.role;
                          const displayName = m.name || m.mmsi;
                          return (
                            <div
                              key={m.mmsi}
                              style={{
                                display: 'flex',
                                alignItems: 'center',
                                gap: 4,
                                marginBottom: 2,
                              }}
                            >
                              <span style={{ flex: 1, color: '#cbd5e1', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                                {displayName}
                              </span>
                              <span style={{ color: role === 'LEADER' ? '#fbbf24' : '#64748b', fontSize: 9, flexShrink: 0 }}>
                                ({role === 'LEADER' ? 'MAIN' : 'SUB'})
                              </span>
                              <button
                                type="button"
                                onClick={() => onShipSelect?.(m.mmsi)}
                                style={{
                                  background: 'none',
                                  border: 'none',
                                  color: '#63b3ed',
                                  fontSize: 10,
                                  cursor: 'pointer',
                                  padding: '0 2px',
                                  flexShrink: 0,
                                }}
                                title="선박으로 이동"
                                aria-label={`${displayName} 선박으로 이동`}
                              >
                                ▶
                              </button>
                            </div>
                          );
                        })}
                      </div>
                    )}
                  </div>
                );
              })
            )}

            </div>
          )}

          {/* ── 조업구역내 어구 그룹 섹션 ── */}
              <div style={{ ...headerStyle, borderTop: '1px solid rgba(220,38,38,0.35)', cursor: 'pointer' }} onClick={() => toggleSection('inZone')}>
                <span style={{ fontWeight: 700, color: '#dc2626', letterSpacing: 0.3 }}>
                  조업구역내 어구 ({inZoneGearGroups.length}개)
                </span>
                <button style={toggleButtonStyle} aria-label="조업구역내 어구 접기/펴기">
                  {activeSection === 'inZone' ? '▲' : '▼'}
                </button>
              </div>
              {activeSection === 'inZone' && (
                <div style={{ padding: '4px 0', overflowY: 'auto', flex: 1 }}>
                  {inZoneGearGroups.map(g => {
                    const name = g.groupKey;
                    const isOpen = expandedGearGroup === name;
                    const accentColor = '#dc2626';
                    const parentMember = g.members.find(m => m.isParent);
                    const gearMembers = g.members.filter(m => !m.isParent);
                    const zoneName = g.zoneName ?? '';
                    return (
                      <div key={name} id={`gear-row-${name}`}>
                        <div
                          style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '3px 10px', cursor: 'pointer', borderLeft: isOpen ? `2px solid ${accentColor}` : '2px solid transparent', transition: 'background-color 0.1s' }}
                          onMouseEnter={e => { (e.currentTarget as HTMLDivElement).style.backgroundColor = 'rgba(220,38,38,0.06)'; }}
                          onMouseLeave={e => { (e.currentTarget as HTMLDivElement).style.backgroundColor = 'transparent'; }}
                        >
                          <span onClick={() => setExpandedGearGroup(prev => (prev === name ? null : name))} style={{ color: '#94a3b8', fontSize: 9, flexShrink: 0, cursor: 'pointer' }}>{isOpen ? '▾' : '▸'}</span>
                          <span style={{ width: 8, height: 8, borderRadius: '50%', backgroundColor: accentColor, flexShrink: 0 }} />
                          <span onClick={() => setExpandedGearGroup(prev => (prev === name ? null : name))} style={{ flex: 1, color: '#e2e8f0', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', cursor: 'pointer' }} title={`${name} — ${zoneName}`}>{name}</span>
                          {parentMember && <span style={{ color: '#fbbf24', fontSize: 8, flexShrink: 0 }} title={`모선: ${parentMember.name}`}>⚓</span>}
                          <span style={{ color: '#ef4444', fontSize: 9, flexShrink: 0 }}>{zoneName}</span>
                          <span style={{ color: '#64748b', fontSize: 10, flexShrink: 0 }}>({gearMembers.length})</span>
                          <button type="button" onClick={e => { e.stopPropagation(); handleGearGroupZoom(name); }} style={{ background: 'none', border: `1px solid rgba(220,38,38,0.5)`, borderRadius: 3, color: accentColor, fontSize: 9, cursor: 'pointer', padding: '1px 4px', flexShrink: 0 }} title="이 어구 그룹으로 지도 이동">zoom</button>
                        </div>
                        {isOpen && (
                          <div style={{ paddingLeft: 24, paddingRight: 10, paddingBottom: 4, fontSize: 9, color: '#94a3b8', borderLeft: `2px solid rgba(220,38,38,0.25)`, marginLeft: 10 }}>
                            {parentMember && <div style={{ color: '#fbbf24', marginBottom: 2 }}>모선: {parentMember.name || parentMember.mmsi}</div>}
                            <div style={{ color: '#64748b', marginBottom: 2 }}>어구 목록:</div>
                            {gearMembers.map(m => (
                              <div key={m.mmsi} style={{ display: 'flex', alignItems: 'center', gap: 4, marginBottom: 1 }}>
                                <span style={{ flex: 1, color: '#475569', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{m.name || m.mmsi}</span>
                                <button type="button" onClick={() => onShipSelect?.(m.mmsi)} style={{ background: 'none', border: 'none', color: accentColor, fontSize: 10, cursor: 'pointer', padding: '0 2px', flexShrink: 0 }} title="어구 위치로 이동">▶</button>
                              </div>
                            ))}
                          </div>
                        )}
                      </div>
                    );
                  })}
                </div>
              )}

          {/* ── 비허가 어구 그룹 섹션 ── */}
              <div style={{ ...headerStyle, borderTop: '1px solid rgba(249,115,22,0.25)', cursor: 'pointer' }} onClick={() => toggleSection('outZone')}>
                <span style={{ fontWeight: 700, color: '#f97316', letterSpacing: 0.3 }}>
                  비허가 어구 ({outZoneGearGroups.length}개)
                </span>
                <button type="button" style={toggleButtonStyle} aria-label="비허가 어구 접기/펴기">
                  {activeSection === 'outZone' ? '▲' : '▼'}
                </button>
              </div>
              {activeSection === 'outZone' && (
                <div style={{ padding: '4px 0', overflowY: 'auto', flex: 1 }}>
                  {outZoneGearGroups.map(g => {
                  const name = g.groupKey;
                  const isOpen = expandedGearGroup === name;
                  const parentMember = g.members.find(m => m.isParent);
                  const gearMembers = g.members.filter(m => !m.isParent);
                  return (
                    <div key={name} id={`gear-row-${name}`}>
                      <div
                        style={{
                          display: 'flex',
                          alignItems: 'center',
                          gap: 4,
                          padding: '3px 10px',
                          cursor: 'pointer',
                          borderLeft: isOpen ? '2px solid #f97316' : '2px solid transparent',
                          transition: 'background-color 0.1s',
                        }}
                        onMouseEnter={e => { (e.currentTarget as HTMLDivElement).style.backgroundColor = 'rgba(255,255,255,0.04)'; }}
                        onMouseLeave={e => { (e.currentTarget as HTMLDivElement).style.backgroundColor = 'transparent'; }}
                      >
                        <span
                          onClick={() => setExpandedGearGroup(prev => (prev === name ? null : name))}
                          style={{ color: '#94a3b8', fontSize: 9, flexShrink: 0, cursor: 'pointer' }}
                        >
                          {isOpen ? '▾' : '▸'}
                        </span>
                        <span style={{
                          width: 8, height: 8, borderRadius: '50%',
                          backgroundColor: '#f97316', flexShrink: 0,
                        }} />
                        <span
                          onClick={() => setExpandedGearGroup(prev => (prev === name ? null : name))}
                          style={{
                            flex: 1,
                            color: '#e2e8f0',
                            overflow: 'hidden',
                            textOverflow: 'ellipsis',
                            whiteSpace: 'nowrap',
                            cursor: 'pointer',
                          }}
                          title={name}
                        >
                          {name}
                        </span>
                        {parentMember && <span style={{ color: '#fbbf24', fontSize: 8, flexShrink: 0 }} title={`모선: ${parentMember.name}`}>⚓</span>}
                        <span style={{ color: '#64748b', fontSize: 10, flexShrink: 0 }}>
                          ({gearMembers.length}개)
                        </span>
                        <button
                          type="button"
                          onClick={e => { e.stopPropagation(); handleGearGroupZoom(name); }}
                          style={{
                            background: 'none',
                            border: '1px solid rgba(249,115,22,0.4)',
                            borderRadius: 3,
                            color: '#f97316',
                            fontSize: 9,
                            cursor: 'pointer',
                            padding: '1px 4px',
                            flexShrink: 0,
                          }}
                          title="이 어구 그룹으로 지도 이동"
                        >
                          zoom
                        </button>
                      </div>

                      {isOpen && (
                        <div style={{
                          paddingLeft: 24,
                          paddingRight: 10,
                          paddingBottom: 4,
                          fontSize: 9,
                          color: '#94a3b8',
                          borderLeft: '2px solid rgba(249,115,22,0.2)',
                          marginLeft: 10,
                        }}>
                          {parentMember && (
                            <div style={{ color: '#fbbf24', marginBottom: 2 }}>
                              모선: {parentMember.name || parentMember.mmsi}
                            </div>
            )}
                          <div style={{ color: '#64748b', marginBottom: 2 }}>어구 목록:</div>
                          {gearMembers.map(m => (
                            <div key={m.mmsi} style={{
                              display: 'flex',
                              alignItems: 'center',
                              gap: 4,
                              marginBottom: 1,
                            }}>
                              <span style={{ flex: 1, color: '#475569', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                                {m.name || m.mmsi}
                              </span>
                              <button
                                type="button"
                                onClick={() => onShipSelect?.(m.mmsi)}
                                style={{
                                  background: 'none',
                                  border: 'none',
                                  color: '#f97316',
                                  fontSize: 10,
                                  cursor: 'pointer',
                                  padding: '0 2px',
                                  flexShrink: 0,
                                }}
                                title="어구 위치로 이동"
                                aria-label={`${m.name || m.mmsi} 위치로 이동`}
                              >
                                ▶
                              </button>
                            </div>
                          ))}
                        </div>
                      )}
                    </div>
                  );
                })}
                </div>
              )}
      </div>
    </>
  );
}

export default FleetClusterLayer;
