import { useEffect, useRef, useCallback } from 'react';
import type { Layer } from '@deck.gl/core';
import { TripsLayer } from '@deck.gl/geo-layers';
import { ScatterplotLayer, IconLayer, PathLayer, PolygonLayer, TextLayer } from '@deck.gl/layers';
import { useGearReplayStore } from '../stores/gearReplayStore';
import { findFrameAtTime, interpolateMemberPositions } from '../stores/gearReplayPreprocess';
import type { MemberPosition } from '../stores/gearReplayPreprocess';
import { MODEL_ORDER, MODEL_COLORS } from '../components/korea/fleetClusterConstants';
import { buildInterpPolygon } from '../components/korea/fleetClusterUtils';
import type { GearCorrelationItem } from '../services/vesselAnalysis';
import { SHIP_ICON_MAPPING } from '../utils/shipIconSvg';

// ── Constants ─────────────────────────────────────────────────────────────────

const TRAIL_LENGTH_MS = 3_600_000;   // 1 hour trail
const RENDER_INTERVAL_MS = 100;      // 10fps throttle during playback

// ── Helper ───────────────────────────────────────────────────────────────────

function hexToRgb(hex: string): [number, number, number] {
  const h = hex.replace('#', '');
  return [
    parseInt(h.substring(0, 2), 16),
    parseInt(h.substring(2, 4), 16),
    parseInt(h.substring(4, 6), 16),
  ];
}

// ── Types ─────────────────────────────────────────────────────────────────────

interface CorrPosition {
  mmsi: string;
  name: string;
  lon: number;
  lat: number;
  cog: number;
  color: [number, number, number, number];
  isVessel: boolean;
}

// ── Hook ──────────────────────────────────────────────────────────────────────

/**
 * Gear group replay animation layers for deck.gl.
 *
 * Performance:
 * - currentTime changes are subscribed via zustand.subscribe (NOT React selectors).
 *   React never re-renders during playback.
 * - Layer objects are built imperatively and written to replayLayerRef.
 * - The parent calls overlay.setProps() to push layers to WebGL.
 */
