import { useState, useMemo, useEffect, useCallback, useRef } from 'react';
import { Map, Marker, Popup, Source, Layer, useMap } from '@vis.gl/react-maplibre';
import {
  ScatterplotLayer,
  PathLayer,
  TextLayer,
  BitmapLayer,
  PolygonLayer,
  GeoJsonLayer,
} from '@deck.gl/layers';
import type { PickingInfo, Layer as DeckLayer } from '@deck.gl/core';
import type { MapLayerMouseEvent } from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css';
import { layerDatabase } from '@common/services/layerService';
import type { PredictionModel, SensitiveResource } from '@tabs/prediction/components/OilSpillView';
import type {
  HydrDataStep,
  SensitiveResourceFeatureCollection,
} from '@tabs/prediction/services/predictionApi';
import HydrParticleOverlay from './HydrParticleOverlay';
import type { BoomLine, BoomLineCoord } from '@common/types/boomLine';
import type { ReplayShip, CollisionEvent, BackwardParticleStep } from '@common/types/backtrack';
import { createBacktrackLayers } from './BacktrackReplayOverlay';
import { buildMeasureLayers } from './measureLayers';
import { MeasureOverlay } from './MeasureOverlay';
import { useMeasureTool } from '@common/hooks/useMeasureTool';
import { hexToRgba } from './mapUtils';
import { S57EncOverlay } from './S57EncOverlay';
import { SrOverlay } from './SrOverlay';
import { DeckGLOverlay } from './DeckGLOverlay';
import { FlyToController } from './FlyToController';
import { useMapStore } from '@common/store/mapStore';
import { useBaseMapStyle } from '@common/hooks/useBaseMapStyle';

const GEOSERVER_URL = import.meta.env.VITE_GEOSERVER_URL || 'http://localhost:8080';

// 인천 송도 국제도시
const DEFAULT_CENTER: [number, number] = [37.39, 126.64];
const DEFAULT_ZOOM = 10;

// 모델별 색상 매핑
const MODEL_COLORS: Record<PredictionModel, string> = {
  KOSPS: '#06b6d4',
  POSEIDON: '#ef4444',
  OpenDrift: '#3b82f6',
};

// 오일펜스 우선순위별 색상/두께
const PRIORITY_COLORS: Record<string, string> = {
  CRITICAL: '#ef4444',
  HIGH: '#f97316',
  MEDIUM: '#eab308',
};
const PRIORITY_WEIGHTS: Record<string, number> = {
  CRITICAL: 4,
  HIGH: 3,
  MEDIUM: 2,
};
const PRIORITY_LABELS: Record<string, string> = {
  CRITICAL: '긴급',
  HIGH: '중요',
  MEDIUM: '보통',
};

function hslToRgb(h: number, s: number, l: number): [number, number, number] {
  const a = s * Math.min(l, 1 - l);
  const f = (n: number) => {
    const k = (n + h * 12) % 12;
    return l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1));
  };
  return [Math.round(f(0) * 255), Math.round(f(8) * 255), Math.round(f(4) * 255)];
}

function categoryToRgb(category: string): [number, number, number] {
  let hash = 0;
  for (let i = 0; i < category.length; i++) {
    hash = (hash * 31 + category.charCodeAt(i)) >>> 0;
  }
  const hue = (hash * 137) % 360;
  return hslToRgb(hue / 360, 0.65, 0.55);
}

const SENSITIVE_COLORS: Record<string, string> = {
  aquaculture: '#22c55e',
  beach: '#0ea5e9',
  ecology: '#eab308',
  intake: '#a855f7',
};
const SENSITIVE_ICONS: Record<string, string> = {
  aquaculture: '🐟',
  beach: '🏖',
  ecology: '🦅',
  intake: '🚰',
};

interface DispersionZone {
  level: string;
  color: string;
  radius: number;
  angle: number;
}

interface DispersionResult {
  zones: DispersionZone[];
  timestamp: string;
  windDirection: number;
  substance: string;
  concentration: Record<string, string>;
}

interface MapViewProps {
  center?: [number, number];
  zoom?: number;
  enabledLayers?: Set<string>;
  incidentCoord?: { lon: number; lat: number };
  isSelectingLocation?: boolean;
  onMapClick?: (lon: number, lat: number) => void;
  oilTrajectory?: Array<{
    lat: number;
    lon: number;
    time: number;
    particle?: number;
    model?: string;
    stranded?: 0 | 1;
  }>;
  selectedModels?: Set<PredictionModel>;
  dispersionResult?: DispersionResult | null;
  dispersionHeatmap?: Array<{ lon: number; lat: number; concentration: number }>;
  boomLines?: BoomLine[];
  showBoomLines?: boolean;
  isDrawingBoom?: boolean;
  drawingPoints?: BoomLineCoord[];
  layerOpacity?: number;
  layerBrightness?: number;
  layerColors?: Record<string, string>;
  backtrackReplay?: {
    isActive: boolean;
    ships: ReplayShip[];
    collisionEvent: CollisionEvent | null;
    replayFrame: number;
    totalFrames: number;
    incidentCoord: { lat: number; lon: number };
    backwardParticles?: BackwardParticleStep[];
  };
  sensitiveResources?: SensitiveResource[];
  sensitiveResourceGeojson?: SensitiveResourceFeatureCollection | null;
  flyToTarget?: { lng: number; lat: number; zoom?: number } | null;
  fitBoundsTarget?: { north: number; south: number; east: number; west: number } | null;
  centerPoints?: Array<{ lat: number; lon: number; time: number; model?: string }>;
  windData?: Array<Array<{ lat: number; lon: number; wind_speed: number; wind_direction: number }>>;
  hydrData?: (HydrDataStep | null)[];
  // 외부 플레이어 제어 (prediction 하단 바에서 제어할 때 사용)
  externalCurrentTime?: number;
  mapCaptureRef?: React.MutableRefObject<(() => Promise<string | null>) | null>;
  onIncidentFlyEnd?: () => void;
  flyToIncident?: { lon: number; lat: number };
  showCurrent?: boolean;
  showWind?: boolean;
  showBeached?: boolean;
  showTimeLabel?: boolean;
  simulationStartTime?: string;
  drawAnalysisMode?: 'polygon' | 'circle' | null;
  analysisPolygonPoints?: Array<{ lat: number; lon: number }>;
  analysisCircleCenter?: { lat: number; lon: number } | null;
  analysisCircleRadiusM?: number;
  /** false로 설정 시 WeatherInfoPanel, MapLegend, CoordinateDisplay 숨김 (기본: true) */
  showOverlays?: boolean;
}

// DeckGLOverlay, FlyToController → @common/components/map/DeckGLOverlay, FlyToController 에서 import

// fitBounds 트리거 컴포넌트 (Map 내부에서 useMap() 사용)
function FitBoundsController({
  fitBoundsTarget,
}: {
  fitBoundsTarget?: { north: number; south: number; east: number; west: number } | null;
}) {
  const { current: map } = useMap();
  useEffect(() => {
    if (!map || !fitBoundsTarget) return;
    map.fitBounds(
      [
        [fitBoundsTarget.west, fitBoundsTarget.south],
        [fitBoundsTarget.east, fitBoundsTarget.north],
      ],
      { padding: 80, duration: 1200, maxZoom: 12 },
    );
  }, [fitBoundsTarget, map]);
  return null;
}

// Map 중앙 좌표 + 줌 추적 컴포넌트 (Map 내부에서 useMap() 사용)
function MapCenterTracker({
  onCenterChange,
}: {
  onCenterChange: (lat: number, lng: number, zoom: number) => void;
}) {
  const { current: map } = useMap();

  useEffect(() => {
    if (!map) return;

    const update = () => {
      const center = map.getCenter();
      const zoom = map.getZoom();
      onCenterChange(center.lat, center.lng, zoom);
    };

    update();
    map.on('move', update);
    map.on('zoom', update);

    return () => {
      map.off('move', update);
      map.off('zoom', update);
    };
  }, [map, onCenterChange]);

  return null;
}

// 3D 모드 pitch/bearing 제어 컴포넌트 (Map 내부에서 useMap() 사용)
function MapPitchController({ threeD }: { threeD: boolean }) {
  const { current: map } = useMap();
  useEffect(() => {
    if (!map) return;
    map.easeTo(
      threeD ? { pitch: 45, bearing: -17, duration: 800 } : { pitch: 0, bearing: 0, duration: 800 },
    );
  }, [threeD, map]);
  return null;
}

