import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import { LeftPanel } from './LeftPanel';
import { RightPanel } from './RightPanel';
import { MapView } from '@common/components/map/MapView';
import { AnalysisListTable, type Analysis } from './AnalysisListTable';
import { OilSpillTheoryView } from './OilSpillTheoryView';
import { BoomDeploymentTheoryView } from './BoomDeploymentTheoryView';
import { BacktrackModal } from './BacktrackModal';
import { RecalcModal } from './RecalcModal';
import { BacktrackReplayBar } from '@common/components/map/BacktrackReplayBar';
import {
  useSubMenu,
  navigateToTab,
  setReportGenCategory,
  setOilReportPayload,
  type OilReportPayload,
} from '@common/hooks/useSubMenu';
import { fetchWeatherSnapshotForCoord } from '@tabs/weather/services/weatherUtils';
import { useWeatherSnapshotStore } from '@common/store/weatherSnapshotStore';
import type {
  BoomLine,
  AlgorithmSettings,
  ContainmentResult,
  BoomLineCoord,
} from '@common/types/boomLine';
import type {
  BacktrackPhase,
  BacktrackVessel,
  BacktrackConditions,
  BacktrackInputConditions,
  ReplayShip,
  CollisionEvent,
  BackwardParticleStep,
} from '@common/types/backtrack';
import { TOTAL_REPLAY_FRAMES } from '@common/types/backtrack';
import {
  fetchBacktrackByAcdnt,
  createBacktrack,
  fetchPredictionDetail,
  fetchAnalysisTrajectory,
  fetchSensitiveResources,
  fetchSensitiveResourcesGeojson,
} from '../services/predictionApi';
import type {
  CenterPoint,
  HydrDataStep,
  ImageAnalyzeResult,
  OilParticle,
  PredictionDetail,
  RunModelSyncResponse,
  SimulationSummary,
  SensitiveResourceCategory,
  SensitiveResourceFeatureCollection,
  WindPoint,
} from '../services/predictionApi';
import SimulationLoadingOverlay from './SimulationLoadingOverlay';
import SimulationErrorModal from './SimulationErrorModal';
import { api } from '@common/services/api';
import {
  generateAIBoomLines,
  haversineDistance,
  pointInPolygon,
  polygonAreaKm2,
  circleAreaKm2,
} from '@common/utils/geo';
import { consumePendingImageAnalysis } from '@common/utils/imageAnalysisSignal';

export type PredictionModel = 'KOSPS' | 'POSEIDON' | 'OpenDrift';

const toLocalDateTimeStr = (raw: string): string => {
  const d = new Date(raw);
  if (isNaN(d.getTime())) return '';
  const pad = (n: number) => String(n).padStart(2, '0');
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
};

// ---------------------------------------------------------------------------
// 민감자원 타입 + 데모 데이터
// ---------------------------------------------------------------------------
export interface SensitiveResource {
  id: string;
  name: string;
  type: 'aquaculture' | 'beach' | 'ecology' | 'intake';
  lat: number;
  lon: number;
  radiusM: number;
  arrivalTimeH: number;
}

export interface DisplayControls {
  showCurrent: boolean; // 유향/유속
  showWind: boolean; // 풍향/풍속
  showBeached: boolean; // 해안부착
  showTimeLabel: boolean; // 시간 표시
  showSensitiveResources: boolean; // 민감자원
}