export function useGearReplayLayers(
  replayLayerRef: React.MutableRefObject<Layer[]>,
  requestRender: () => void,
  shipsRef: React.MutableRefObject<Map<string, { lng: number; lat: number; course?: number }>>,
): void {
  // ── React selectors (infrequent changes only) ────────────────────────────
  const historyFrames = useGearReplayStore(s => s.historyFrames);
  const memberTripsData = useGearReplayStore(s => s.memberTripsData);
  const correlationTripsData = useGearReplayStore(s => s.correlationTripsData);
  const centerTrailSegments = useGearReplayStore(s => s.centerTrailSegments);
  const centerDotsPositions = useGearReplayStore(s => s.centerDotsPositions);
  const enabledModels = useGearReplayStore(s => s.enabledModels);
  const enabledVessels = useGearReplayStore(s => s.enabledVessels);
  const hoveredMmsi = useGearReplayStore(s => s.hoveredMmsi);
  const correlationByModel = useGearReplayStore(s => s.correlationByModel);
  const showTrails = useGearReplayStore(s => s.showTrails);
  const showLabels = useGearReplayStore(s => s.showLabels);

  // ── Refs ─────────────────────────────────────────────────────────────────
  const cursorRef = useRef(0);   // frame cursor for O(1) forward lookup

  // ── renderFrame ──────────────────────────────────────────────────────────

  // 디버그 로그 (첫 프레임에서만 출력)
  const debugLoggedRef = useRef(false);

  const renderFrame = useCallback(() => {
    if (historyFrames.length === 0) {
      replayLayerRef.current = [];
      requestRender();
      return;
    }

    const state = useGearReplayStore.getState();
    const ct = state.currentTime;
    const st = state.startTime;

    const shouldLog = !debugLoggedRef.current;
    if (shouldLog) debugLoggedRef.current = true;

    // Find current frame
    const { index: frameIdx, cursor } = findFrameAtTime(state.frameTimes, ct, cursorRef.current);
    cursorRef.current = cursor;

    const layers: Layer[] = [];

    // ── 항상 표시: 센터 트레일 + 도트 ──────────────────────────────────

    // Center trail segments (PathLayer) — 항상 ON
    for (let i = 0; i < centerTrailSegments.length; i++) {
      const seg = centerTrailSegments[i];
      if (seg.path.length < 2) continue;
      layers.push(new PathLayer({
        id: `replay-center-trail-${i}`,
        data: [{ path: seg.path }],
        getPath: (d: { path: [number, number][] }) => d.path,
        getColor: seg.isInterpolated
          ? [249, 115, 22, 200]
          : [251, 191, 36, 180],
        widthMinPixels: 2,
      }));
    }

    // Center dots (real data only) — 항상 ON
    if (centerDotsPositions.length > 0) {
      layers.push(new ScatterplotLayer({
        id: 'replay-center-dots',
        data: centerDotsPositions,
        getPosition: (d: [number, number]) => d,
        getFillColor: [251, 191, 36, 150],
        getRadius: 80,
        radiusUnits: 'meters',
        radiusMinPixels: 2.5,
      }));
    }

    // ── Dynamic layers (depend on currentTime) ────────────────────────────

    if (frameIdx < 0) {
      // No valid frame at this time — only show static layers
      replayLayerRef.current = layers;
      requestRender();
      return;
    }

    const frame = state.historyFrames[frameIdx];
    const isStale = !!frame._longGap || !!frame._interp;

    // Member positions (interpolated) — 항상 계산 (배지/오퍼레이셔널에서 사용)
    const members = interpolateMemberPositions(state.historyFrames, frameIdx, ct);
    const memberPts: [number, number][] = members.map(m => [m.lon, m.lat]);

    // ── "항적" 토글: 전체 24h 경로 PathLayer (정적 배경) ─────────────
    if (showTrails) {
      // 멤버 전체 항적 (identity)
      if (enabledModels.has('identity') && memberTripsData.length > 0) {
        for (const trip of memberTripsData) {
          if (trip.path.length < 2) continue;
          layers.push(new PathLayer({
            id: `replay-member-path-${trip.id}`,
            data: [{ path: trip.path }],
            getPath: (d: { path: [number, number][] }) => d.path,
            getColor: [180, 180, 180, 80], // 낮은 채도 — TripsLayer보다 연하게
            widthMinPixels: 1,
          }));
        }
      }
      // 연관 선박 전체 항적 (correlation)
      if (correlationTripsData.length > 0) {
        const activeMmsis = new Set<string>();
        for (const [mn, items] of correlationByModel) {
          if (!enabledModels.has(mn)) continue;
          for (const c of items as GearCorrelationItem[]) {
            if (enabledVessels.has(c.targetMmsi)) activeMmsis.add(c.targetMmsi);
          }
        }
        for (const trip of correlationTripsData) {
          if (!activeMmsis.has(trip.id) || trip.path.length < 2) continue;
          layers.push(new PathLayer({
            id: `replay-corr-path-${trip.id}`,
            data: [{ path: trip.path }],
            getPath: (d: { path: [number, number][] }) => d.path,
            getColor: [100, 140, 200, 60], // 연한 파랑
            widthMinPixels: 1,
          }));
        }
      }
    }

    // 1. Identity 모델: TripsLayer + 폴리곤 + 마커 (항상 ON)
    if (enabledModels.has('identity')) {
      // TripsLayer — member trails (GPU animated, 항상 ON, 고채도)
      if (memberTripsData.length > 0) {
        layers.push(new TripsLayer({
          id: 'replay-member-trails',
          data: memberTripsData,
          getPath: d => d.path,
          getTimestamps: d => d.timestamps,
          getColor: [255, 200, 60, 220], // 고채도 노란색 (항적보다 밝게)
          widthMinPixels: 2,
          fadeTrail: true,
          trailLength: TRAIL_LENGTH_MS,
          currentTime: ct - st,
        }));
      }

      // Current animated polygon (convex hull of members)
      const polygon = buildInterpPolygon(memberPts);
      if (polygon) {
        layers.push(new PolygonLayer({
          id: 'replay-polygon',
          data: [{ polygon: polygon.coordinates }],
          getPolygon: (d: { polygon: number[][][] }) => d.polygon,
          getFillColor: isStale ? [148, 163, 184, 30] : [251, 191, 36, 40],
          getLineColor: isStale ? [148, 163, 184, 100] : [251, 191, 36, 180],
          getLineWidth: isStale ? 1 : 2,
          lineWidthMinPixels: 1,
          filled: true,
          stroked: true,
        }));
      }
    }

    // 2. Correlation TripsLayer (GPU animated, 항상 ON, 고채도)
    if (correlationTripsData.length > 0) {
      const activeMmsis = new Set<string>();
      for (const [mn, items] of correlationByModel) {
        if (!enabledModels.has(mn)) continue;
        for (const c of items as GearCorrelationItem[]) {
          if (enabledVessels.has(c.targetMmsi)) activeMmsis.add(c.targetMmsi);
        }
      }
      const enabledTrips = correlationTripsData.filter(d => activeMmsis.has(d.id));
      if (enabledTrips.length > 0) {
        layers.push(new TripsLayer({
          id: 'replay-corr-trails',
          data: enabledTrips,
          getPath: d => d.path,
          getTimestamps: d => d.timestamps,
          getColor: [100, 180, 255, 220], // 고채도 파랑 (항적보다 밝게)
          widthMinPixels: 2.5,
          fadeTrail: true,
          trailLength: TRAIL_LENGTH_MS,
          currentTime: ct - st,
        }));
      }
    }

    // 4. Current center point
    layers.push(new ScatterplotLayer({
      id: 'replay-center',
      data: [{ position: [frame.centerLon, frame.centerLat] as [number, number] }],
      getPosition: (d: { position: [number, number] }) => d.position,
      getFillColor: isStale ? [249, 115, 22, 255] : [239, 68, 68, 255],
      getRadius: 200,
      radiusUnits: 'meters',
      radiusMinPixels: 7,
      stroked: true,
      getLineColor: [255, 255, 255, 255],
      lineWidthMinPixels: 2,
    }));

    // 5. Member position markers (IconLayer, identity 모델 활성 시만)
    if (members.length > 0 && enabledModels.has('identity')) {
      layers.push(new IconLayer<MemberPosition>({
        id: 'replay-members',
        data: members,
        getPosition: d => [d.lon, d.lat],
        getIcon: d => d.isGear ? SHIP_ICON_MAPPING['gear-diamond'] : SHIP_ICON_MAPPING['ship-triangle'],
        getSize: d => d.isParent ? 24 : d.isGear ? 14 : 18,
        getAngle: d => d.isGear ? 0 : -(d.cog || 0),
        getColor: d => {
          if (d.stale) return [100, 116, 139, 180];
          if (d.isGear) return [168, 184, 200, 230];
          return [251, 191, 36, 230];
        },
        sizeUnits: 'pixels',
        billboard: false,
      }));

      // Member labels — showLabels 제어
      if (showLabels) layers.push(new TextLayer<MemberPosition>({
        id: 'replay-member-labels',
        data: members,
        getPosition: d => [d.lon, d.lat],
        getText: d => d.name || d.mmsi,
        getColor: d => d.stale
          ? [148, 163, 184, 200]
          : d.isGear
            ? [226, 232, 240, 255]
            : [251, 191, 36, 255],
        getSize: 10,
        getPixelOffset: [0, 14],
        background: true,
        getBackgroundColor: [0, 0, 0, 200],
        backgroundPadding: [2, 1],
        fontFamily: '"Fira Code Variable", monospace',
      }));
    }

    // 6. Correlation vessel positions (트랙 보간 → 끝점 clamp → live fallback)
    const corrPositions: CorrPosition[] = [];
    const corrTrackMap = new Map(correlationTripsData.map(d => [d.id, d]));
    const liveShips = shipsRef.current;
    const relTime = ct - st;

    for (const [mn, items] of correlationByModel) {
      if (!enabledModels.has(mn)) continue;
      const color = MODEL_COLORS[mn] ?? '#94a3b8';
      const [r, g, b] = hexToRgb(color);

      for (const c of items as GearCorrelationItem[]) {
        if (!enabledVessels.has(c.targetMmsi)) continue; // OFF → 아이콘+트레일+폴리곤 모두 제외
        if (corrPositions.some(p => p.mmsi === c.targetMmsi)) continue;

        let lon: number | undefined;
        let lat: number | undefined;
        let cog = 0;

        // 방법 1: 트랙 데이터 (보간 + 범위 밖은 끝점 clamp)
        const tripData = corrTrackMap.get(c.targetMmsi);
        if (tripData && tripData.path.length > 0) {
          const ts = tripData.timestamps;
          const path = tripData.path;

          if (relTime <= ts[0]) {
            // 트랙 시작 전 → 첫 점 사용
            lon = path[0][0]; lat = path[0][1];
            if (path.length > 1) {
              const dx = path[1][0] - path[0][0];
              const dy = path[1][1] - path[0][1];
              cog = (dx === 0 && dy === 0) ? 0 : (Math.atan2(dx, dy) * 180 / Math.PI + 360) % 360;
            }
          } else if (relTime >= ts[ts.length - 1]) {
            // 트랙 종료 후 → 마지막 점 사용
            const last = path.length - 1;
            lon = path[last][0]; lat = path[last][1];
            if (last > 0) {
              const dx = path[last][0] - path[last - 1][0];
              const dy = path[last][1] - path[last - 1][1];
              cog = (dx === 0 && dy === 0) ? 0 : (Math.atan2(dx, dy) * 180 / Math.PI + 360) % 360;
            }
          } else {
            // 범위 내 → 보간
            let lo = 0;
            let hi = ts.length - 1;
            while (lo < hi - 1) {
              const mid = (lo + hi) >> 1;
              if (ts[mid] <= relTime) lo = mid; else hi = mid;
            }
            const ratio = ts[hi] !== ts[lo] ? (relTime - ts[lo]) / (ts[hi] - ts[lo]) : 0;
            lon = path[lo][0] + (path[hi][0] - path[lo][0]) * ratio;
            lat = path[lo][1] + (path[hi][1] - path[lo][1]) * ratio;
            const dx = path[hi][0] - path[lo][0];
            const dy = path[hi][1] - path[lo][1];
            cog = (dx === 0 && dy === 0) ? 0 : (Math.atan2(dx, dy) * 180 / Math.PI + 360) % 360;
          }
        }

        // 방법 2: live 선박 위치 fallback
        if (lon === undefined) {
          const ship = liveShips.get(c.targetMmsi);
          if (ship) {
            lon = ship.lng;
            lat = ship.lat;
            cog = ship.course ?? 0;
          }
        }

        if (lon === undefined || lat === undefined) continue;

        corrPositions.push({
          mmsi: c.targetMmsi,
          name: c.targetName || c.targetMmsi,
          lon,
          lat,
          cog,
          color: [r, g, b, 230],
          isVessel: c.targetType === 'VESSEL',
        });
      }
    }

    // 디버그: 첫 프레임에서 전체 상태 출력
    if (shouldLog) {
      const trackHit = corrPositions.filter(p => corrTrackMap.has(p.mmsi)).length;
      const liveHit = corrPositions.length - trackHit;
      const sampleTrip = memberTripsData[0];
      console.log('[GearReplay] renderFrame:', {
        historyFrames: state.historyFrames.length,
        memberTripsData: memberTripsData.length,
        corrTripsData: correlationTripsData.length,
        corrTrackMap: corrTrackMap.size,
        showTrails, showLabels,
        relTime: Math.round(relTime / 60000) + 'min',
        currentTime: Math.round((ct - st) / 60000) + 'min (rel)',
        members: members.length,
        corrPositions: corrPositions.length,
        posSource: `track:${trackHit} live:${liveHit}`,
        memberTrip0: sampleTrip ? { id: sampleTrip.id, pts: sampleTrip.path.length, tsRange: `${Math.round(sampleTrip.timestamps[0]/60000)}~${Math.round(sampleTrip.timestamps[sampleTrip.timestamps.length-1]/60000)}min` } : 'none',
      });
      // 모델별 상세
      for (const [mn, items] of state.correlationByModel) {
        const modEnabled = enabledModels.has(mn);
        const modPositions = corrPositions.filter(p => {
          return items.some(c => c.targetMmsi === p.mmsi);
        }).length;
        console.log(`  [${mn}] ${modEnabled ? 'ON' : 'OFF'} ${items.length}건 → 위치확인 ${modPositions}`);
      }
    }

    if (corrPositions.length > 0) {
      layers.push(new IconLayer<CorrPosition>({
        id: 'replay-corr-vessels',
        data: corrPositions,
        getPosition: d => [d.lon, d.lat],
        getIcon: d => d.isVessel ? SHIP_ICON_MAPPING['ship-triangle'] : SHIP_ICON_MAPPING['gear-diamond'],
        getSize: d => d.isVessel ? 18 : 12,
        getAngle: d => d.isVessel ? -(d.cog || 0) : 0,
        getColor: d => d.color,
        sizeUnits: 'pixels',
        billboard: false,
      }));

      if (showLabels) layers.push(new TextLayer<CorrPosition>({
        id: 'replay-corr-labels',
        data: corrPositions,
        getPosition: d => [d.lon, d.lat],
        getText: d => d.name,
        getColor: d => d.color,
        getSize: 8,
        getPixelOffset: [0, 15],
        background: true,
        getBackgroundColor: [0, 0, 0, 200],
        backgroundPadding: [2, 1],
      }));
    }

    // 7. Hover highlight
    if (hoveredMmsi) {
      const hoveredMember = members.find(m => m.mmsi === hoveredMmsi);
      const hoveredCorr = corrPositions.find(c => c.mmsi === hoveredMmsi);
      const hoveredPos: [number, number] | null = hoveredMember
        ? [hoveredMember.lon, hoveredMember.lat]
        : hoveredCorr
          ? [hoveredCorr.lon, hoveredCorr.lat]
          : null;

      if (hoveredPos) {
        layers.push(new ScatterplotLayer({
          id: 'replay-hover-glow',
          data: [{ position: hoveredPos }],
          getPosition: (d: { position: [number, number] }) => d.position,
          getFillColor: [255, 255, 255, 60],
          getRadius: 400,
          radiusUnits: 'meters',
          radiusMinPixels: 14,
        }));
        layers.push(new ScatterplotLayer({
          id: 'replay-hover-ring',
          data: [{ position: hoveredPos }],
          getPosition: (d: { position: [number, number] }) => d.position,
          getFillColor: [0, 0, 0, 0],
          getRadius: 250,
          radiusUnits: 'meters',
          radiusMinPixels: 8,
          stroked: true,
          getLineColor: [255, 255, 255, 255],
          lineWidthMinPixels: 2,
        }));
      }

      // Hover trail (from correlation track)
      const hoveredTrack = correlationTripsData.find(d => d.id === hoveredMmsi);
      if (hoveredTrack) {
        const relTime = ct - st;
        let clipIdx = hoveredTrack.timestamps.length;
        for (let i = 0; i < hoveredTrack.timestamps.length; i++) {
          if (hoveredTrack.timestamps[i] > relTime) {
            clipIdx = i;
            break;
          }
        }
        const clippedPath = hoveredTrack.path.slice(0, clipIdx);
        if (clippedPath.length >= 2) {
          layers.push(new PathLayer({
            id: 'replay-hover-trail',
            data: [{ path: clippedPath }],
            getPath: (d: { path: [number, number][] }) => d.path,
            getColor: [255, 255, 255, 180],
            widthMinPixels: 3,
          }));
        }
      }
    }

    // 8. Operational polygons (멤버 위치 + enabledVessels ON인 연관 선박으로 폴리곤 생성)
    for (const [mn, items] of correlationByModel) {
      if (!enabledModels.has(mn)) continue;
      const color = MODEL_COLORS[mn] ?? '#94a3b8';
      const [r, g, b] = hexToRgb(color);

      const extraPts: [number, number][] = [];
      for (const c of items as GearCorrelationItem[]) {
        // enabledVessels로 개별 on/off 제어 (토글 대응)
        if (!enabledVessels.has(c.targetMmsi)) continue;
        const cp = corrPositions.find(p => p.mmsi === c.targetMmsi);
        if (cp) extraPts.push([cp.lon, cp.lat]);
      }
      if (extraPts.length === 0) continue;

      const basePts = enabledModels.has('identity') ? memberPts : [];
      const opPolygon = buildInterpPolygon([...basePts, ...extraPts]);
      if (!opPolygon) continue;

      layers.push(new PolygonLayer({
        id: `replay-op-${mn}`,
        data: [{ polygon: opPolygon.coordinates }],
        getPolygon: (d: { polygon: number[][][] }) => d.polygon,
        getFillColor: [r, g, b, 30],
        getLineColor: [r, g, b, 200],
        getLineWidth: 2,
        lineWidthMinPixels: 2,
        filled: true,
        stroked: true,
      }));
    }

    // 9. Model badges (small colored dots next to each vessel/gear per model)
    {
      const badgeTargets = new Map<string, { lon: number; lat: number; models: Set<string> }>();

      // Identity model: group members
      if (enabledModels.has('identity')) {
        for (const m of members) {
          const e = badgeTargets.get(m.mmsi) ?? { lon: m.lon, lat: m.lat, models: new Set<string>() };
          e.lon = m.lon; e.lat = m.lat; e.models.add('identity');
          badgeTargets.set(m.mmsi, e);
        }
      }

      // Correlation models
      for (const [mn, items] of correlationByModel) {
        if (!enabledModels.has(mn)) continue;
        for (const c of items as GearCorrelationItem[]) {
          if (c.score < 0.3) continue;
          const cp = corrPositions.find(p => p.mmsi === c.targetMmsi);
          if (!cp) continue;
          const e = badgeTargets.get(c.targetMmsi) ?? { lon: cp.lon, lat: cp.lat, models: new Set<string>() };
          e.lon = cp.lon; e.lat = cp.lat; e.models.add(mn);
          badgeTargets.set(c.targetMmsi, e);
        }
      }

      // Render one ScatterplotLayer per model (offset by index)
      for (let mi = 0; mi < MODEL_ORDER.length; mi++) {
        const model = MODEL_ORDER[mi];
        if (!enabledModels.has(model)) continue;
        const color = MODEL_COLORS[model] ?? '#94a3b8';
        const [r, g, b] = hexToRgb(color);
        const badgeData: { position: [number, number] }[] = [];
        for (const [, t] of badgeTargets) {
          if (t.models.has(model)) badgeData.push({ position: [t.lon, t.lat] });
        }
        if (badgeData.length === 0) continue;
        layers.push(new ScatterplotLayer({
          id: `replay-badge-${model}`,
          data: badgeData,
          getPosition: (d: { position: [number, number] }) => d.position,
          getFillColor: [r, g, b, 255],
          getRadius: 3,
          radiusUnits: 'pixels',
          stroked: true,
          getLineColor: [0, 0, 0, 150],
          lineWidthMinPixels: 0.5,
          // Offset each model's badges to the right
          getPixelOffset: [10 + mi * 7, -6] as [number, number],
        }));
      }
    }

    // 디버그: 연관 선박 렌더링 상태
    if (corrPositions.length > 0 && !debugLoggedRef.current) {
      console.log('[GearReplay] corrPositions:', corrPositions.length, 'operationalPolygons:', layers.filter(l => l.id?.toString().startsWith('replay-op-')).length);
    }

    replayLayerRef.current = layers;
    requestRender();
  }, [
    historyFrames, memberTripsData, correlationTripsData,
    centerTrailSegments, centerDotsPositions,
    enabledModels, enabledVessels, hoveredMmsi, correlationByModel,
    showTrails, showLabels,
    replayLayerRef, requestRender,
  ]);

  // 데이터/필터 변경 시 디버그 로그 리셋
  useEffect(() => {
    debugLoggedRef.current = false;
    if (correlationByModel.size > 0) {
      console.log('[GearReplay] 데이터 갱신:', {
        models: [...correlationByModel.keys()],
        enabledModels: [...enabledModels],
        corrTrips: correlationTripsData.length,
      });
    }
  }, [correlationByModel, enabledModels, correlationTripsData]);

  // ── zustand.subscribe effect (currentTime → renderFrame) ─────────────────

  useEffect(() => {
    if (historyFrames.length === 0) return;

    // Initial render
    renderFrame();

    let lastRenderTime = 0;
    let pendingRafId: number | null = null;

    const unsub = useGearReplayStore.subscribe(
      s => s.currentTime,
      () => {
        const isPlaying = useGearReplayStore.getState().isPlaying;
        if (!isPlaying) {
          // Seek/pause — immediate render for responsiveness
          renderFrame();
          return;
        }
        const now = performance.now();
        if (now - lastRenderTime >= RENDER_INTERVAL_MS) {
          lastRenderTime = now;
          renderFrame();
        } else if (!pendingRafId) {
          pendingRafId = requestAnimationFrame(() => {
            pendingRafId = null;
            lastRenderTime = performance.now();
            renderFrame();
          });
        }
      },
    );

    return () => {
      unsub();
      if (pendingRafId) cancelAnimationFrame(pendingRafId);
    };
  }, [historyFrames, renderFrame]);

  // ── Cleanup on unmount ────────────────────────────────────────────────────

  useEffect(() => {
    return () => {
      replayLayerRef.current = [];
      requestRender();
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps -- intentional: run only on unmount
  }, []);
}