// 사고 지점 변경 시 지도 이동 (Map 내부 컴포넌트)
function MapFlyToIncident({
  coord,
  onFlyEnd,
}: {
  coord?: { lon: number; lat: number };
  onFlyEnd?: () => void;
}) {
  const { current: map } = useMap();
  const onFlyEndRef = useRef(onFlyEnd);
  useEffect(() => {
    onFlyEndRef.current = onFlyEnd;
  }, [onFlyEnd]);

  useEffect(() => {
    if (!map || !coord) return;

    const { lon, lat } = coord;
    const doFly = () => {
      map.flyTo({ center: [lon, lat], zoom: 11, duration: 1200 });
      map.once('moveend', () => onFlyEndRef.current?.());
    };

    if (map.loaded()) {
      doFly();
    } else {
      map.once('load', doFly);
    }
  }, [coord, map]); // 객체 참조 추적: 같은 좌표라도 새 객체면 effect 재실행

  return null;
}

// 지도 캡처 지원 (map.once('render') 후 캡처로 빈 캔버스 문제 방지)
function MapCaptureSetup({
  captureRef,
}: {
  captureRef: React.MutableRefObject<(() => Promise<string | null>) | null>;
}) {
  const { current: map } = useMap();
  useEffect(() => {
    if (!map) return;
    captureRef.current = () =>
      new Promise<string | null>((resolve) => {
        map.once('render', () => {
          try {
            // WebGL 캔버스는 alpha=0 투명 배경이므로 불투명 배경과 합성 후 추출
            // 최대 1200px로 리사이즈 + JPEG 압축으로 전송 크기 절감
            const src = map.getCanvas();
            const maxW = 1200;
            const scale = src.width > maxW ? maxW / src.width : 1;
            const composite = document.createElement('canvas');
            composite.width = Math.round(src.width * scale);
            composite.height = Math.round(src.height * scale);
            const ctx = composite.getContext('2d')!;
            ctx.fillStyle = '#0f1117';
            ctx.fillRect(0, 0, composite.width, composite.height);
            ctx.drawImage(src, 0, 0, composite.width, composite.height);
            resolve(composite.toDataURL('image/jpeg', 0.82));
          } catch {
            resolve(null);
          }
        });
        map.triggerRepaint();
      });
  }, [map, captureRef]);
  return null;
}

// 팝업 정보
interface PopupInfo {
  longitude: number;
  latitude: number;
  content: React.ReactNode;
}