// ---------------------------------------------------------------------------
// 데모 궤적 생성 (seeded PRNG — deterministic)
// ---------------------------------------------------------------------------
function mulberry32(seed: number) {
  return () => {
    seed |= 0;
    seed = (seed + 0x6d2b79f5) | 0;
    let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

const DEG2RAD = Math.PI / 180;

function generateDemoTrajectory(
  incident: { lat: number; lon: number },
  models: PredictionModel[],
  durationHours: number,
): Array<{ lat: number; lon: number; time: number; particle: number; model: PredictionModel }> {
  const result: Array<{
    lat: number;
    lon: number;
    time: number;
    particle: number;
    model: PredictionModel;
  }> = [];
  const PARTICLES_PER_MODEL = 60;
  const TIME_STEP = 3; // hours

  const modelParams: Record<
    PredictionModel,
    { bearing: number; speed: number; spread: number; seed: number }
  > = {
    KOSPS: { bearing: 200, speed: 0.003, spread: 0.008, seed: 42 },
    POSEIDON: { bearing: 210, speed: 0.0025, spread: 0.01, seed: 137 },
    OpenDrift: { bearing: 190, speed: 0.0035, spread: 0.006, seed: 271 },
  };

  for (const model of models) {
    const p = modelParams[model];
    const rng = mulberry32(p.seed);

    for (let pid = 0; pid < PARTICLES_PER_MODEL; pid++) {
      const particleAngleOffset = (rng() - 0.5) * 40; // ±20°
      const particleSpeedFactor = 0.7 + rng() * 0.6; // 0.7~1.3

      for (let t = 0; t <= durationHours; t += TIME_STEP) {
        const timeFactor = t / durationHours;
        const bearing = (p.bearing + particleAngleOffset) * DEG2RAD;

        const dist = p.speed * t * particleSpeedFactor;
        const turbLat = (rng() - 0.5) * p.spread * timeFactor;
        const turbLon = (rng() - 0.5) * p.spread * timeFactor;

        const lat = incident.lat + dist * Math.cos(bearing) + turbLat;
        const lon =
          incident.lon + (dist * Math.sin(bearing)) / Math.cos(incident.lat * DEG2RAD) + turbLon;

        result.push({ lat, lon, time: t, particle: pid, model });
      }
    }
  }

  return result;
}
// eslint-disable-next-line react-refresh/only-export-components
export const ALL_MODELS: PredictionModel[] = ['KOSPS', 'POSEIDON', 'OpenDrift'];

export function OilSpillView() {
  const { activeSubTab, setActiveSubTab } = useSubMenu('prediction');
  const [leftCollapsed, setLeftCollapsed] = useState(false);
  const [rightCollapsed, setRightCollapsed] = useState(false);
  const [enabledLayers, setEnabledLayers] = useState<Set<string>>(new Set());
  const [incidentCoord, setIncidentCoord] = useState<{ lon: number; lat: number } | null>(null);
  const [flyToCoord, setFlyToCoord] = useState<{ lon: number; lat: number } | undefined>(undefined);
  const flyToTarget = null;
  const fitBoundsTarget = null;
  const [isSelectingLocation, setIsSelectingLocation] = useState(false);
  const [oilTrajectory, setOilTrajectory] = useState<OilParticle[]>([]);
  const [centerPoints, setCenterPoints] = useState<CenterPoint[]>([]);
  const [windDataByModel, setWindDataByModel] = useState<Record<string, WindPoint[][]>>({});
  const [hydrDataByModel, setHydrDataByModel] = useState<Record<string, (HydrDataStep | null)[]>>(
    {},
  );
  const [windHydrModel, setWindHydrModel] = useState<string>('OpenDrift');
  const [isRunningSimulation, setIsRunningSimulation] = useState(false);
  const [simulationProgress, setSimulationProgress] = useState(0);
  const progressTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
  const [simulationError, setSimulationError] = useState<string | null>(null);
  const [validationErrors, setValidationErrors] = useState<Set<string>>(new Set());
  const [selectedModels, setSelectedModels] = useState<Set<PredictionModel>>(
    new Set(['OpenDrift']),
  );
  const [visibleModels, setVisibleModels] = useState<Set<PredictionModel>>(new Set(['OpenDrift']));
  const [predictionTime, setPredictionTime] = useState(6);
  const [accidentTime, setAccidentTime] = useState<string>('');
  const [spillType, setSpillType] = useState('연속');
  const [oilType, setOilType] = useState('벙커C유');
  const [spillAmount, setSpillAmount] = useState(100);
  const [incidentName, setIncidentName] = useState('');
  const [spillUnit, setSpillUnit] = useState('kL');

  // 민감자원
  const [sensitiveResources, setSensitiveResources] = useState<SensitiveResource[]>([]);
  const [sensitiveResourceCategories, setSensitiveResourceCategories] = useState<
    SensitiveResourceCategory[]
  >([]);
  const [sensitiveResourceGeojson, setSensitiveResourceGeojson] =
    useState<SensitiveResourceFeatureCollection | null>(null);

  // 오일펜스 배치 상태
  const [boomLines, setBoomLines] = useState<BoomLine[]>([]);
  const [showBoomLines, setShowBoomLines] = useState(true);
  const [algorithmSettings, setAlgorithmSettings] = useState<AlgorithmSettings>({
    currentOrthogonalCorrection: 15,
    safetyMarginMinutes: 60,
    minContainmentEfficiency: 80,
    waveHeightCorrectionFactor: 1.0,
  });
  const [isDrawingBoom, setIsDrawingBoom] = useState(false);
  const [drawingPoints, setDrawingPoints] = useState<BoomLineCoord[]>([]);
  const [containmentResult, setContainmentResult] = useState<ContainmentResult | null>(null);

  // 레이어 스타일 (투명도 / 밝기 / 색상)
  const [layerOpacity, setLayerOpacity] = useState(50);
  const [layerBrightness, setLayerBrightness] = useState(50);
  const [layerColors, setLayerColors] = useState<Record<string, string>>({});

  // 표시 정보 제어
  const [displayControls, setDisplayControls] = useState<DisplayControls>({
    showCurrent: true,
    showWind: false,
    showBeached: false,
    showTimeLabel: false,
    showSensitiveResources: false,
  });

  // 타임라인 플레이어 상태
  const [isPlaying, setIsPlaying] = useState(false);
  const [currentStep, setCurrentStep] = useState(0); // 현재 시간값 (시간 단위)
  const [playSpeed, setPlaySpeed] = useState(1);

  // 역추적 상태
  const [backtrackModalOpen, setBacktrackModalOpen] = useState(false);
  const [backtrackPhase, setBacktrackPhase] = useState<BacktrackPhase>('conditions');
  const [backtrackVessels, setBacktrackVessels] = useState<BacktrackVessel[]>([]);
  const [isReplayActive, setIsReplayActive] = useState(false);
  const [isReplayPlaying, setIsReplayPlaying] = useState(false);
  const [replayFrame, setReplayFrame] = useState(0);
  const [replaySpeed, setReplaySpeed] = useState(1);

  // 선택된 분석 (목록에서 클릭 시)
  const [selectedAnalysis, setSelectedAnalysis] = useState<Analysis | null>(null);
  // 분석 상세 (API에서 가져온 선박/기상 정보)
  const [analysisDetail, setAnalysisDetail] = useState<PredictionDetail | null>(null);

  // 역추적 API 데이터
  const [backtrackConditions, setBacktrackConditions] = useState<BacktrackConditions>({
    estimatedSpillTime: '',
    analysisRange: '±12시간',
    searchRadius: '10 NM',
    spillLocation: { lat: 37.3883, lon: 126.6435 },
    totalVessels: 0,
  });
  const [replayShips, setReplayShips] = useState<ReplayShip[]>([]);
  const [collisionEvent, setCollisionEvent] = useState<CollisionEvent | null>(null);
  const [replayTimeRange, setReplayTimeRange] = useState<{ start: string; end: string } | null>(
    null,
  );
  const [backwardParticles, setBackwardParticles] = useState<BackwardParticleStep[]>([]);

  // 재계산 상태
  const [recalcModalOpen, setRecalcModalOpen] = useState(false);
  const [simulationSummary, setSimulationSummary] = useState<SimulationSummary | null>(null);
  const [summaryByModel, setSummaryByModel] = useState<Record<string, SimulationSummary>>({});
  const [stepSummariesByModel, setStepSummariesByModel] = useState<
    Record<string, SimulationSummary[]>
  >({});

  // 펜스차단량 계산 (오일붐 차단 효율 × 총 유류량)
  const boomBlockedVolume = useMemo(() => {
    if (!containmentResult || !simulationSummary) return 0;
    const totalVolumeM3 =
      simulationSummary.remainingVolume +
      simulationSummary.weatheredVolume +
      simulationSummary.beachedVolume;
    return totalVolumeM3 * (containmentResult.overallEfficiency / 100);
  }, [containmentResult, simulationSummary]);

  // 오염분석 상태
  const [analysisTab, setAnalysisTab] = useState<'polygon' | 'circle'>('polygon');
  const [drawAnalysisMode, setDrawAnalysisMode] = useState<'polygon' | null>(null);
  const [analysisPolygonPoints, setAnalysisPolygonPoints] = useState<
    { lat: number; lon: number }[]
  >([]);
  const [circleRadiusNm, setCircleRadiusNm] = useState<number>(5);
  const [analysisResult, setAnalysisResult] = useState<{
    area: number;
    particleCount: number;
    particlePercent: number;
    sensitiveCount: number;
  } | null>(null);

  // 원 분석용 derived 값 (state 아님)
  const analysisCircleCenter = analysisTab === 'circle' && incidentCoord ? incidentCoord : null;
  const analysisCircleRadiusM = circleRadiusNm * 1852;

  const handleToggleLayer = (layerId: string, enabled: boolean) => {
    setEnabledLayers((prev) => {
      const newSet = new Set(prev);
      if (enabled) {
        newSet.add(layerId);
      } else {
        newSet.delete(layerId);
      }
      return newSet;
    });
  };

  // 역추적: API에서 기존 결과 로딩
  const loadBacktrackData = useCallback(
    async (acdntSn: number) => {
      try {
        const bt = await fetchBacktrackByAcdnt(acdntSn);
        if (bt && bt.execSttsCd === 'COMPLETED' && bt.rsltData) {
          const rslt = bt.rsltData as Record<string, unknown>;
          if (Array.isArray(rslt.vessels)) {
            setBacktrackVessels(rslt.vessels as BacktrackVessel[]);
          }
          if (Array.isArray(rslt.replayShips)) {
            setReplayShips(rslt.replayShips as ReplayShip[]);
          }
          if (rslt.collisionEvent) {
            setCollisionEvent(rslt.collisionEvent as CollisionEvent);
          }
          if (rslt['timeRange']) {
            setReplayTimeRange(rslt['timeRange'] as { start: string; end: string });
          }
          setBackwardParticles(
            Array.isArray(rslt['backwardParticles'])
              ? (rslt['backwardParticles'] as BackwardParticleStep[])
              : [],
          );
          setBacktrackConditions({
            estimatedSpillTime: bt.estSpilDtm
              ? new Date(bt.estSpilDtm).toLocaleString('ko-KR', {
                  month: '2-digit',
                  day: '2-digit',
                  hour: '2-digit',
                  minute: '2-digit',
                })
              : '',
            analysisRange: bt.anlysRange || '±12시간',
            searchRadius: bt.srchRadiusNm ? `${bt.srchRadiusNm} NM` : '10 NM',
            spillLocation: {
              lat: bt.lat || incidentCoord?.lat || 0,
              lon: bt.lon || incidentCoord?.lon || 0,
            },
            totalVessels: bt.totalVessels || 0,
          });
          setBacktrackPhase('results');
          return;
        }
      } catch (err) {
        console.error('[prediction] 역추적 데이터 로딩 실패:', err);
      }
      // 기존 결과 없으면 conditions 상태 유지
      setBacktrackPhase('conditions');
      setBacktrackVessels([]);
      setReplayShips([]);
      setCollisionEvent(null);
    },
    [incidentCoord],
  );

  // 역추적 핸들러
  const handleOpenBacktrack = () => {
    setBacktrackModalOpen(true);
    setBacktrackConditions((prev) => ({
      ...prev,
      spillLocation: incidentCoord ?? prev.spillLocation,
      estimatedSpillTime: selectedAnalysis?.occurredAt
        ? toLocalDateTimeStr(selectedAnalysis.occurredAt)
        : prev.estimatedSpillTime,
    }));
    if (selectedAnalysis) {
      loadBacktrackData(selectedAnalysis.acdntSn);
    } else {
      setBacktrackPhase('conditions');
      setBacktrackVessels([]);
    }
  };

  const handleRunBacktrackAnalysis = async (input: BacktrackInputConditions) => {
    if (!incidentCoord || !selectedAnalysis) return;
    setBacktrackPhase('analyzing');
    try {
      const anlysRangeStr = `±${input.analysisRange}시간`;
      const bt = await createBacktrack({
        acdntSn: selectedAnalysis.acdntSn,
        lon: incidentCoord.lon,
        lat: incidentCoord.lat,
        estSpilDtm: input.estimatedSpillTime
          ? new Date(input.estimatedSpillTime).toISOString()
          : undefined,
        anlysRange: anlysRangeStr,
        srchRadiusNm: input.searchRadius,
      });

      if (bt.execSttsCd === 'COMPLETED' && bt.rsltData) {
        const rslt = bt.rsltData as Record<string, unknown>;
        if (Array.isArray(rslt['vessels']))
          setBacktrackVessels(rslt['vessels'] as BacktrackVessel[]);
        if (Array.isArray(rslt['replayShips'])) setReplayShips(rslt['replayShips'] as ReplayShip[]);
        if (rslt['collisionEvent']) setCollisionEvent(rslt['collisionEvent'] as CollisionEvent);
        if (rslt['timeRange'])
          setReplayTimeRange(rslt['timeRange'] as { start: string; end: string });
        setBackwardParticles(
          Array.isArray(rslt['backwardParticles'])
            ? (rslt['backwardParticles'] as BackwardParticleStep[])
            : [],
        );
        setBacktrackConditions((prev) => ({ ...prev, totalVessels: bt.totalVessels || 0 }));
        setBacktrackPhase('results');
      } else {
        console.error('[prediction] 역추적 분석 실패:', bt.execSttsCd);
        setBacktrackPhase('conditions');
      }
    } catch (err) {
      console.error('[prediction] 역추적 분석 실패:', err);
      setBacktrackPhase('conditions');
    }
  };

  const handleStartReplay = () => {
    setBacktrackModalOpen(false);
    setIsReplayActive(true);
    setReplayFrame(0);
    setIsReplayPlaying(false);
  };

  const handleCloseReplay = () => {
    setIsReplayActive(false);
    setIsReplayPlaying(false);
    setReplayFrame(0);
  };

  // 역추적 리플레이 애니메이션
  useEffect(() => {
    if (!isReplayPlaying) return;
    const interval = setInterval(() => {
      setReplayFrame((prev) => {
        const next = prev + 1;
        if (next >= TOTAL_REPLAY_FRAMES) {
          setIsReplayPlaying(false);
          return TOTAL_REPLAY_FRAMES;
        }
        return next;
      });
    }, 50 / replaySpeed);
    return () => clearInterval(interval);
  }, [isReplayPlaying, replaySpeed]);

  // flyTo 완료 후 재생 대기 플래그
  const pendingPlayRef = useRef(false);

  // 항공 이미지 분석 완료 후 자동실행 플래그
  const pendingAutoRunRef = useRef(false);

  // 마운트 시 이미지 분석 시그널 확인 (유출유면적분석 탭에서 이동한 경우)
  useEffect(() => {
    const pending = consumePendingImageAnalysis();
    if (!pending) return;
    handleImageAnalysisResult({
      acdntSn: pending.acdntSn,
      lat: pending.lat,
      lon: pending.lon,
      oilType: pending.oilType,
      area: pending.area,
      volume: pending.volume,
      fileId: pending.fileId,
      occurredAt: pending.occurredAt,
    });
    if (pending.autoRun) pendingAutoRunRef.current = true;
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // incidentCoord 업데이트 후 시뮬레이션 자동실행
  useEffect(() => {
    if (pendingAutoRunRef.current && incidentCoord) {
      pendingAutoRunRef.current = false;
      handleRunSimulation();
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [incidentCoord]);

  const handleFlyEnd = useCallback(() => {
    setFlyToCoord(undefined);
    if (pendingPlayRef.current) {
      pendingPlayRef.current = false;
      setIsPlaying(true);
    }
  }, []);

  // trajectory 변경 시 플레이어 스텝 초기화 (재생은 각 경로에서 별도 처리)
  useEffect(() => {
    if (oilTrajectory.length > 0) {
      setCurrentStep(0);
    }
  }, [oilTrajectory.length]);

  useEffect(() => {
    return () => {
      if (progressTimerRef.current) clearInterval(progressTimerRef.current);
    };
  }, []);

  // visibleModels 변경 시 windHydrModel 동기화
  useEffect(() => {
    if (visibleModels.size === 0) return;
    if (visibleModels.size === 1) {
      // 단일 모델 → 항상 해당 모델로 동기화
      setWindHydrModel(Array.from(visibleModels)[0]);
    } else if (!visibleModels.has(windHydrModel as PredictionModel)) {
      // 다중 모델이지만 현재 선택이 사라진 경우 → fallback
      const preferred: PredictionModel[] = ['OpenDrift', 'POSEIDON', 'KOSPS'];
      const next = preferred.find((m) => visibleModels.has(m)) ?? Array.from(visibleModels)[0];
      setWindHydrModel(next);
    }
  }, [visibleModels]);

  // 플레이어 재생 애니메이션 (1x = 1초/스텝, 2x = 0.5초/스텝, 4x = 0.25초/스텝)
  const timeSteps = useMemo(() => {
    if (oilTrajectory.length === 0) return [];
    const unique = [...new Set(oilTrajectory.map((p) => p.time))].sort((a, b) => a - b);
    return unique;
  }, [oilTrajectory]);

  const maxTime = timeSteps[timeSteps.length - 1] ?? predictionTime;

  // 유향유속/풍향풍속 데이터 — 선택한 모델 기준으로 파생
  const windHydrModelOptions = useMemo(() => Array.from(visibleModels), [visibleModels]);
  const windData = useMemo(
    () => windDataByModel[windHydrModel] ?? [],
    [windDataByModel, windHydrModel],
  );
  const hydrData = useMemo(
    () => hydrDataByModel[windHydrModel] ?? [],
    [hydrDataByModel, windHydrModel],
  );

  useEffect(() => {
    if (!isPlaying || timeSteps.length === 0) return;
    if (currentStep >= maxTime) {
      setIsPlaying(false);
      return;
    }
    const ms = 1000 / playSpeed;
    const id = setInterval(() => {
      setCurrentStep((prev) => {
        const idx = timeSteps.indexOf(prev);
        if (idx < 0 || idx >= timeSteps.length - 1) {
          setIsPlaying(false);
          return timeSteps[timeSteps.length - 1];
        }
        return timeSteps[idx + 1];
      });
    }, ms);
    return () => clearInterval(id);
  }, [isPlaying, currentStep, playSpeed, timeSteps, maxTime]);

  // 분석 목록에서 사고명 클릭 시
  const handleSelectAnalysis = async (analysis: Analysis) => {
    setIsPlaying(false);
    setCurrentStep(0);
    setSelectedAnalysis(analysis);
    setCenterPoints([]);
    if (analysis.occurredAt) {
      setAccidentTime(toLocalDateTimeStr(analysis.occurredAt));
    }
    if (analysis.lon != null && analysis.lat != null) {
      setIncidentCoord({ lon: analysis.lon, lat: analysis.lat });
      setFlyToCoord({ lon: analysis.lon, lat: analysis.lat });
    }
    // 유종 매핑
    const oilTypeMap: Record<string, string> = {
      BUNKER_C: '벙커C유',
      DIESEL: '경유',
      CRUDE_OIL: '원유',
      LUBE_OIL: '윤활유',
    };
    setOilType(oilTypeMap[analysis.oilType] || '벙커C유');
    setSpillAmount(analysis.volume ?? 100);
    setPredictionTime(parseInt(analysis.duration) || 6);
    // 모델 상태에 따라 선택 모델 설정
    const models = new Set<PredictionModel>();
    if (analysis.kospsStatus !== 'pending') models.add('KOSPS');
    if (analysis.poseidonStatus !== 'pending') models.add('POSEIDON');
    if (analysis.opendriftStatus !== 'pending') models.add('OpenDrift');
    setSelectedModels(models);
    setVisibleModels(models);
    // 분석 상세 로딩 (선박/기상 정보)
    try {
      const detail = await fetchPredictionDetail(analysis.acdntSn);
      setAnalysisDetail(detail);
    } catch (err) {
      console.error('[prediction] 분석 상세 로딩 실패:', err);
    }
    // 분석 화면으로 전환
    setActiveSubTab('analysis');

    const coord =
      analysis.lon != null && analysis.lat != null
        ? { lon: analysis.lon, lat: analysis.lat }
        : incidentCoord;
    const demoModels = Array.from(models.size > 0 ? models : new Set<PredictionModel>(['KOSPS']));

    // 완료된 모델이 있는 경우 실제 궤적 로드, 없으면 데모로 fallback
    const hasCompletedModel =
      analysis.opendriftStatus === 'completed' || analysis.poseidonStatus === 'completed';
    if (hasCompletedModel) {
      try {
        const {
          trajectory,
          summary,
          centerPoints: cp,
          windDataByModel: wdByModel,
          hydrDataByModel: hdByModel,
          summaryByModel: sbModel,
          stepSummariesByModel: stepSbModel,
        } = await fetchAnalysisTrajectory(analysis.acdntSn, analysis.predRunSn ?? undefined);
        if (trajectory && trajectory.length > 0) {
          setOilTrajectory(trajectory);
          if (summary) setSimulationSummary(summary);
          setCenterPoints(cp ?? []);
          setWindDataByModel(wdByModel ?? {});
          setHydrDataByModel(hdByModel ?? {});
          if (sbModel) setSummaryByModel(sbModel);
          if (stepSbModel) setStepSummariesByModel(stepSbModel);
          if (coord) setBoomLines(generateAIBoomLines(trajectory, coord, algorithmSettings));
          setSensitiveResources([]);
          fetchSensitiveResources(analysis.acdntSn)
            .then(setSensitiveResourceCategories)
            .catch((err) => console.warn('[prediction] 민감자원 조회 실패:', err));
          fetchSensitiveResourcesGeojson(analysis.acdntSn)
            .then(setSensitiveResourceGeojson)
            .catch((err) => console.warn('[prediction] 민감자원 GeoJSON 조회 실패:', err));
          // incidentCoord가 변경된 경우 flyTo 완료 후 재생, 그렇지 않으면 즉시 재생
          if (analysis.lon !== incidentCoord?.lon || analysis.lat !== incidentCoord?.lat) {
            pendingPlayRef.current = true;
          } else {
            setIsPlaying(true);
          }
          return;
        }
      } catch (err) {
        console.error('[prediction] trajectory 로딩 실패, 데모로 fallback:', err);
      }
    }

    // 데모 궤적 생성 (fallback) — stale wind/current 데이터 초기화
    setWindDataByModel({});
    setHydrDataByModel({});
    setSummaryByModel({});
    setStepSummariesByModel({});
    const demoTrajectory = generateDemoTrajectory(
      coord ?? { lat: 37.39, lon: 126.64 },
      demoModels,
      parseInt(analysis.duration) || 6,
    );
    setOilTrajectory(demoTrajectory);
    if (coord) setBoomLines(generateAIBoomLines(demoTrajectory, coord, algorithmSettings));
    setSensitiveResources([]);
    setSensitiveResourceCategories([]);
    // incidentCoord가 변경된 경우 flyTo 완료 후 재생, 그렇지 않으면 즉시 재생
    if (analysis.lon !== incidentCoord?.lon || analysis.lat !== incidentCoord?.lat) {
      pendingPlayRef.current = true;
    } else {
      setIsPlaying(true);
    }
  };

  const handleMapClick = (lon: number, lat: number) => {
    if (isDrawingBoom) {
      setDrawingPoints((prev) => [...prev, { lat, lon }]);
    } else if (drawAnalysisMode === 'polygon') {
      setAnalysisPolygonPoints((prev) => [...prev, { lat, lon }]);
    } else if (isSelectingLocation) {
      setIncidentCoord({ lon, lat });
      setIsSelectingLocation(false);
    }
  };

  const handleStartPolygonDraw = () => {
    setDrawAnalysisMode('polygon');
    setAnalysisPolygonPoints([]);
    setAnalysisResult(null);
  };

  const handleRunPolygonAnalysis = async () => {
    if (analysisPolygonPoints.length < 3) return;
    const currentParticles = oilTrajectory.filter((p) => p.time === currentStep);
    const totalIds = new Set(oilTrajectory.map((p) => p.particle ?? 0)).size || 1;
    const inside = currentParticles.filter((p) =>
      pointInPolygon({ lat: p.lat, lon: p.lon }, analysisPolygonPoints),
    ).length;
    const sensitiveCount = sensitiveResources.filter((r) =>
      pointInPolygon({ lat: r.lat, lon: r.lon }, analysisPolygonPoints),
    ).length;
    setAnalysisResult({
      area: polygonAreaKm2(analysisPolygonPoints),
      particleCount: inside,
      particlePercent: Math.round((inside / totalIds) * 100),
      sensitiveCount,
    });
    setDrawAnalysisMode(null);
  };

  const handleRunCircleAnalysis = async () => {
    if (!incidentCoord) return;
    const radiusM = circleRadiusNm * 1852;
    const currentParticles = oilTrajectory.filter((p) => p.time === currentStep);
    const totalIds = new Set(oilTrajectory.map((p) => p.particle ?? 0)).size || 1;
    const inside = currentParticles.filter(
      (p) =>
        haversineDistance(
          { lat: incidentCoord.lat, lon: incidentCoord.lon },
          { lat: p.lat, lon: p.lon },
        ) <= radiusM,
    ).length;
    const sensitiveCount = sensitiveResources.filter(
      (r) =>
        haversineDistance(
          { lat: incidentCoord.lat, lon: incidentCoord.lon },
          { lat: r.lat, lon: r.lon },
        ) <= radiusM,
    ).length;
    setAnalysisResult({
      area: circleAreaKm2(radiusM),
      particleCount: inside,
      particlePercent: Math.round((inside / totalIds) * 100),
      sensitiveCount,
    });
  };

  const handleCancelAnalysis = () => {
    setDrawAnalysisMode(null);
    setAnalysisPolygonPoints([]);
  };

  const handleClearAnalysis = () => {
    setDrawAnalysisMode(null);
    setAnalysisPolygonPoints([]);
    setAnalysisResult(null);
  };

  const handleImageAnalysisResult = useCallback((result: ImageAnalyzeResult) => {
    setIncidentCoord({ lat: result.lat, lon: result.lon });
    setFlyToCoord({ lat: result.lat, lon: result.lon });
    setAccidentTime(toLocalDateTimeStr(result.occurredAt));
    setOilType(result.oilType);
    setSpillAmount(parseFloat(result.volume.toFixed(20)));
    setSpillUnit('kL');
    setSelectedAnalysis({
      acdntSn: result.acdntSn,
      acdntNm: '',
      occurredAt: result.occurredAt,
      analysisDate: '',
      requestor: '',
      duration: '6',
      oilType: result.oilType,
      volume: result.volume,
      location: '',
      lat: result.lat,
      lon: result.lon,
      kospsStatus: 'pending',
      poseidonStatus: 'pending',
      opendriftStatus: 'pending',
      backtrackStatus: 'pending',
      analyst: '',
      officeName: '',
      acdntSttsCd: 'ACTIVE',
    });
  }, []);

  const startProgressTimer = useCallback((runTimeHours: number) => {
    const expectedMs = runTimeHours * 6000;
    const startTime = Date.now();
    progressTimerRef.current = setInterval(() => {
      const elapsed = Date.now() - startTime;
      setSimulationProgress(Math.min(90, Math.round((elapsed / expectedMs) * 90)));
    }, 500);
  }, []);

  const stopProgressTimer = useCallback((completed: boolean) => {
    if (progressTimerRef.current) {
      clearInterval(progressTimerRef.current);
      progressTimerRef.current = null;
    }
    if (completed) {
      setSimulationProgress(100);
      setTimeout(() => setSimulationProgress(0), 800);
    } else {
      setSimulationProgress(0);
    }
  }, []);

  const handleRunSimulation = async (overrides?: {
    models?: Set<PredictionModel>;
    oilType?: string;
    spillAmount?: number;
    spillType?: string;
    predictionTime?: number;
    incidentCoord?: { lat: number; lon: number } | null;
  }) => {
    // incidentName이 있으면 직접 입력 모드 — 기존 selectedAnalysis.acdntSn 무시하고 새 사고 생성
    const isDirectInput = incidentName.trim().length > 0;
    const existingAcdntSn = isDirectInput
      ? undefined
      : (selectedAnalysis?.acdntSn ?? analysisDetail?.acdnt?.acdntSn);

    const effectiveCoord = overrides?.incidentCoord ?? incidentCoord;
    const effectiveModels = overrides?.models ?? selectedModels;

    // ── 입력 유효성 검증 (border 하이라이트) ──
    const errors = new Set<string>();
    if (isDirectInput) {
      if (!incidentName.trim()) errors.add('incidentName');
      if (!accidentTime) errors.add('accidentTime');
      if (!effectiveCoord || (effectiveCoord.lat === 0 && effectiveCoord.lon === 0))
        errors.add('coord');
    } else if (!existingAcdntSn) {
      errors.add('incidentName');
    }
    if (!effectiveCoord) errors.add('coord');
    if (effectiveModels.size === 0) errors.add('models');
    if (errors.size > 0) {
      setValidationErrors(errors);
      return;
    }
    setValidationErrors(new Set());
    const coord = effectiveCoord!; // 검증 통과 후 non-null 보장

    const effectiveOilType = overrides?.oilType ?? oilType;
    const effectiveSpillAmount = overrides?.spillAmount ?? spillAmount;
    const effectiveSpillType = overrides?.spillType ?? spillType;
    const effectivePredictionTime = overrides?.predictionTime ?? predictionTime;

    setIsRunningSimulation(true);
    setSimulationSummary(null);
    startProgressTimer(effectivePredictionTime);
    let simulationSucceeded = false;
    try {
      const payload: Record<string, unknown> = {
        acdntSn: existingAcdntSn,
        lat: coord.lat,
        lon: coord.lon,
        runTime: effectivePredictionTime,
        matTy: effectiveOilType,
        matVol: effectiveSpillAmount,
        spillTime: effectiveSpillType === '연속' ? effectivePredictionTime : 0,
        startTime: accidentTime ? `${accidentTime}:00` : analysisDetail?.acdnt?.occurredAt,
        models: Array.from(effectiveModels),
      };

      if (isDirectInput) {
        payload.acdntNm = incidentName.trim();
        payload.spillUnit = spillUnit;
        payload.spillTypeCd = spillType;
      }

      // 동기 방식: 예측 완료 시 결과를 직접 반환 (최대 35분 대기)
      const { data } = await api.post<RunModelSyncResponse>('/simulation/run-model', payload, {
        timeout: 35 * 60 * 1000,
      });

      // 직접 입력으로 신규 생성된 경우: selectedAnalysis 갱신 + incidentName 초기화
      if (data.acdntSn && isDirectInput) {
        setSelectedAnalysis({
          acdntSn: data.acdntSn,
          acdntNm: incidentName.trim(),
          occurredAt: accidentTime ? `${accidentTime}:00` : '',
          analysisDate: new Date().toISOString(),
          requestor: '',
          duration: String(predictionTime),
          oilType,
          volume: spillAmount,
          location: '',
          lat: coord.lat,
          lon: coord.lon,
          kospsStatus: 'pending',
          poseidonStatus: 'pending',
          opendriftStatus: 'pending',
          backtrackStatus: 'pending',
          analyst: '',
          officeName: '',
        } as Analysis);
        setIncidentName('');
      }

      // 결과 처리
      const merged: OilParticle[] = [];
      let latestSummary: SimulationSummary | null = null;
      let latestCenterPoints: CenterPoint[] = [];
      const newWindDataByModel: Record<string, WindPoint[][]> = {};
      const newHydrDataByModel: Record<string, (HydrDataStep | null)[]> = {};
      const newSummaryByModel: Record<string, SimulationSummary> = {};
      const newStepSummariesByModel: Record<string, SimulationSummary[]> = {};
      const errors: string[] = [];

      data.results.forEach(
        ({
          model,
          status,
          trajectory,
          summary,
          stepSummaries,
          centerPoints,
          windData,
          hydrData,
          error,
        }) => {
          if (status === 'ERROR') {
            errors.push(error || `${model} 분석 중 오류가 발생했습니다.`);
            return;
          }
          if (trajectory) {
            merged.push(...trajectory.map((p) => ({ ...p, model })));
          }
          if (summary) {
            newSummaryByModel[model] = summary;
            if (model === 'OpenDrift' || !latestSummary) latestSummary = summary;
          }
          if (stepSummaries) newStepSummariesByModel[model] = stepSummaries;
          if (windData) newWindDataByModel[model] = windData;
          if (hydrData) newHydrDataByModel[model] = hydrData;
          if (centerPoints) {
            latestCenterPoints = [
              ...latestCenterPoints,
              ...centerPoints.map((p) => ({ ...p, model })),
            ];
          }
        },
      );

      if (merged.length > 0) {
        setOilTrajectory(merged);
        const doneModels = new Set<PredictionModel>(
          data.results
            .filter((r) => r.status === 'DONE' && r.trajectory && r.trajectory.length > 0)
            .map((r) => r.model as PredictionModel),
        );
        setVisibleModels(doneModels);
        setSimulationSummary(latestSummary);
        setCenterPoints(latestCenterPoints);

        const refWindData = newWindDataByModel['OpenDrift'] ?? Object.values(newWindDataByModel)[0];
        const refHydrData = newHydrDataByModel['OpenDrift'] ?? Object.values(newHydrDataByModel)[0];
        doneModels.forEach((model) => {
          if (!newWindDataByModel[model] && refWindData) newWindDataByModel[model] = refWindData;
          if (!newHydrDataByModel[model] && refHydrData) newHydrDataByModel[model] = refHydrData;
        });

        setWindDataByModel(newWindDataByModel);
        setHydrDataByModel(newHydrDataByModel);
        setSummaryByModel(newSummaryByModel);
        setStepSummariesByModel(newStepSummariesByModel);
        const booms = generateAIBoomLines(merged, coord, algorithmSettings);
        setBoomLines(booms);
        setSensitiveResources([]);
        setCurrentStep(0);
        setIsPlaying(true);
        setFlyToCoord({ lon: coord.lon, lat: coord.lat });
      }

      if (errors.length > 0 && merged.length === 0) {
        setSimulationError(errors.join('; '));
      } else {
        simulationSucceeded = true;
        const effectiveAcdntSn = data.acdntSn ?? selectedAnalysis?.acdntSn;
        if (coord) {
          fetchWeatherSnapshotForCoord(coord.lat, coord.lon)
            .then((snapshot) => {
              useWeatherSnapshotStore.getState().setSnapshot(snapshot);
              if (effectiveAcdntSn) {
                api
                  .post(`/incidents/${effectiveAcdntSn}/weather`, snapshot)
                  .catch((err) => console.warn('[weather] 기상 저장 실패:', err));
              }
            })
            .catch((err) => console.warn('[weather] 기상 데이터 수집 실패:', err));
        }
        if (effectiveAcdntSn) {
          fetchSensitiveResources(effectiveAcdntSn)
            .then(setSensitiveResourceCategories)
            .catch((err) => console.warn('[prediction] 민감자원 조회 실패:', err));
          fetchSensitiveResourcesGeojson(effectiveAcdntSn)
            .then(setSensitiveResourceGeojson)
            .catch((err) => console.warn('[prediction] 민감자원 GeoJSON 조회 실패:', err));
        }
      }
    } catch (err) {
      const msg =
        (err as { message?: string })?.message ?? '시뮬레이션 실행 중 오류가 발생했습니다.';
      setSimulationError(msg);
    } finally {
      stopProgressTimer(simulationSucceeded);
      setIsRunningSimulation(false);
    }
  };

  const handleOpenReport = () => {
    const OIL_TYPE_CODE: Record<string, string> = {
      벙커C유: 'BUNKER_C',
      경유: 'DIESEL',
      원유: 'CRUDE_OIL',
      윤활유: 'LUBE_OIL',
    };
    const accidentName =
      selectedAnalysis?.acdntNm || analysisDetail?.acdnt?.acdntNm || incidentName || '(미입력)';
    const occurTime =
      selectedAnalysis?.occurredAt || analysisDetail?.acdnt?.occurredAt || accidentTime || '';
    const wx = analysisDetail?.weather?.[0] ?? null;
    const weatherSnapshot = useWeatherSnapshotStore.getState().snapshot;

    const payload: OilReportPayload = {
      incident: {
        name: accidentName,
        occurTime,
        location:
          selectedAnalysis?.location ||
          analysisDetail?.acdnt?.location ||
          (() => {
            const _lat = incidentCoord?.lat ?? selectedAnalysis?.lat ?? null;
            const _lon = incidentCoord?.lon ?? selectedAnalysis?.lon ?? null;
            return _lat != null && _lon != null
              ? `위도 ${Number(_lat).toFixed(4)}, 경도 ${Number(_lon).toFixed(4)}`
              : '';
          })(),
        lat: incidentCoord?.lat ?? selectedAnalysis?.lat ?? null,
        lon: incidentCoord?.lon ?? selectedAnalysis?.lon ?? null,
        pollutant: OIL_TYPE_CODE[oilType] || oilType,
        spillAmount: `${spillAmount} ${spillUnit}`,
        shipName: analysisDetail?.vessels?.[0]?.vesselNm || '',
      },
      pollution: {
        spillAmount: `${spillAmount.toFixed(2)} ${spillUnit}`,
        weathered: simulationSummary ? `${simulationSummary.weatheredVolume.toFixed(2)} m³` : '—',
        seaRemain: simulationSummary ? `${simulationSummary.remainingVolume.toFixed(2)} m³` : '—',
        pollutionArea: simulationSummary
          ? `${simulationSummary.pollutionArea.toFixed(2)} km²`
          : '—',
        coastAttach: simulationSummary ? `${simulationSummary.beachedVolume.toFixed(2)} m³` : '—',
        coastLength: simulationSummary
          ? `${simulationSummary.pollutionCoastLength.toFixed(2)} km`
          : '—',
        oilType: OIL_TYPE_CODE[oilType] || oilType,
      },
      weather: (() => {
        if (weatherSnapshot) {
          return {
            windDir: `${weatherSnapshot.wind.directionLabel} ${weatherSnapshot.wind.direction}°`,
            windSpeed: `${weatherSnapshot.wind.speed.toFixed(1)} m/s`,
            waveHeight: `${weatherSnapshot.wave.height.toFixed(1)} m`,
            temp: `${weatherSnapshot.temperature.current.toFixed(1)} °C`,
            pressure: `${weatherSnapshot.pressure} hPa`,
            visibility: `${weatherSnapshot.visibility} km`,
            salinity: `${weatherSnapshot.salinity} PSU`,
            waveMaxHeight: `${weatherSnapshot.wave.maxHeight.toFixed(1)} m`,
            wavePeriod: `${weatherSnapshot.wave.period} s`,
            currentDir: '',
            currentSpeed: '',
          };
        }
        if (wx) {
          return { windDir: wx.wind, windSpeed: wx.wind, waveHeight: wx.wave, temp: wx.temp };
        }
        return null;
      })(),
      spread: (() => {
        const fmt = (model: string) => {
          const s = summaryByModel[model];
          return s ? `${s.pollutionArea.toFixed(2)} km²` : '—';
        };
        return { kosps: fmt('KOSPS'), openDrift: fmt('OpenDrift'), poseidon: fmt('POSEIDON') };
      })(),
      coastal: {
        firstTime: (() => {
          const beachedTimes = oilTrajectory.filter((p) => p.stranded === 1).map((p) => p.time);
          if (beachedTimes.length === 0) return null;
          const d = new Date(Math.min(...beachedTimes) * 1000);
          return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
        })(),
      },
      spreadSteps: (() => {
        const steps = stepSummariesByModel[windHydrModel] ?? [];
        const toRow = (elapsed: string, s: (typeof steps)[0] | undefined) => ({
          elapsed,
          weathered: s ? s.weatheredVolume.toFixed(2) : '',
          seaRemain: s ? s.remainingVolume.toFixed(2) : '',
          coastAttach: s ? s.beachedVolume.toFixed(2) : '',
          area: s ? s.pollutionArea.toFixed(2) : '',
        });
        return [toRow('3시간', steps[3]), toRow('6시간', steps[6])];
      })(),
      hasSimulation: simulationSummary !== null,
      sensitiveResources:
        sensitiveResourceCategories.length > 0
          ? sensitiveResourceCategories.map((r) => ({
              category: r.category,
              count: r.count,
              totalArea: r.totalArea,
            }))
          : undefined,
      acdntSn: selectedAnalysis?.acdntSn ?? undefined,
      mapData: incidentCoord
        ? {
            center: [incidentCoord.lat, incidentCoord.lon],
            zoom: 10,
            trajectory: oilTrajectory,
            currentStep,
            centerPoints,
            simulationStartTime: accidentTime,
          }
        : null,
    };

    setOilReportPayload(payload);
    setReportGenCategory(0);
    navigateToTab('reports', 'generate');
  };

  return (
    <div className="relative flex flex-1 overflow-hidden">
      {/* Left Sidebar */}
      {activeSubTab === 'analysis' && (
        <div className="shrink-0 overflow-hidden" style={{ width: leftCollapsed ? 0 : 320 }}>
          <LeftPanel
            selectedAnalysis={selectedAnalysis}
            enabledLayers={enabledLayers}
            onToggleLayer={handleToggleLayer}
            accidentTime={accidentTime}
            onAccidentTimeChange={(v) => {
              setAccidentTime(v);
              setValidationErrors((prev) => {
                const n = new Set(prev);
                n.delete('accidentTime');
                return n;
              });
            }}
            incidentCoord={incidentCoord}
            onCoordChange={(v) => {
              setIncidentCoord(v);
              setValidationErrors((prev) => {
                const n = new Set(prev);
                n.delete('coord');
                return n;
              });
            }}
            isSelectingLocation={isSelectingLocation}
            onMapSelectClick={() => setIsSelectingLocation((prev) => !prev)}
            onRunSimulation={handleRunSimulation}
            isRunningSimulation={isRunningSimulation}
            selectedModels={selectedModels}
            onModelsChange={(v) => {
              setSelectedModels(v);
              setValidationErrors((prev) => {
                const n = new Set(prev);
                n.delete('models');
                return n;
              });
            }}
            visibleModels={visibleModels}
            onVisibleModelsChange={setVisibleModels}
            hasResults={oilTrajectory.length > 0}
            predictionTime={predictionTime}
            onPredictionTimeChange={setPredictionTime}
            spillType={spillType}
            onSpillTypeChange={setSpillType}
            oilType={oilType}
            onOilTypeChange={setOilType}
            spillAmount={spillAmount}
            onSpillAmountChange={setSpillAmount}
            incidentName={incidentName}
            onIncidentNameChange={(v) => {
              setIncidentName(v);
              setValidationErrors((prev) => {
                const n = new Set(prev);
                n.delete('incidentName');
                return n;
              });
            }}
            spillUnit={spillUnit}
            onSpillUnitChange={setSpillUnit}
            boomLines={boomLines}
            onBoomLinesChange={setBoomLines}
            showBoomLines={showBoomLines}
            onShowBoomLinesChange={setShowBoomLines}
            oilTrajectory={oilTrajectory}
            algorithmSettings={algorithmSettings}
            onAlgorithmSettingsChange={setAlgorithmSettings}
            isDrawingBoom={isDrawingBoom}
            onDrawingBoomChange={setIsDrawingBoom}
            drawingPoints={drawingPoints}
            onDrawingPointsChange={setDrawingPoints}
            containmentResult={containmentResult}
            onContainmentResultChange={setContainmentResult}
            layerOpacity={layerOpacity}
            onLayerOpacityChange={setLayerOpacity}
            layerBrightness={layerBrightness}
            onLayerBrightnessChange={setLayerBrightness}
            layerColors={layerColors}
            onLayerColorChange={(id, color) => setLayerColors((prev) => ({ ...prev, [id]: color }))}
            sensitiveResources={sensitiveResourceCategories}
            onImageAnalysisResult={handleImageAnalysisResult}
            onFlyToCoord={(c: { lon: number; lat: number }) =>
              setFlyToCoord({ lat: c.lat, lon: c.lon })
            }
            validationErrors={validationErrors}
          />
        </div>
      )}

      {/* Center - Map/Content Area */}
      <div className="flex-1 relative overflow-hidden">
        {/* Left panel toggle button */}
        {activeSubTab === 'analysis' && (
          <button
            onClick={() => setLeftCollapsed((v) => !v)}
            className="absolute z-[500] top-1/2 -translate-y-1/2 flex items-center justify-center text-[10px]"
            style={{
              left: 0,
              width: 18,
              height: 40,
              background: 'var(--bg-elevated)',
              border: '1px solid var(--stroke-default)',
              borderLeft: 'none',
              borderRadius: '0 6px 6px 0',
              color: 'var(--fg-sub)',
              cursor: 'pointer',
            }}
          >
            {leftCollapsed ? '▶' : '◀'}
          </button>
        )}

        {/* Right panel toggle button */}
        {activeSubTab === 'analysis' && (
          <button
            onClick={() => setRightCollapsed((v) => !v)}
            className="absolute z-[500] top-1/2 -translate-y-1/2 flex items-center justify-center text-[10px]"
            style={{
              right: 0,
              width: 18,
              height: 40,
              background: 'var(--bg-elevated)',
              border: '1px solid var(--stroke-default)',
              borderRight: 'none',
              borderRadius: '6px 0 0 6px',
              color: 'var(--fg-sub)',
              cursor: 'pointer',
            }}
          >
            {rightCollapsed ? '◀' : '▶'}
          </button>
        )}

        {activeSubTab === 'list' ? (
          <AnalysisListTable
            onTabChange={setActiveSubTab}
            onSelectAnalysis={handleSelectAnalysis}
          />
        ) : activeSubTab === 'theory' ? (
          <OilSpillTheoryView />
        ) : activeSubTab === 'boom-theory' ? (
          <BoomDeploymentTheoryView />
        ) : (
          <>
            <MapView
              enabledLayers={enabledLayers}
              incidentCoord={incidentCoord ?? undefined}
              flyToIncident={flyToCoord}
              isSelectingLocation={
                isSelectingLocation || isDrawingBoom || drawAnalysisMode === 'polygon'
              }
              onMapClick={handleMapClick}
              oilTrajectory={oilTrajectory.filter((p) =>
                visibleModels.has((p.model || 'OpenDrift') as PredictionModel),
              )}
              selectedModels={selectedModels}
              boomLines={boomLines}
              showBoomLines={showBoomLines}
              isDrawingBoom={isDrawingBoom}
              drawingPoints={drawingPoints}
              layerOpacity={layerOpacity}
              layerBrightness={layerBrightness}
              layerColors={layerColors}
              sensitiveResources={sensitiveResources}
              sensitiveResourceGeojson={
                displayControls.showSensitiveResources ? sensitiveResourceGeojson : null
              }
              centerPoints={centerPoints.filter((p) =>
                visibleModels.has((p.model || 'OpenDrift') as PredictionModel),
              )}
              windData={windData}
              hydrData={hydrData}
              flyToTarget={flyToTarget}
              fitBoundsTarget={fitBoundsTarget}
              onIncidentFlyEnd={handleFlyEnd}
              drawAnalysisMode={drawAnalysisMode}
              analysisPolygonPoints={analysisPolygonPoints}
              analysisCircleCenter={analysisCircleCenter}
              analysisCircleRadiusM={analysisCircleRadiusM}
              externalCurrentTime={oilTrajectory.length > 0 ? currentStep : undefined}
              backtrackReplay={
                isReplayActive && replayShips.length > 0 && incidentCoord
                  ? {
                      isActive: true,
                      ships: replayShips,
                      collisionEvent: collisionEvent ?? null,
                      replayFrame,
                      totalFrames: TOTAL_REPLAY_FRAMES,
                      incidentCoord,
                      backwardParticles,
                    }
                  : undefined
              }
              showCurrent={displayControls.showCurrent}
              showWind={displayControls.showWind}
              showBeached={displayControls.showBeached}
              showTimeLabel={displayControls.showTimeLabel}
              simulationStartTime={accidentTime || undefined}
            />

            {/* 타임라인 플레이어 (리플레이 비활성 시) */}
            {!isReplayActive &&
              (() => {
                const progressPct = maxTime > 0 ? (currentStep / maxTime) * 100 : 0;
                // 동적 라벨: 스텝 수에 따라 균등 분배
                const visibleLabels: number[] = (() => {
                  if (timeSteps.length === 0) return [0];
                  if (timeSteps.length <= 8) return timeSteps;
                  const interval = Math.ceil(timeSteps.length / 7);
                  return timeSteps.filter(
                    (_, i) => i % interval === 0 || i === timeSteps.length - 1,
                  );
                })();
                return (
                  <div
                    className="absolute bottom-0 left-0 right-0 h-[72px] flex items-center px-5 gap-4"
                    style={{
                      background: 'var(--bg-surface)',
                      backdropFilter: 'blur(16px)',
                      borderTop: '1px solid var(--stroke-default)',
                      zIndex: 1100,
                    }}
                  >
                    {/* 컨트롤 버튼 */}
                    <div className="flex gap-1 shrink-0">
                      {[
                        {
                          icon: '⏮',
                          action: () => {
                            setCurrentStep(timeSteps[0] ?? 0);
                            setIsPlaying(false);
                          },
                        },
                        {
                          icon: '◀',
                          action: () => {
                            const idx = timeSteps.indexOf(currentStep);
                            if (idx > 0) setCurrentStep(timeSteps[idx - 1]);
                          },
                        },
                      ].map((btn, i) => (
                        <button
                          key={i}
                          onClick={btn.action}
                          style={{
                            width: '34px',
                            height: '34px',
                            borderRadius: 'var(--rS, 4px)',
                            border: '1px solid var(--stroke-default)',
                            background: 'var(--bg-card)',
                            color: 'var(--fg-sub)',
                            display: 'flex',
                            alignItems: 'center',
                            justifyContent: 'center',
                            cursor: 'pointer',
                            fontSize: 'var(--font-size-body-2)',
                            transition: '0.2s',
                          }}
                        >
                          {btn.icon}
                        </button>
                      ))}
                      <button
                        onClick={() => {
                          if (!isPlaying && currentStep >= maxTime)
                            setCurrentStep(timeSteps[0] ?? 0);
                          setIsPlaying((p) => !p);
                        }}
                        style={{
                          width: '34px',
                          height: '34px',
                          borderRadius: 'var(--rS, 4px)',
                          border: isPlaying
                            ? '1px solid var(--color-accent)'
                            : '1px solid var(--stroke-default)',
                          background: isPlaying ? 'var(--color-accent)' : 'var(--bg-card)',
                          color: isPlaying ? 'var(--bg-base)' : 'var(--fg-sub)',
                          display: 'flex',
                          alignItems: 'center',
                          justifyContent: 'center',
                          cursor: 'pointer',
                          fontSize: 'var(--font-size-body-2)',
                          transition: '0.2s',
                        }}
                      >
                        {isPlaying ? '⏸' : '▶'}
                      </button>
                      {[
                        {
                          icon: '▶▶',
                          action: () => {
                            const idx = timeSteps.indexOf(currentStep);
                            if (idx < timeSteps.length - 1) setCurrentStep(timeSteps[idx + 1]);
                          },
                        },
                        {
                          icon: '⏭',
                          action: () => {
                            setCurrentStep(maxTime);
                            setIsPlaying(false);
                          },
                        },
                      ].map((btn, i) => (
                        <button
                          key={i}
                          onClick={btn.action}
                          style={{
                            width: '34px',
                            height: '34px',
                            borderRadius: 'var(--rS, 4px)',
                            border: '1px solid var(--stroke-default)',
                            background: 'var(--bg-card)',
                            color: 'var(--fg-sub)',
                            display: 'flex',
                            alignItems: 'center',
                            justifyContent: 'center',
                            cursor: 'pointer',
                            fontSize: 'var(--font-size-caption)',
                            transition: '0.2s',
                          }}
                        >
                          {btn.icon}
                        </button>
                      ))}
                      <div className="w-2" />
                      <button
                        onClick={() => setPlaySpeed(playSpeed >= 4 ? 1 : playSpeed * 2)}
                        style={{
                          width: '34px',
                          height: '34px',
                          borderRadius: 'var(--rS, 4px)',
                          border: '1px solid var(--stroke-default)',
                          background: 'var(--bg-card)',
                          color: 'var(--fg-sub)',
                          display: 'flex',
                          alignItems: 'center',
                          justifyContent: 'center',
                          cursor: 'pointer',
                          fontSize: 'var(--font-size-caption)',
                          fontWeight: 600,
                          fontFamily: 'var(--font-mono)',
                          transition: '0.2s',
                        }}
                      >
                        {playSpeed}×
                      </button>
                    </div>

                    {/* 타임라인 슬라이더 */}
                    <div className="flex-1 flex flex-col gap-1.5">
                      {/* 동적 시간 라벨 */}
                      <div className="relative h-4">
                        {visibleLabels.map((t) => {
                          const pos = maxTime > 0 ? (t / maxTime) * 100 : 0;
                          const isActive = t === currentStep;
                          return (
                            <span
                              key={t}
                              style={{
                                position: 'absolute',
                                left: `${pos}%`,
                                transform: 'translateX(-50%)',
                                fontSize: 'var(--font-size-caption)',
                                fontFamily: 'var(--font-mono)',
                                color: isActive ? 'var(--color-accent)' : 'var(--fg-disabled)',
                                fontWeight: isActive ? 600 : 400,
                                cursor: 'pointer',
                                whiteSpace: 'nowrap',
                              }}
                              onClick={() => setCurrentStep(t)}
                            >
                              {t}h
                            </span>
                          );
                        })}
                      </div>

                      {/* 슬라이더 트랙 */}
                      <div className="relative h-6 flex items-center">
                        <div
                          style={{
                            width: '100%',
                            height: '4px',
                            background: 'var(--stroke-default)',
                            borderRadius: '2px',
                            position: 'relative',
                            cursor: 'pointer',
                          }}
                          onClick={(e) => {
                            if (timeSteps.length === 0) return;
                            const rect = e.currentTarget.getBoundingClientRect();
                            const pct = (e.clientX - rect.left) / rect.width;
                            const targetTime = pct * maxTime;
                            const closest = timeSteps.reduce((a, b) =>
                              Math.abs(b - targetTime) < Math.abs(a - targetTime) ? b : a,
                            );
                            setCurrentStep(closest);
                          }}
                        >
                          {/* 진행 바 */}
                          <div
                            style={{
                              position: 'absolute',
                              top: 0,
                              left: 0,
                              width: `${progressPct}%`,
                              height: '100%',
                              background:
                                'linear-gradient(90deg, var(--color-accent), var(--color-info))',
                              borderRadius: '2px',
                              transition: 'width 0.15s',
                            }}
                          />
                          {/* 스텝 마커 (각 타임스텝 위치에 틱 표시) */}
                          {timeSteps.map((t) => {
                            const pos = maxTime > 0 ? (t / maxTime) * 100 : 0;
                            return (
                              <div
                                key={`tick-${t}`}
                                style={{
                                  position: 'absolute',
                                  width: '2px',
                                  height: '10px',
                                  background:
                                    t <= currentStep ? 'var(--color-accent)' : 'var(--fg-disabled)',
                                  top: '-3px',
                                  left: `${pos}%`,
                                  opacity: 0.6,
                                }}
                              />
                            );
                          })}
                          {/* 방어선 설치 이벤트 마커 */}
                          {boomLines.length > 0 &&
                            [
                              { pos: 4.2, label: '1차 방어선 설치 (+3h)' },
                              { pos: 8.3, label: '2차 방어선 설치 (+6h)' },
                              { pos: 12.5, label: '3차 방어선 설치 (+9h)' },
                            ]
                              .slice(0, boomLines.length)
                              .map((bm, i) => (
                                <div
                                  key={`bm-${i}`}
                                  title={bm.label}
                                  style={{
                                    position: 'absolute',
                                    top: '-18px',
                                    left: `${bm.pos}%`,
                                    transform: 'translateX(-50%)',
                                    fontSize: 'var(--font-size-caption)',
                                    cursor: 'pointer',
                                    filter: 'drop-shadow(0 0 4px rgba(245,158,11,0.5))',
                                  }}
                                >
                                  🛡
                                </div>
                              ))}
                        </div>
                        {/* 드래그 핸들 */}
                        <div
                          style={{
                            position: 'absolute',
                            left: `${progressPct}%`,
                            top: '50%',
                            transform: 'translate(-50%, -50%)',
                            width: '16px',
                            height: '16px',
                            background: 'var(--color-accent)',
                            border: '3px solid var(--bg-base)',
                            borderRadius: '50%',
                            cursor: 'grab',
                            boxShadow: '0 0 10px rgba(6,182,212,0.4)',
                            zIndex: 2,
                            transition: 'left 0.15s',
                          }}
                        />
                      </div>
                    </div>

                    {/* 시간 정보 */}
                    <div
                      style={{
                        display: 'flex',
                        flexDirection: 'column',
                        alignItems: 'flex-end',
                        gap: '4px',
                        flexShrink: 0,
                        minWidth: '200px',
                      }}
                    >
                      <div
                        style={{
                          fontSize: 'var(--font-size-body-2)',
                          fontWeight: 600,
                          color: 'var(--color-accent)',
                          fontFamily: 'var(--font-mono)',
                        }}
                      >
                        +{currentStep}h —{' '}
                        {(() => {
                          const base = accidentTime ? new Date(accidentTime) : new Date();
                          const d = new Date(base.getTime() + currentStep * 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 style={{ display: 'flex', gap: '14px' }}>
                        {(() => {
                          const stepSummary =
                            stepSummariesByModel[windHydrModel]?.[currentStep] ?? null;
                          const weatheredVal = stepSummary
                            ? `${stepSummary.weatheredVolume.toFixed(2)} m³`
                            : '—';
                          const areaVal = stepSummary
                            ? `${stepSummary.pollutionArea.toFixed(1)} km²`
                            : '—';
                          return [
                            { label: '풍화량', value: weatheredVal },
                            { label: '면적', value: areaVal },
                            {
                              label: '차단율',
                              value:
                                boomLines.length > 0
                                  ? `${Math.min(95, 70 + Math.round(progressPct * 0.2))}%`
                                  : '—',
                              color: 'var(--color-boom)',
                            },
                          ].map((s, i) => (
                            <div
                              key={i}
                              style={{
                                display: 'flex',
                                alignItems: 'center',
                                gap: '5px',
                                fontSize: 'var(--font-size-caption)',
                              }}
                            >
                              <span className="text-fg-default">{s.label}</span>
                              <span
                                style={{
                                  color: s.color,
                                  fontWeight: 600,
                                  fontFamily: 'var(--font-mono)',
                                }}
                              >
                                {s.value}
                              </span>
                            </div>
                          ));
                        })()}
                      </div>
                    </div>
                  </div>
                );
              })()}

            {/* 역추적 리플레이 바 */}
            {isReplayActive && (
              <BacktrackReplayBar
                isPlaying={isReplayPlaying}
                replayFrame={replayFrame}
                totalFrames={TOTAL_REPLAY_FRAMES}
                replaySpeed={replaySpeed}
                onTogglePlay={() => setIsReplayPlaying(!isReplayPlaying)}
                onSeek={setReplayFrame}
                onSpeedChange={setReplaySpeed}
                onClose={handleCloseReplay}
                replayShips={replayShips}
                collisionEvent={collisionEvent}
                replayTimeRange={replayTimeRange ?? undefined}
                hasBackwardParticles={backwardParticles.length > 0}
              />
            )}
          </>
        )}
      </div>

      {/* Right Panel */}
      {activeSubTab === 'analysis' && (
        <div className="shrink-0 overflow-hidden" style={{ width: rightCollapsed ? 0 : 300 }}>
          <RightPanel
            onOpenBacktrack={handleOpenBacktrack}
            onOpenRecalc={() => {
              if (!selectedAnalysis) {
                alert('선택된 사고가 없습니다.\n분석 목록에서 사고를 선택해주세요.');
                return;
              }
              setRecalcModalOpen(true);
            }}
            onOpenReport={handleOpenReport}
            detail={analysisDetail}
            summary={
              stepSummariesByModel[windHydrModel]?.[currentStep] ??
              summaryByModel[windHydrModel] ??
              simulationSummary
            }
            boomBlockedVolume={boomBlockedVolume}
            displayControls={displayControls}
            onDisplayControlsChange={setDisplayControls}
            windHydrModel={windHydrModel}
            windHydrModelOptions={windHydrModelOptions}
            onWindHydrModelChange={setWindHydrModel}
            analysisTab={analysisTab}
            onSwitchAnalysisTab={setAnalysisTab}
            drawAnalysisMode={drawAnalysisMode}
            analysisPolygonPoints={analysisPolygonPoints}
            circleRadiusNm={circleRadiusNm}
            onCircleRadiusChange={setCircleRadiusNm}
            analysisResult={analysisResult}
            incidentCoord={incidentCoord}
            centerPoints={centerPoints}
            predictionTime={predictionTime}
            onStartPolygonDraw={handleStartPolygonDraw}
            onRunPolygonAnalysis={handleRunPolygonAnalysis}
            onRunCircleAnalysis={handleRunCircleAnalysis}
            onCancelAnalysis={handleCancelAnalysis}
            onClearAnalysis={handleClearAnalysis}
          />
        </div>
      )}

      {/* 확산 예측 실행 중 로딩 오버레이 */}
      {isRunningSimulation && (
        <SimulationLoadingOverlay status="RUNNING" progress={simulationProgress} />
      )}

      {/* 확산 예측 에러 팝업 */}
      {simulationError && (
        <SimulationErrorModal message={simulationError} onClose={() => setSimulationError(null)} />
      )}

      {/* 재계산 모달 */}
      <RecalcModal
        isOpen={recalcModalOpen}
        onClose={() => setRecalcModalOpen(false)}
        incidentName={selectedAnalysis?.acdntNm || incidentName}
        oilType={oilType}
        spillAmount={spillAmount}
        spillType={spillType}
        predictionTime={predictionTime}
        incidentCoord={incidentCoord ?? { lat: 0, lon: 0 }}
        selectedModels={selectedModels}
        onSubmit={(params) => {
          setOilType(params.oilType);
          setSpillAmount(params.spillAmount);
          setSpillType(params.spillType);
          setPredictionTime(params.predictionTime);
          setIncidentCoord(params.incidentCoord);
          setSelectedModels(params.selectedModels);
          handleRunSimulation({
            models: params.selectedModels,
            oilType: params.oilType,
            spillAmount: params.spillAmount,
            spillType: params.spillType,
            predictionTime: params.predictionTime,
            incidentCoord: params.incidentCoord,
          });
        }}
      />

      {/* 역추적 모달 */}
      <BacktrackModal
        isOpen={backtrackModalOpen}
        onClose={() => setBacktrackModalOpen(false)}
        phase={backtrackPhase}
        conditions={backtrackConditions}
        vessels={backtrackVessels}
        onRunAnalysis={handleRunBacktrackAnalysis}
        onStartReplay={handleStartReplay}
      />
    </div>
  );
}
