import { useState, useMemo, useEffect, useCallback, useRef } from 'react'
import { Map, Marker, Popup, Source, Layer, useControl, useMap } from '@vis.gl/react-maplibre'
import { MapboxOverlay } from '@deck.gl/mapbox'
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 { 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[]
  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
}

// deck.gl 오버레이 컴포넌트 (MapLibre 컨트롤로 등록, interleaved)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function DeckGLOverlay({ layers }: { layers: any[] }) {
  const overlay = useControl<MapboxOverlay>(() => new MapboxOverlay({ interleaved: true }))
  overlay.setProps({ layers })
  return null
}

// flyTo 트리거 컴포넌트 (Map 내부에서 useMap() 사용)
function FlyToController({ flyToTarget }: { flyToTarget?: { lng: number; lat: number; zoom?: number } | null }) {
  const { current: map } = useMap()
  useEffect(() => {
    if (!map || !flyToTarget) return
    map.flyTo({
      center: [flyToTarget.lng, flyToTarget.lat],
      zoom: flyToTarget.zoom ?? 10,
      duration: 1200,
    })
  }, [flyToTarget, map])
  return null
}

// 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 = [],
  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-xs 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-[10px] text-[#888] shrink-0">{key}</span>
                    <span className="text-[10px] text-[#333] font-medium text-right break-all">
                      {typeof val === 'object' ? JSON.stringify(val) : String(val)}
                    </span>
                  </div>
                ))}
              </div>
            ) : (
              <p className="text-[10px] 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-xs">
                    <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 (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-xs" 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-xs 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-xs" 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-[10px] 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, 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 flyToTarget={flyToTarget} />
        {/* 예측 완료 시 궤적 전체 범위로 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-[rgba(18,25,41,0.65)] backdrop-blur-sm border border-[rgba(30,42,66,0.5)] rounded-sm text-fg-sub flex items-center justify-center hover:bg-bg-surface-hover hover:text-fg transition-all text-xs"
        >
          +
        </button>
        <button
          onClick={() => map?.zoomOut()}
          className="w-[28px] h-[28px] bg-[rgba(18,25,41,0.65)] backdrop-blur-sm border border-[rgba(30,42,66,0.5)] rounded-sm text-fg-sub flex items-center justify-center hover:bg-bg-surface-hover hover:text-fg transition-all text-xs"
        >
          −
        </button>
        <button
          onClick={() => map?.flyTo({ center: [center[1], center[0]], zoom, duration: 1000 })}
          className="w-[28px] h-[28px] bg-[rgba(18,25,41,0.65)] backdrop-blur-sm border border-[rgba(30,42,66,0.5)] rounded-sm text-fg-sub flex items-center justify-center hover:text-fg transition-all text-[10px]"
        >
          &#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-[10px] font-bold text-fg-disabled uppercase tracking-wider">범례</span>
          <span className="text-[10px] 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-[11px] font-bold text-color-warning">사고 위치</h4>
                <div className="text-[8px] text-fg-disabled font-mono">
                  {incidentCoord.lat.toFixed(4)}°N, {incidentCoord.lon.toFixed(4)}°E
                </div>
              </div>
            </div>
            <div className="text-[9px] 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-[9px] font-bold text-fg-disabled mb-2">위험 구역</h5>
              <div className="flex flex-col gap-1.5">
                <div className="flex items-center gap-2 text-[10px] 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-[10px] 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-[10px] 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-xs">🧭</div>
              <span className="text-[9px] 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-[10px] font-bold text-fg-sub font-korean">범례</span>
          <span className="text-[9px] 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-[10px] 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-[10px] 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-[10px] 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-[10px] 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-[10px] 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-[10px] 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-[10px] 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-sm 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>
  )
}