export function MapView({
  center = DEFAULT_CENTER,
  zoom = DEFAULT_ZOOM,
  enabledLayers = new Set(),
  incidentCoord,
  isSelectingLocation = false,
  onMapClick,
  oilTrajectory = [],
  selectedModels = new Set(['OpenDrift'] as PredictionModel[]),
  dispersionResult = null,
  dispersionHeatmap = [],
  boomLines = [],
  showBoomLines = true,
  isDrawingBoom = false,
  drawingPoints = [],
  layerOpacity = 50,
  layerBrightness = 50,
  layerColors,
  backtrackReplay,
  sensitiveResources = [],
  sensitiveResourceGeojson,
  flyToTarget,
  fitBoundsTarget,
  centerPoints = [],
  windData = [],
  hydrData = [],
  externalCurrentTime,
  mapCaptureRef,
  onIncidentFlyEnd,
  flyToIncident,
  showCurrent = true,
  showWind = true,
  showBeached = false,
  showTimeLabel = false,
  simulationStartTime,
  drawAnalysisMode = null,
  analysisPolygonPoints = [],
  analysisCircleCenter,
  analysisCircleRadiusM = 0,
  showOverlays = true,
}: MapViewProps) {
  const lightMode = true;
  const { mapToggles, measureMode, measureInProgress, measurements } = useMapStore();
  const { handleMeasureClick } = useMeasureTool();
  const isControlled = externalCurrentTime !== undefined;
  const [currentPosition, setCurrentPosition] = useState<[number, number]>(DEFAULT_CENTER);
  const [mapCenter, setMapCenter] = useState<[number, number]>(DEFAULT_CENTER);
  const [mapZoom, setMapZoom] = useState<number>(DEFAULT_ZOOM);
  const [internalCurrentTime, setInternalCurrentTime] = useState(0);
  const [isPlaying, setIsPlaying] = useState(false);
  const [playbackSpeed, setPlaybackSpeed] = useState(1);
  const [popupInfo, setPopupInfo] = useState<PopupInfo | null>(null);
  // deck.gl 레이어 클릭 시 MapLibre 맵 클릭 핸들러 차단용 플래그 (민감자원 등)
  const deckClickHandledRef = useRef(false);
  // 클릭으로 열린 팝업(닫기 전까지 유지) 추적 — 호버 핸들러가 닫지 않도록 방지
  const persistentPopupRef = useRef(false);
  // 현재 호버 중인 민감자원 feature properties (handleMapClick에서 팝업 생성에 사용)
  const hoveredSensitiveRef = useRef<Record<string, unknown> | null>(null);
  const currentTime = isControlled ? externalCurrentTime : internalCurrentTime;

  const handleMapCenterChange = useCallback((lat: number, lng: number, zoom: number) => {
    setMapCenter([lat, lng]);
    setMapZoom(zoom);
  }, []);

  const handleMapClick = useCallback(
    (e: MapLayerMouseEvent) => {
      const { lng, lat } = e.lngLat;
      setCurrentPosition([lat, lng]);
      // deck.gl 다른 레이어 onClick이 처리한 클릭 — 팝업 유지
      if (deckClickHandledRef.current) {
        deckClickHandledRef.current = false;
        return;
      }
      // 민감자원 hover 중이면 팝업 표시
      if (hoveredSensitiveRef.current) {
        const props = hoveredSensitiveRef.current;
        const { category, ...rest } = props;
        const entries = Object.entries(rest).filter(
          ([k, v]) => k !== 'srId' && v !== null && v !== undefined && v !== '',
        );
        persistentPopupRef.current = true;
        setPopupInfo({
          longitude: lng,
          latitude: lat,
          content: (
            <div
              className="text-caption font-korean"
              style={{ minWidth: '180px', maxWidth: '260px' }}
            >
              <div className="font-semibold mb-1.5 pb-1 border-b border-[rgba(0,0,0,0.12)]">
                {String(category ?? '민감자원')}
              </div>
              {entries.length > 0 ? (
                <div className="space-y-0.5">
                  {entries.map(([key, val]) => (
                    <div key={key} className="flex gap-2 justify-between">
                      <span className="text-caption text-[#888] shrink-0">{key}</span>
                      <span className="text-caption text-[#333] font-medium text-right break-all">
                        {typeof val === 'object' ? JSON.stringify(val) : String(val)}
                      </span>
                    </div>
                  ))}
                </div>
              ) : (
                <p className="text-caption text-[#999]">상세 정보 없음</p>
              )}
            </div>
          ),
        });
        return;
      }
      if (measureMode !== null) {
        handleMeasureClick(lng, lat);
        return;
      }
      if (onMapClick) {
        onMapClick(lng, lat);
      }
      setPopupInfo(null);
    },
    [onMapClick, measureMode, handleMeasureClick],
  );

  // 애니메이션 재생 로직 (외부 제어 모드에서는 비활성)
  useEffect(() => {
    if (isControlled || !isPlaying || oilTrajectory.length === 0) return;

    const maxTime = Math.max(...oilTrajectory.map((p) => p.time));
    if (internalCurrentTime >= maxTime) {
      setIsPlaying(false);
      return;
    }

    const interval = setInterval(() => {
      setInternalCurrentTime((prev) => {
        const next = prev + 1 * playbackSpeed;
        return next > maxTime ? maxTime : next;
      });
    }, 200);

    return () => clearInterval(interval);
  }, [isControlled, isPlaying, internalCurrentTime, playbackSpeed, oilTrajectory]);

  // 시뮬레이션 시작 시 자동으로 애니메이션 재생 (외부 제어 모드에서는 비활성)
  useEffect(() => {
    if (isControlled) return;
    if (oilTrajectory.length > 0) {
      setInternalCurrentTime(0);
      setIsPlaying(true);
    }
  }, [isControlled, oilTrajectory.length]);

  // WMS 레이어 목록
  const wmsLayers = useMemo(() => {
    return Array.from(enabledLayers)
      .map((layerId) => {
        const layer = layerDatabase.find((l) => l.id === layerId);
        return layer?.wmsLayer ? { id: layerId, wmsLayer: layer.wmsLayer } : null;
      })
      .filter((l): l is { id: string; wmsLayer: string } => l !== null);
  }, [enabledLayers]);

  // WMS 밝기 값 (MapLibre raster paint)
  const wmsBrightnessMax = Math.min(layerBrightness / 50, 2);
  const wmsBrightnessMin = wmsBrightnessMax > 1 ? wmsBrightnessMax - 1 : 0;
  const wmsOpacity = layerOpacity / 100;

  // deck.gl 레이어 구축
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const deckLayers = useMemo((): any[] => {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const result: any[] = [];

    // --- 유류 확산 입자 (ScatterplotLayer) ---
    const visibleParticles = oilTrajectory.filter((p) => p.time <= currentTime);
    const activeStep =
      visibleParticles.length > 0 ? Math.max(...visibleParticles.map((p) => p.time)) : -1;
    if (visibleParticles.length > 0) {
      result.push(
        new ScatterplotLayer({
          id: 'oil-particles',
          data: visibleParticles,
          getPosition: (d: (typeof visibleParticles)[0]) => [d.lon, d.lat],
          getRadius: 3,
          getFillColor: (d: (typeof visibleParticles)[0]) => {
            const modelKey = (d.model ||
              Array.from(selectedModels)[0] ||
              'OpenDrift') as PredictionModel;
            // 1순위: stranded 입자 → showBeached=true 시 모델 색, false 시 회색
            if (d.stranded === 1)
              return showBeached
                ? hexToRgba(MODEL_COLORS[modelKey] || '#3b82f6', 180)
                : ([130, 130, 130, 70] as [number, number, number, number]);
            // 2순위: 현재 활성 스텝 → 모델 기본 색상
            if (d.time === activeStep) return hexToRgba(MODEL_COLORS[modelKey] || '#3b82f6', 180);
            // 3순위: 과거 스텝 → 회색 + 투명
            return [130, 130, 130, 70] as [number, number, number, number];
          },
          radiusMinPixels: 2.5,
          radiusMaxPixels: 5,
          pickable: true,
          onClick: (info: PickingInfo) => {
            if (info.object) {
              const d = info.object as (typeof visibleParticles)[0];
              const modelKey = d.model || Array.from(selectedModels)[0] || 'OpenDrift';
              setPopupInfo({
                longitude: d.lon,
                latitude: d.lat,
                content: (
                  <div className="text-caption">
                    <strong>
                      {modelKey} 입자 #{(d.particle ?? 0) + 1}
                    </strong>
                    {d.stranded === 1 && <span className="text-red-400"> (육지 부착)</span>}
                    <br />
                    시간: +{d.time}h
                    <br />
                    위치: {d.lat.toFixed(4)}°, {d.lon.toFixed(4)}°
                  </div>
                ),
              });
            }
          },
          updateTriggers: {
            getFillColor: [selectedModels, currentTime, showBeached],
          },
        }),
      );
    }

    // --- 육지부착 hollow ring (stranded 모양 구분) ---
    const strandedParticles = showBeached ? visibleParticles.filter((p) => p.stranded === 1) : [];
    if (strandedParticles.length > 0) {
      result.push(
        new ScatterplotLayer({
          id: 'oil-stranded-ring',
          data: strandedParticles,
          getPosition: (d: (typeof strandedParticles)[0]) => [d.lon, d.lat],
          stroked: true,
          filled: false,
          getLineColor: (d: (typeof strandedParticles)[0]) => {
            const modelKey = (d.model ||
              Array.from(selectedModels)[0] ||
              'OpenDrift') as PredictionModel;
            return hexToRgba(MODEL_COLORS[modelKey] || '#3b82f6', 255);
          },
          lineWidthMinPixels: 2,
          getRadius: 4,
          radiusMinPixels: 5,
          radiusMaxPixels: 8,
          updateTriggers: {
            getLineColor: [selectedModels],
          },
        }),
      );
    }

    // --- 오일펜스 라인 (PathLayer) ---
    if (showBoomLines && boomLines.length > 0) {
      result.push(
        new PathLayer({
          id: 'boom-lines',
          data: boomLines,
          getPath: (d: BoomLine) => d.coords.map((c) => [c.lon, c.lat] as [number, number]),
          getColor: (d: BoomLine) => hexToRgba(PRIORITY_COLORS[d.priority] || '#f59e0b', 230),
          getWidth: (d: BoomLine) => PRIORITY_WEIGHTS[d.priority] || 2,
          getDashArray: (d: BoomLine) => (d.status === 'PLANNED' ? [10, 5] : [0, 0]),
          dashJustified: true,
          widthMinPixels: 2,
          widthMaxPixels: 6,
          pickable: true,
          onClick: (info: PickingInfo) => {
            if (info.object) {
              const d = info.object as BoomLine;
              setPopupInfo({
                longitude: info.coordinate?.[0] ?? 0,
                latitude: info.coordinate?.[1] ?? 0,
                content: (
                  <div className="text-caption" style={{ minWidth: '140px' }}>
                    <strong style={{ color: PRIORITY_COLORS[d.priority] }}>{d.name}</strong>
                    <br />
                    우선순위: {PRIORITY_LABELS[d.priority] || d.priority}
                    <br />
                    길이: {d.length.toFixed(0)}m
                    <br />
                    각도: {d.angle.toFixed(0)}°
                    <br />
                    차단 효율: {d.efficiency}%
                  </div>
                ),
              });
            }
          },
        }),
      );

      // 오일펜스 끝점 마커
      const endpoints: Array<{
        position: [number, number];
        color: [number, number, number, number];
      }> = [];
      boomLines.forEach((line) => {
        if (line.coords.length >= 2) {
          const c = hexToRgba(PRIORITY_COLORS[line.priority] || '#f59e0b', 230);
          endpoints.push({ position: [line.coords[0].lon, line.coords[0].lat], color: c });
          endpoints.push({
            position: [
              line.coords[line.coords.length - 1].lon,
              line.coords[line.coords.length - 1].lat,
            ],
            color: c,
          });
        }
      });
      if (endpoints.length > 0) {
        result.push(
          new ScatterplotLayer({
            id: 'boom-endpoints',
            data: endpoints,
            getPosition: (d: (typeof endpoints)[0]) => d.position,
            getRadius: 5,
            getFillColor: (d: (typeof endpoints)[0]) => d.color,
            getLineColor: [255, 255, 255, 255],
            getLineWidth: 2,
            stroked: true,
            radiusMinPixels: 5,
            radiusMaxPixels: 8,
          }),
        );
      }
    }

    // --- 드로잉 미리보기 ---
    if (isDrawingBoom && drawingPoints.length > 0) {
      result.push(
        new PathLayer({
          id: 'drawing-preview',
          data: [{ path: drawingPoints.map((c) => [c.lon, c.lat] as [number, number]) }],
          getPath: (d: { path: [number, number][] }) => d.path,
          getColor: [245, 158, 11, 200],
          getWidth: 3,
          getDashArray: [10, 6],
          dashJustified: true,
          widthMinPixels: 3,
        }),
      );
      result.push(
        new ScatterplotLayer({
          id: 'drawing-points',
          data: drawingPoints.map((c) => ({ position: [c.lon, c.lat] as [number, number] })),
          getPosition: (d: { position: [number, number] }) => d.position,
          getRadius: 4,
          getFillColor: [245, 158, 11, 255],
          getLineColor: [255, 255, 255, 255],
          getLineWidth: 2,
          stroked: true,
          radiusMinPixels: 4,
          radiusMaxPixels: 6,
        }),
      );
    }

    // --- 오염분석 다각형 그리기 ---
    if (analysisPolygonPoints.length > 0) {
      if (analysisPolygonPoints.length >= 3) {
        result.push(
          new PolygonLayer({
            id: 'analysis-polygon-fill',
            data: [
              { polygon: analysisPolygonPoints.map((p) => [p.lon, p.lat] as [number, number]) },
            ],
            getPolygon: (d: { polygon: [number, number][] }) => d.polygon,
            getFillColor: [168, 85, 247, 40],
            getLineColor: [168, 85, 247, 220],
            getLineWidth: 2,
            stroked: true,
            filled: true,
            lineWidthMinPixels: 2,
          }),
        );
      }
      result.push(
        new PathLayer({
          id: 'analysis-polygon-outline',
          data: [
            {
              path: [
                ...analysisPolygonPoints.map((p) => [p.lon, p.lat] as [number, number]),
                ...(analysisPolygonPoints.length >= 3
                  ? [
                      [analysisPolygonPoints[0].lon, analysisPolygonPoints[0].lat] as [
                        number,
                        number,
                      ],
                    ]
                  : []),
              ],
            },
          ],
          getPath: (d: { path: [number, number][] }) => d.path,
          getColor: [168, 85, 247, 220],
          getWidth: 2,
          getDashArray: [8, 4],
          dashJustified: true,
          widthMinPixels: 2,
        }),
      );
      result.push(
        new ScatterplotLayer({
          id: 'analysis-polygon-points',
          data: analysisPolygonPoints.map((p) => ({
            position: [p.lon, p.lat] as [number, number],
          })),
          getPosition: (d: { position: [number, number] }) => d.position,
          getRadius: 5,
          getFillColor: [168, 85, 247, 255],
          getLineColor: [255, 255, 255, 255],
          getLineWidth: 2,
          stroked: true,
          radiusMinPixels: 5,
          radiusMaxPixels: 8,
        }),
      );
    }

    // --- 오염분석 원 그리기 ---
    if (analysisCircleCenter) {
      result.push(
        new ScatterplotLayer({
          id: 'analysis-circle-center',
          data: [
            { position: [analysisCircleCenter.lon, analysisCircleCenter.lat] as [number, number] },
          ],
          getPosition: (d: { position: [number, number] }) => d.position,
          getRadius: 6,
          getFillColor: [168, 85, 247, 255],
          getLineColor: [255, 255, 255, 255],
          getLineWidth: 2,
          stroked: true,
          radiusMinPixels: 6,
          radiusMaxPixels: 9,
        }),
      );
      if (analysisCircleRadiusM > 0) {
        result.push(
          new ScatterplotLayer({
            id: 'analysis-circle-area',
            data: [
              {
                position: [analysisCircleCenter.lon, analysisCircleCenter.lat] as [number, number],
              },
            ],
            getPosition: (d: { position: [number, number] }) => d.position,
            getRadius: analysisCircleRadiusM,
            radiusUnits: 'meters',
            getFillColor: [168, 85, 247, 35],
            getLineColor: [168, 85, 247, 200],
            getLineWidth: 2,
            stroked: true,
            filled: true,
            lineWidthMinPixels: 2,
          }),
        );
      }
    }

    // --- HNS 대기확산 히트맵 (BitmapLayer, 고정 이미지) ---
    if (dispersionHeatmap && dispersionHeatmap.length > 0) {
      const maxConc = Math.max(...dispersionHeatmap.map((p) => p.concentration));
      const minConc = Math.min(
        ...dispersionHeatmap.filter((p) => p.concentration > 0.01).map((p) => p.concentration),
      );
      const filtered = dispersionHeatmap.filter((p) => p.concentration > 0.01);
      console.log(
        '[MapView] HNS 히트맵:',
        dispersionHeatmap.length,
        '→ filtered:',
        filtered.length,
        'maxConc:',
        maxConc.toFixed(2),
      );

      if (filtered.length > 0) {
        // 경위도 바운드 계산
        let minLon = Infinity,
          maxLon = -Infinity,
          minLat = Infinity,
          maxLat = -Infinity;
        for (const p of dispersionHeatmap) {
          if (p.lon < minLon) minLon = p.lon;
          if (p.lon > maxLon) maxLon = p.lon;
          if (p.lat < minLat) minLat = p.lat;
          if (p.lat > maxLat) maxLat = p.lat;
        }
        const padLon = (maxLon - minLon) * 0.02;
        const padLat = (maxLat - minLat) * 0.02;
        minLon -= padLon;
        maxLon += padLon;
        minLat -= padLat;
        maxLat += padLat;

        // 캔버스에 농도 이미지 렌더링
        const W = 1200,
          H = 960;
        const canvas = document.createElement('canvas');
        canvas.width = W;
        canvas.height = H;
        const ctx = canvas.getContext('2d')!;
        ctx.clearRect(0, 0, W, H);

        // 로그 스케일: 농도 범위를 고르게 분포
        const logMin = Math.log(minConc);
        const logMax = Math.log(maxConc);
        const logRange = logMax - logMin || 1;

        const stops: [number, number, number, number][] = [
          [34, 197, 94, 220], // green  (저농도)
          [234, 179, 8, 235], // yellow
          [249, 115, 22, 245], // orange
          [239, 68, 68, 250], // red    (고농도)
          [185, 28, 28, 255], // dark red (초고농도)
        ];

        for (const p of filtered) {
          // 로그 스케일 정규화 (0~1)
          const ratio = Math.max(0, Math.min(1, (Math.log(p.concentration) - logMin) / logRange));
          const t = ratio * (stops.length - 1);
          const lo = Math.floor(t);
          const hi = Math.min(lo + 1, stops.length - 1);
          const f = t - lo;
          const r = Math.round(stops[lo][0] + (stops[hi][0] - stops[lo][0]) * f);
          const g = Math.round(stops[lo][1] + (stops[hi][1] - stops[lo][1]) * f);
          const b = Math.round(stops[lo][2] + (stops[hi][2] - stops[lo][2]) * f);
          const a = (stops[lo][3] + (stops[hi][3] - stops[lo][3]) * f) / 255;

          const px = ((p.lon - minLon) / (maxLon - minLon)) * W;
          const py = (1 - (p.lat - minLat) / (maxLat - minLat)) * H;

          ctx.fillStyle = `rgba(${r},${g},${b},${a.toFixed(2)})`;
          ctx.beginPath();
          ctx.arc(px, py, 6, 0, Math.PI * 2);
          ctx.fill();
        }

        // Canvas → data URL 변환 (interleaved MapboxOverlay에서 HTMLCanvasElement 직접 전달 시 렌더링 안 되는 문제 회피)
        const imageUrl = canvas.toDataURL('image/png');
        result.push(
          new BitmapLayer({
            id: 'hns-dispersion-bitmap',
            image: imageUrl,
            bounds: [minLon, minLat, maxLon, maxLat],
            opacity: 1.0,
            pickable: false,
          }) as unknown as DeckLayer,
        );
      }
    }

    // --- HNS 대기확산 구역 (ScatterplotLayer, meters 단위) ---
    if (dispersionResult && incidentCoord) {
      const zones = dispersionResult.zones.map((zone, idx) => ({
        position: [incidentCoord.lon, incidentCoord.lat] as [number, number],
        radius: zone.radius,
        fillColor: hexToRgba(zone.color, 100),
        lineColor: hexToRgba(zone.color, 180),
        level: zone.level,
        idx,
      }));

      result.push(
        new ScatterplotLayer({
          id: 'hns-zones',
          data: zones,
          getPosition: (d: (typeof zones)[0]) => d.position,
          getRadius: (d: (typeof zones)[0]) => d.radius,
          getFillColor: (d: (typeof zones)[0]) => d.fillColor,
          getLineColor: (d: (typeof zones)[0]) => d.lineColor,
          getLineWidth: 2,
          stroked: true,
          radiusUnits: 'meters' as const,
          pickable: true,
          autoHighlight: true,
          onHover: (info: PickingInfo) => {
            if (info.object && info.coordinate) {
              const zoneAreas = zones.map((z) => ({
                level: z.level,
                area: (Math.PI * z.radius * z.radius) / 1e6,
              }));
              const totalArea = (Math.PI * Math.max(...zones.map((z) => z.radius)) ** 2) / 1e6;
              setPopupInfo({
                longitude: info.coordinate[0],
                latitude: info.coordinate[1],
                content: (
                  <div className="text-caption leading-relaxed" style={{ minWidth: 180 }}>
                    <strong className="text-color-warning">
                      {dispersionResult.substance} 대기확산 면적
                    </strong>
                    <table style={{ width: '100%', marginTop: 4, borderCollapse: 'collapse' }}>
                      <tbody>
                        {zoneAreas.map((z) => (
                          <tr key={z.level} style={{ borderBottom: '1px solid rgba(0,0,0,0.08)' }}>
                            <td style={{ padding: '2px 0', fontSize: 10 }}>{z.level}</td>
                            <td
                              style={{
                                padding: '2px 0',
                                fontSize: 10,
                                textAlign: 'right',
                                fontFamily: 'monospace',
                              }}
                            >
                              {z.area.toFixed(3)} km²
                            </td>
                          </tr>
                        ))}
                        <tr>
                          <td style={{ padding: '3px 0 0', fontSize: 10, fontWeight: 700 }}>
                            총 면적
                          </td>
                          <td
                            style={{
                              padding: '3px 0 0',
                              fontSize: 10,
                              fontWeight: 700,
                              textAlign: 'right',
                              fontFamily: 'monospace',
                            }}
                          >
                            {totalArea.toFixed(3)} km²
                          </td>
                        </tr>
                      </tbody>
                    </table>
                  </div>
                ),
              });
            } else if (!info.object) {
              // 클릭으로 열린 팝업(어장 등)이 있으면 호버로 닫지 않음
              if (!persistentPopupRef.current) {
                setPopupInfo(null);
              }
            }
          },
        }),
      );
    }

    // --- 역추적 리플레이 ---
    if (backtrackReplay?.isActive) {
      result.push(
        ...createBacktrackLayers({
          replayShips: backtrackReplay.ships,
          collisionEvent: backtrackReplay.collisionEvent,
          replayFrame: backtrackReplay.replayFrame,
          totalFrames: backtrackReplay.totalFrames,
          incidentCoord: backtrackReplay.incidentCoord,
          backwardParticles: backtrackReplay.backwardParticles,
        }),
      );
    }

    // --- 민감자원 영역 (ScatterplotLayer) ---
    if (sensitiveResources.length > 0) {
      result.push(
        new ScatterplotLayer({
          id: 'sensitive-zones',
          data: sensitiveResources,
          getPosition: (d: SensitiveResource) => [d.lon, d.lat],
          getRadius: (d: SensitiveResource) => d.radiusM,
          getFillColor: (d: SensitiveResource) =>
            hexToRgba(SENSITIVE_COLORS[d.type] || '#22c55e', 40),
          getLineColor: (d: SensitiveResource) =>
            hexToRgba(SENSITIVE_COLORS[d.type] || '#22c55e', 150),
          getLineWidth: 2,
          stroked: true,
          radiusUnits: 'meters' as const,
          pickable: true,
          onClick: (info: PickingInfo) => {
            if (info.object) {
              const d = info.object as SensitiveResource;
              setPopupInfo({
                longitude: d.lon,
                latitude: d.lat,
                content: (
                  <div className="text-caption" style={{ minWidth: '130px' }}>
                    <div className="flex items-center gap-1 mb-1">
                      <span>{SENSITIVE_ICONS[d.type]}</span>
                      <strong style={{ color: SENSITIVE_COLORS[d.type] }}>{d.name}</strong>
                    </div>
                    <div className="text-caption text-[#666]">
                      반경: {d.radiusM}m<br />
                      도달 예상:{' '}
                      <strong style={{ color: d.arrivalTimeH <= 6 ? '#ef4444' : '#f97316' }}>
                        {d.arrivalTimeH}h
                      </strong>
                    </div>
                  </div>
                ),
              });
            }
          },
        }),
      );

      // 민감자원 중심 마커
      result.push(
        new ScatterplotLayer({
          id: 'sensitive-centers',
          data: sensitiveResources,
          getPosition: (d: SensitiveResource) => [d.lon, d.lat],
          getRadius: 6,
          getFillColor: (d: SensitiveResource) =>
            hexToRgba(SENSITIVE_COLORS[d.type] || '#22c55e', 220),
          getLineColor: [255, 255, 255, 200],
          getLineWidth: 2,
          stroked: true,
          radiusMinPixels: 6,
          radiusMaxPixels: 10,
        }),
      );

      // 민감자원 라벨
      result.push(
        new TextLayer({
          id: 'sensitive-labels',
          data: sensitiveResources,
          getPosition: (d: SensitiveResource) => [d.lon, d.lat],
          getText: (d: SensitiveResource) =>
            `${SENSITIVE_ICONS[d.type]} ${d.name} (${d.arrivalTimeH}h)`,
          getSize: 12,
          getColor: (lightMode ? [20, 40, 100, 240] : [255, 255, 255, 200]) as [
            number,
            number,
            number,
            number,
          ],
          getPixelOffset: [0, -20],
          fontFamily: 'NanumSquare, Malgun Gothic, 맑은 고딕, sans-serif',
          fontWeight: 'bold',
          characterSet: 'auto',
          outlineWidth: 2,
          outlineColor: [15, 21, 36, 200],
          billboard: true,
          sizeUnits: 'pixels' as const,
        }),
      );
    }

    // --- 민감자원 GeoJSON 레이어 ---
    if (sensitiveResourceGeojson && sensitiveResourceGeojson.features.length > 0) {
      result.push(
        new GeoJsonLayer({
          id: 'sensitive-resource-geojson',
          // eslint-disable-next-line @typescript-eslint/no-explicit-any
          data: sensitiveResourceGeojson as any,
          pickable: true,
          stroked: true,
          filled: true,
          pointRadiusMinPixels: 10,
          pointRadiusMaxPixels: 20,
          lineWidthMinPixels: 1,
          getLineWidth: 1.5,
          getFillColor: (f: { properties: { category?: string } | null }) => {
            const cat = f.properties?.category ?? '';
            const [r, g, b] = categoryToRgb(cat);
            return [r, g, b, 80] as [number, number, number, number];
          },
          getLineColor: (f: { properties: { category?: string } | null }) => {
            const cat = f.properties?.category ?? '';
            const [r, g, b] = categoryToRgb(cat);
            return [r, g, b, 210] as [number, number, number, number];
          },
          onHover: (info: PickingInfo) => {
            if (info.object) {
              hoveredSensitiveRef.current =
                (info.object as { properties: Record<string, unknown> | null }).properties ?? {};
            } else {
              hoveredSensitiveRef.current = null;
            }
          },
        }) as unknown as DeckLayer,
      );
    }

    // --- 입자 중심점 이동 경로 (모델별 PathLayer + ScatterplotLayer) ---
    const visibleCenters = centerPoints.filter((p) => p.time <= currentTime);
    if (visibleCenters.length > 0) {
      // 모델별 그룹핑 (Record 사용 — Map 컴포넌트와 이름 충돌 회피)
      const modelGroups: Record<string, typeof visibleCenters> = {};
      visibleCenters.forEach((p) => {
        const key = p.model || 'OpenDrift';
        if (!modelGroups[key]) modelGroups[key] = [];
        modelGroups[key].push(p);
      });

      Object.entries(modelGroups).forEach(([model, points]) => {
        const modelColor = hexToRgba(MODEL_COLORS[model as PredictionModel] || '#06b6d4', 210);
        if (points.length >= 2) {
          result.push(
            new PathLayer({
              id: `center-path-${model}`,
              data: [
                {
                  path: points.map(
                    (p: { lon: number; lat: number }) => [p.lon, p.lat] as [number, number],
                  ),
                },
              ],
              getPath: (d: { path: [number, number][] }) => d.path,
              getColor: modelColor,
              getWidth: 2,
              widthMinPixels: 2,
              widthMaxPixels: 4,
            }),
          );
        }
        result.push(
          new ScatterplotLayer({
            id: `center-points-${model}`,
            data: points,
            getPosition: (d: { lon: number; lat: number }) => [d.lon, d.lat],
            getRadius: 5,
            getFillColor: modelColor,
            radiusMinPixels: 4,
            radiusMaxPixels: 8,
            pickable: false,
          }),
        );
        if (showTimeLabel) {
          const baseTime = simulationStartTime ? new Date(simulationStartTime) : null;
          const pad = (n: number) => String(n).padStart(2, '0');
          result.push(
            new TextLayer({
              id: `time-labels-${model}`,
              data: points,
              getPosition: (d: { lon: number; lat: number }) => [d.lon, d.lat],
              getText: (d: { time: number }) => {
                if (baseTime) {
                  const dt = new Date(baseTime.getTime() + d.time * 3600 * 1000);
                  return `${dt.getFullYear()}-${pad(dt.getMonth() + 1)}-${pad(dt.getDate())} ${pad(dt.getHours())}:${pad(dt.getMinutes())}`;
                }
                return `+${d.time}h`;
              },
              getSize: 12,
              getColor: hexToRgba(MODEL_COLORS[model as PredictionModel] || '#06b6d4', 240),
              getPixelOffset: [0, 16] as [number, number],
              fontWeight: 'bold',
              outlineWidth: 2,
              outlineColor: (lightMode ? [255, 255, 255, 180] : [15, 21, 36, 200]) as [
                number,
                number,
                number,
                number,
              ],
              billboard: true,
              sizeUnits: 'pixels' as const,
              updateTriggers: {
                getText: [simulationStartTime, currentTime],
              },
            }),
          );
        }
      });
    }

    // --- 바람 화살표 (TextLayer) ---
    if (incidentCoord && windData.length > 0 && showWind) {
      type ArrowPoint = { lon: number; lat: number; bearing: number; speed: number };

      const activeWindStep = windData[currentTime] ?? windData[0] ?? [];
      const currentArrows: ArrowPoint[] = activeWindStep
        .filter((d) => d.wind_speed != null && d.wind_direction != null)
        .map((d) => ({
          lon: d.lon,
          lat: d.lat,
          bearing: d.wind_direction,
          speed: d.wind_speed,
        }));

      result.push(
        new TextLayer({
          id: 'current-arrows',
          data: currentArrows,
          getPosition: (d: ArrowPoint) => [d.lon, d.lat],
          getText: () => '➤',
          getAngle: (d: ArrowPoint) => -d.bearing + 90,
          getSize: 22,
          getColor: (d: ArrowPoint): [number, number, number, number] => {
            const s = d.speed;
            if (s < 3) return [6, 182, 212, 130]; // cyan-500: calm
            if (s < 7) return [34, 197, 94, 150]; // green-500: light
            if (s < 12) return [234, 179, 8, 170]; // yellow-500: moderate
            if (s < 17) return [249, 115, 22, 190]; // orange-500: fresh
            return [239, 68, 68, 210]; // red-500: strong
          },
          characterSet: 'auto',
          sizeUnits: 'pixels' as const,
          billboard: true,
          updateTriggers: {
            getColor: [currentTime, windData],
            getAngle: [currentTime, windData],
          },
        }),
      );
    }

    // 거리/면적 측정 레이어
    result.push(...buildMeasureLayers(measureInProgress, measureMode, measurements));

    return result.filter(Boolean);
  }, [
    oilTrajectory,
    currentTime,
    selectedModels,
    boomLines,
    showBoomLines,
    isDrawingBoom,
    drawingPoints,
    dispersionResult,
    dispersionHeatmap,
    incidentCoord,
    backtrackReplay,
    sensitiveResources,
    sensitiveResourceGeojson,
    centerPoints,
    windData,
    showWind,
    showBeached,
    showTimeLabel,
    simulationStartTime,
    analysisPolygonPoints,
    analysisCircleCenter,
    analysisCircleRadiusM,
    lightMode,
  ]);

  // 3D 모드 / 테마에 따른 지도 스타일 전환
  const currentMapStyle = useBaseMapStyle();

  return (
    <div className="w-full h-full relative">
      <Map
        initialViewState={{
          longitude: center[1],
          latitude: center[0],
          zoom: zoom,
        }}
        mapStyle={currentMapStyle}
        className="w-full h-full"
        style={{
          cursor:
            isSelectingLocation || drawAnalysisMode !== null || measureMode !== null
              ? 'crosshair'
              : 'grab',
        }}
        onClick={handleMapClick}
        attributionControl={false}
        preserveDrawingBuffer={true}
      >
        {/* 지도 캡처 셋업 */}
        {mapCaptureRef && <MapCaptureSetup captureRef={mapCaptureRef} />}
        {/* 지도 중앙 좌표 + 줌 추적 */}
        <MapCenterTracker onCenterChange={handleMapCenterChange} />
        {/* 3D 모드 pitch 제어 */}
        <MapPitchController threeD={mapToggles['threeD'] ?? false} />
        {/* 사고 지점 변경 시 지도 이동 */}
        <MapFlyToIncident coord={flyToIncident} onFlyEnd={onIncidentFlyEnd} />
        {/* 외부에서 flyTo 트리거 */}
        <FlyToController target={flyToTarget} duration={1200} />
        {/* 예측 완료 시 궤적 전체 범위로 fitBounds */}
        <FitBoundsController fitBoundsTarget={fitBoundsTarget} />

        {/* S-57 전자해도 오버레이 (공식 style.json 기반) */}
        <S57EncOverlay visible={mapToggles['s57'] ?? false} />

        {/* SR 민감자원 벡터타일 오버레이 */}
        <SrOverlay enabledLayers={enabledLayers} opacity={layerOpacity} layerColors={layerColors} />

        {/* WMS 레이어 */}
        {wmsLayers.map((layer) => (
          <Source
            key={layer.id}
            id={`wms-${layer.id}`}
            type="raster"
            tiles={[
              `${GEOSERVER_URL}/geoserver/gwc/service/wms?service=WMS&version=1.1.0&request=GetMap&layers=${layer.wmsLayer}&styles=&bbox={bbox-epsg-3857}&width=256&height=256&srs=EPSG:3857&format=image/png&transparent=true`,
            ]}
            tileSize={256}
          >
            <Layer
              id={`wms-layer-${layer.id}`}
              type="raster"
              paint={{
                'raster-opacity': wmsOpacity,
                'raster-brightness-min': wmsBrightnessMin,
                'raster-brightness-max': Math.min(wmsBrightnessMax, 1),
              }}
            />
          </Source>
        ))}

        {/* deck.gl 오버레이 (인터리브드: 일반 레이어) */}
        <DeckGLOverlay layers={deckLayers} />

        {/* 해류 파티클 오버레이 */}
        {hydrData.length > 0 && showCurrent && (
          <HydrParticleOverlay hydrStep={hydrData[currentTime] ?? null} />
        )}

        {/* 사고 위치 마커 (MapLibre Marker) */}
        {incidentCoord &&
          !isNaN(incidentCoord.lat) &&
          !isNaN(incidentCoord.lon) &&
          !(dispersionHeatmap && dispersionHeatmap.length > 0) && (
            <Marker longitude={incidentCoord.lon} latitude={incidentCoord.lat} anchor="bottom">
              <div
                title={`사고 지점\n${incidentCoord.lat.toFixed(4)}°N, ${incidentCoord.lon.toFixed(4)}°E`}
                className="w-6 h-6 bg-color-accent border-2 border-white"
                style={{
                  borderRadius: '50% 50% 50% 0',
                  transform: 'rotate(-45deg)',
                  boxShadow: '0 2px 8px rgba(6,182,212,0.5)',
                }}
              />
            </Marker>
          )}

        {/* deck.gl 객체 클릭 팝업 */}
        {popupInfo && (
          <Popup
            longitude={popupInfo.longitude}
            latitude={popupInfo.latitude}
            anchor="bottom"
            onClose={() => {
              persistentPopupRef.current = false;
              setPopupInfo(null);
            }}
          >
            <div className="text-[#333]">{popupInfo.content}</div>
          </Popup>
        )}

        {/* 측정 결과 지우기 버튼 */}
        <MeasureOverlay />

        {/* 커스텀 줌 컨트롤 */}
        <MapControls center={center} zoom={zoom} />
      </Map>

      {/* 드로잉 모드 안내 */}
      {isDrawingBoom && (
        <div className="boom-drawing-indicator">
          오일펜스 배치 모드 — 지도를 클릭하여 포인트를 추가하세요 ({drawingPoints.length}개 포인트)
        </div>
      )}
      {drawAnalysisMode === 'polygon' && (
        <div
          className="boom-drawing-indicator"
          style={{ background: 'rgba(168,85,247,0.15)', borderColor: 'rgba(168,85,247,0.4)' }}
        >
          다각형 분석 모드 — 지도를 클릭하여 꼭짓점을 추가하세요 ({analysisPolygonPoints.length}개)
        </div>
      )}
      {drawAnalysisMode === 'circle' && (
        <div
          className="boom-drawing-indicator"
          style={{ background: 'rgba(168,85,247,0.15)', borderColor: 'rgba(168,85,247,0.4)' }}
        >
          {!analysisCircleCenter ? '원 분석 모드 — 중심점을 클릭하세요' : '반경 지점을 클릭하세요'}
        </div>
      )}
      {measureMode === 'distance' && (
        <div className="boom-drawing-indicator">
          거리 재기 — {measureInProgress.length === 0 ? '시작점을 클릭하세요' : '끝점을 클릭하세요'}
        </div>
      )}
      {measureMode === 'area' && (
        <div className="boom-drawing-indicator">
          면적 재기 — 꼭짓점을 클릭하세요 ({measureInProgress.length}개)
          {measureInProgress.length >= 3 && ' → 첫 번째 점 근처를 클릭하면 닫힙니다'}
        </div>
      )}

      {/* 기상청 연계 정보 */}
      {showOverlays && <WeatherInfoPanel position={currentPosition} />}

      {/* 범례 */}
      {showOverlays && (
        <MapLegend
          dispersionResult={dispersionResult}
          incidentCoord={incidentCoord}
          oilTrajectory={oilTrajectory}
          boomLines={boomLines}
          selectedModels={selectedModels}
        />
      )}

      {/* 좌표 표시 */}
      {showOverlays && <CoordinateDisplay position={mapCenter} zoom={mapZoom} />}

      {/* 타임라인 컨트롤 (외부 제어 모드에서는 숨김 — 하단 플레이어가 대신 담당) */}
      {!isControlled && oilTrajectory.length > 0 && (
        <TimelineControl
          currentTime={currentTime}
          maxTime={Math.max(...oilTrajectory.map((p) => p.time))}
          isPlaying={isPlaying}
          playbackSpeed={playbackSpeed}
          onTimeChange={setInternalCurrentTime}
          onPlayPause={() => setIsPlaying(!isPlaying)}
          onSpeedChange={setPlaybackSpeed}
        />
      )}

      {/* 역추적 리플레이 바 */}
      {backtrackReplay?.isActive && (
        <BacktrackReplayBar
          replayFrame={backtrackReplay.replayFrame}
          totalFrames={backtrackReplay.totalFrames}
          ships={backtrackReplay.ships}
        />
      )}
    </div>
  );
}

// 지도 컨트롤 (줌, 위치 초기화)
function MapControls({ center, zoom }: { center: [number, number]; zoom: number }) {
  const { current: map } = useMap();

  return (
    <div className="absolute top-[80px] left-[10px] z-10">
      <div className="flex flex-col gap-1">
        <button
          onClick={() => map?.zoomIn()}
          className="w-[28px] h-[28px] bg-[color-mix(in_srgb,var(--bg-elevated)_85%,transparent)] backdrop-blur-sm border border-stroke rounded-sm text-fg-sub flex items-center justify-center hover:bg-bg-surface-hover hover:text-fg transition-all text-caption"
        >
          +
        </button>
        <button
          onClick={() => map?.zoomOut()}
          className="w-[28px] h-[28px] bg-[color-mix(in_srgb,var(--bg-elevated)_85%,transparent)] backdrop-blur-sm border border-stroke rounded-sm text-fg-sub flex items-center justify-center hover:bg-bg-surface-hover hover:text-fg transition-all text-caption"
        >
          −
        </button>
        <button
          onClick={() => map?.flyTo({ center: [center[1], center[0]], zoom, duration: 1000 })}
          className="w-[28px] h-[28px] bg-[color-mix(in_srgb,var(--bg-elevated)_85%,transparent)] backdrop-blur-sm border border-stroke rounded-sm text-fg-sub flex items-center justify-center hover:text-fg transition-all text-caption"
        >
          &#x1F3AF;
        </button>
      </div>
    </div>
  );
}

// 지도 범례
interface MapLegendProps {
  dispersionResult?: DispersionResult | null;
  incidentCoord?: { lon: number; lat: number };
  oilTrajectory?: Array<{ lat: number; lon: number; time: number }>;
  boomLines?: BoomLine[];
  selectedModels?: Set<PredictionModel>;
}

function MapLegend({
  dispersionResult,
  incidentCoord,
  oilTrajectory = [],
  selectedModels = new Set(['OpenDrift'] as PredictionModel[]),
}: MapLegendProps) {
  const [minimized, setMinimized] = useState(true);

  if (dispersionResult && incidentCoord) {
    return (
      <div className="absolute top-4 right-4 bg-[rgba(18,25,41,0.95)] backdrop-blur-xl border border-stroke rounded-lg min-w-[200px] z-[20]">
        {/* 헤더 + 최소화 버튼 */}
        <div
          className="flex items-center justify-between px-3.5 pt-3 pb-1 cursor-pointer"
          onClick={() => setMinimized(!minimized)}
        >
          <span className="text-caption font-bold text-fg-disabled uppercase tracking-wider">
            범례
          </span>
          <span className="text-caption text-fg-disabled hover:text-fg transition-colors">
            {minimized ? '▶' : '▼'}
          </span>
        </div>
        {!minimized && (
          <div className="px-3.5 pb-3.5">
            <div className="flex items-center gap-1.5 mb-2.5">
              <div className="text-base">📍</div>
              <div>
                <h4 className="text-label-2 font-bold text-color-warning">사고 위치</h4>
                <div className="text-caption text-fg-disabled font-mono">
                  {incidentCoord.lat.toFixed(4)}°N, {incidentCoord.lon.toFixed(4)}°E
                </div>
              </div>
            </div>
            <div
              className="text-caption text-fg-sub mb-2 rounded"
              style={{ background: 'rgba(249,115,22,0.08)', padding: '8px' }}
            >
              <div className="flex justify-between mb-[3px]">
                <span className="text-fg-disabled">물질</span>
                <span className="font-semibold text-color-warning">
                  {dispersionResult.substance}
                </span>
              </div>
              <div className="flex justify-between mb-[3px]">
                <span className="text-fg-disabled">풍향</span>
                <span className="font-semibold font-mono">
                  SW {dispersionResult.windDirection}°
                </span>
              </div>
              <div className="flex justify-between">
                <span className="text-fg-disabled">확산 구역</span>
                <span className="font-semibold text-color-accent">
                  {dispersionResult.zones.length}개
                </span>
              </div>
            </div>
            <div>
              <h5 className="text-caption font-bold text-fg-disabled mb-2">위험 구역</h5>
              <div className="flex flex-col gap-1.5">
                <div className="flex items-center gap-2 text-caption text-fg-sub">
                  <div
                    className="w-3 h-3 rounded-full"
                    style={{ background: 'rgba(239,68,68,0.7)' }}
                  />
                  <span>치명적 위험 구역 (AEGL-3)</span>
                </div>
                <div className="flex items-center gap-2 text-caption text-fg-sub">
                  <div
                    className="w-3 h-3 rounded-full"
                    style={{ background: 'rgba(249,115,22,0.7)' }}
                  />
                  <span>높은 위험 구역 (AEGL-2)</span>
                </div>
                <div className="flex items-center gap-2 text-caption text-fg-sub">
                  <div
                    className="w-3 h-3 rounded-full"
                    style={{ background: 'rgba(234,179,8,0.7)' }}
                  />
                  <span>중간 위험 구역 (AEGL-1)</span>
                </div>
              </div>
            </div>
            <div
              className="flex items-center gap-1.5 mt-2 rounded"
              style={{ padding: '6px', background: 'rgba(168,85,247,0.08)' }}
            >
              <div className="text-caption">🧭</div>
              <span className="text-caption text-fg-disabled">풍향 (방사형)</span>
            </div>
          </div>
        )}
      </div>
    );
  }

  if (oilTrajectory.length > 0) {
    return (
      <div
        className="absolute top-4 right-4 bg-[rgba(18,25,41,0.92)] backdrop-blur-xl border border-stroke rounded-md z-[20]"
        style={{ minWidth: 155 }}
      >
        {/* 헤더 + 접기/펼치기 */}
        <div
          className="flex items-center justify-between px-3 py-2 cursor-pointer select-none"
          onClick={() => setMinimized(!minimized)}
        >
          <span className="text-caption font-bold text-fg-sub font-korean">범례</span>
          <span className="text-caption text-fg-disabled hover:text-fg transition-colors ml-3">
            {minimized ? '▶' : '▼'}
          </span>
        </div>

        {!minimized && (
          <div className="px-3 pb-2.5 flex flex-col gap-[5px]">
            {/* 모델별 색상 */}
            {Array.from(selectedModels).map((model) => (
              <div key={model} className="flex items-center gap-2 text-caption text-fg-sub">
                <div
                  className="w-[14px] h-[3px] rounded-sm"
                  style={{ background: MODEL_COLORS[model] }}
                />
                <span className="font-korean">{model}</span>
              </div>
            ))}
            {/* 앙상블 */}
            <div className="flex items-center gap-2 text-caption text-fg-sub">
              <div className="w-[14px] h-[3px] rounded-sm" style={{ background: '#a855f7' }} />
              <span className="font-korean">앙상블</span>
            </div>

            {/* 오일펜스 라인 */}
            <div className="h-px bg-border my-0.5" />
            <div className="flex items-center gap-2 text-caption text-fg-sub">
              <div className="flex gap-px">
                <div className="w-[4px] h-[4px] rounded-full bg-[#f97316]" />
                <div className="w-[4px] h-[4px] rounded-full bg-[#f97316]" />
                <div className="w-[4px] h-[4px] rounded-full bg-[#f97316]" />
              </div>
              <span className="font-korean">오일펜스 라인</span>
            </div>

            {/* 도달시간별 선종 */}
            <div className="h-px bg-border my-0.5" />
            <div className="flex items-center gap-2 text-caption text-fg-sub">
              <div className="w-[14px] h-[3px] rounded-sm bg-[#ef4444]" />
              <span className="font-korean">위험 (&lt;6h)</span>
            </div>
            <div className="flex items-center gap-2 text-caption text-fg-sub">
              <div className="w-[14px] h-[3px] rounded-sm bg-[#f97316]" />
              <span className="font-korean">경고 (6~12h)</span>
            </div>
            <div className="flex items-center gap-2 text-caption text-fg-sub">
              <div className="w-[14px] h-[3px] rounded-sm bg-[#eab308]" />
              <span className="font-korean">주의 (12~24h)</span>
            </div>
            <div className="flex items-center gap-2 text-caption text-fg-sub">
              <div className="w-[14px] h-[3px] rounded-sm bg-[#22c55e]" />
              <span className="font-korean">안전</span>
            </div>
          </div>
        )}
      </div>
    );
  }

  return null;
}

// 좌표 표시
function CoordinateDisplay({ position, zoom }: { position: [number, number]; zoom: number }) {
  const [lat, lng] = position;
  const latDirection = lat >= 0 ? 'N' : 'S';
  const lngDirection = lng >= 0 ? 'E' : 'W';

  // MapLibre 줌 → 축척 변환 (96 DPI 기준)
  const metersPerPixel =
    (40075016.686 * Math.cos((lat * Math.PI) / 180)) / (256 * Math.pow(2, zoom));
  const scaleRatio = Math.round(metersPerPixel * (96 / 0.0254));
  const scaleLabel =
    scaleRatio >= 1000000
      ? `1:${(scaleRatio / 1000000).toFixed(1)}M`
      : `1:${scaleRatio.toLocaleString()}`;

  return (
    <div className="cod">
      <span>
        위도{' '}
        <span className="cov">
          {Math.abs(lat).toFixed(4)}°{latDirection}
        </span>
      </span>
      <span>
        경도{' '}
        <span className="cov">
          {Math.abs(lng).toFixed(4)}°{lngDirection}
        </span>
      </span>
      <span>
        축척 <span className="cov">{scaleLabel}</span>
      </span>
    </div>
  );
}

// 타임라인 컨트롤
interface TimelineControlProps {
  currentTime: number;
  maxTime: number;
  isPlaying: boolean;
  playbackSpeed: number;
  onTimeChange: (time: number) => void;
  onPlayPause: () => void;
  onSpeedChange: (speed: number) => void;
}

function TimelineControl({
  currentTime,
  maxTime,
  isPlaying,
  playbackSpeed,
  onTimeChange,
  onPlayPause,
  onSpeedChange,
}: TimelineControlProps) {
  const progressPercent = (currentTime / maxTime) * 100;

  const handleRewind = () => onTimeChange(Math.max(0, currentTime - 6));
  const handleForward = () => onTimeChange(Math.min(maxTime, currentTime + 6));
  const handleStart = () => onTimeChange(0);
  const handleEnd = () => onTimeChange(maxTime);

  const toggleSpeed = () => {
    const speeds = [1, 2, 4];
    const currentIndex = speeds.indexOf(playbackSpeed);
    onSpeedChange(speeds[(currentIndex + 1) % speeds.length]);
  };

  const handleTimelineClick = (e: React.MouseEvent<HTMLDivElement>) => {
    const rect = e.currentTarget.getBoundingClientRect();
    const percent = (e.clientX - rect.left) / rect.width;
    onTimeChange(Math.max(0, Math.min(maxTime, Math.round(percent * maxTime))));
  };

  const timeLabels = [];
  for (let t = 0; t <= maxTime; t += 6) {
    timeLabels.push(t);
  }

  return (
    <div className="tlb">
      <div className="tlc">
        <div className="tb" onClick={handleStart}>
          ⏮
        </div>
        <div className="tb" onClick={handleRewind}>
          ◀
        </div>
        <div className={`tb ${isPlaying ? 'on' : ''}`} onClick={onPlayPause}>
          {isPlaying ? '⏸' : '▶'}
        </div>
        <div className="tb" onClick={handleForward}>
          ▶▶
        </div>
        <div className="tb" onClick={handleEnd}>
          ⏭
        </div>
        <div className="w-2" />
        <div className="tb" onClick={toggleSpeed}>
          {playbackSpeed}×
        </div>
      </div>
      <div className="tlt">
        <div className="tlls">
          {timeLabels.map((t) => (
            <span
              key={t}
              className={`tll ${Math.abs(currentTime - t) < 1 ? 'on' : ''}`}
              style={{ left: `${(t / maxTime) * 100}%` }}
            >
              {t}h
            </span>
          ))}
        </div>
        <div className="tlsw" onClick={handleTimelineClick}>
          <div className="tlr">
            <div className="tlp" style={{ width: `${progressPercent}%` }} />
            {timeLabels.map((t) => (
              <div
                key={`marker-${t}`}
                className={`tlm ${t % 12 === 0 ? 'mj' : ''}`}
                style={{ left: `${(t / maxTime) * 100}%` }}
              />
            ))}
          </div>
          <div className="tlth" style={{ left: `${progressPercent}%` }} />
        </div>
      </div>
      <div className="tli">
        {/* eslint-disable-next-line react-hooks/purity */}
        <div className="tlct">
          +{currentTime.toFixed(0)}h —{' '}
          {(() => {
            const base = simulationStartTime ? new Date(simulationStartTime) : new Date();
            const d = new Date(base.getTime() + currentTime * 3600 * 1000);
            return `${String(d.getMonth() + 1).padStart(2, '0')}/${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')} KST`;
          })()}
        </div>
        <div className="tlss">
          <div className="tls">
            <span className="tlsl">진행률</span>
            <span className="tlsv">{progressPercent.toFixed(0)}%</span>
          </div>
          <div className="tls">
            <span className="tlsl">속도</span>
            <span className="tlsv">{playbackSpeed}×</span>
          </div>
          <div className="tls">
            <span className="tlsl">시간</span>
            <span className="tlsv">
              {currentTime.toFixed(0)}/{maxTime}h
            </span>
          </div>
        </div>
      </div>
    </div>
  );
}

// 기상 데이터 Mock
function getWeatherData(position: [number, number]) {
  const [lat, lng] = position;
  const latSeed = Math.abs(lat * 100) % 10;
  const lngSeed = Math.abs(lng * 100) % 10;
  const directions = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'];
  return {
    windSpeed: Number((5 + latSeed).toFixed(1)),
    windDirection: directions[Math.floor(lngSeed * 0.8)],
    waveHeight: Number((1 + latSeed * 0.2).toFixed(1)),
    waterTemp: Number((8 + (lngSeed - 5) * 0.5).toFixed(1)),
    currentSpeed: Number((0.3 + lngSeed * 0.05).toFixed(2)),
    currentDirection: directions[Math.floor(latSeed * 0.8)],
  };
}

function WeatherInfoPanel({ position }: { position: [number, number] }) {
  const weather = getWeatherData(position);
  return (
    <div className="wip">
      <div className="wii">
        <div className="wii-icon">💨</div>
        <div className="wii-value">{weather.windSpeed} m/s</div>
        <div className="wii-label">풍속 ({weather.windDirection})</div>
      </div>
      <div className="wii">
        <div className="wii-icon">🌊</div>
        <div className="wii-value">{weather.waveHeight} m</div>
        <div className="wii-label">파고</div>
      </div>
      <div className="wii">
        <div className="wii-icon">🌡</div>
        <div className="wii-value">{weather.waterTemp}°C</div>
        <div className="wii-label">수온</div>
      </div>
      <div className="wii">
        <div className="wii-icon">🔄</div>
        <div className="wii-value">{weather.currentSpeed} m/s</div>
        <div className="wii-label">해류 ({weather.currentDirection})</div>
      </div>
    </div>
  );
}

// 역추적 리플레이 컨트롤 바 (HTML 오버레이)
function BacktrackReplayBar({
  replayFrame,
  totalFrames,
  ships,
}: {
  replayFrame: number;
  totalFrames: number;
  ships: ReplayShip[];
}) {
  const progress = (replayFrame / totalFrames) * 100;
  return (
    <div
      className="absolute flex items-center gap-4"
      style={{
        bottom: 80,
        left: '50%',
        transform: 'translateX(-50%)',
        background: 'rgba(10,14,26,0.92)',
        backdropFilter: 'blur(12px)',
        border: '1px solid var(--stroke-light)',
        borderRadius: '10px',
        padding: '12px 18px',
        zIndex: 50,
        minWidth: '340px',
      }}
    >
      <div className="text-body-2 text-color-tertiary font-mono font-bold">
        {progress.toFixed(0)}%
      </div>
      <div className="flex-1 h-1 bg-border relative rounded-[2px]">
        <div
          className="h-full rounded-[2px]"
          style={{
            width: `${progress}%`,
            background: 'linear-gradient(90deg, var(--color-tertiary), var(--color-accent))',
            transition: 'width 0.05s',
          }}
        />
      </div>
      <div className="flex gap-1.5">
        {ships.map((s) => (
          <div
            key={s.vesselName}
            className="w-2 h-2 rounded-full border border-white/30"
            style={{ background: s.color }}
            title={s.vesselName}
          />
        ))}
      </div>
    </div>
  );
}
