// globe.jsx — NASA EONET globe
// Three render styles: "dots" (Variation A), "map" (generated filled
// continents), and "country" (loaded country borders for Variation B).
// Markers are positioned in a way that aligns 1:1 with the texture's UVs.

const { useEffect, useRef, useState } = React;

// IMPORTANT: lat/lng → 3D position must match the SphereGeometry's UV mapping.
// Three's SphereGeometry maps u=0 at -X and u=0.5 at +X. Our textures use
// equirectangular x = (lng + 180) / 360, so marker positions need the same
// 180-degree offset as the texture.
function latLngToVec3(lat, lng, radius) {
  const latR = lat * Math.PI / 180;
  const lngR = lng * Math.PI / 180;
  const x = radius * Math.cos(latR) * Math.cos(lngR);
  const y = radius * Math.sin(latR);
  const z = -radius * Math.cos(latR) * Math.sin(lngR);
  return { x, y, z };
}

// Higher-resolution land bitmap (3° lng × 3° lat). 1=land, 0=water.
// Rows go from lat 87 down to -87 in 3° steps (58 rows). Cols 120 (3° lng each, -180..180).
// This is hand-tuned for recognizable continent shapes.
const LAND_BITS_HI = (function build() {
  // Generate from a parametric definition of major landmasses (rough polygons).
  // Polygons defined as [[lat,lng],...] in degrees. Point-in-polygon test.
  const polys = [
    // North America
    [[71,-156],[70,-141],[68,-130],[60,-140],[55,-130],[49,-124],[40,-124],[33,-117],[28,-115],[20,-105],[18,-94],[26,-82],[31,-81],[35,-76],[40,-74],[44,-67],[48,-60],[52,-58],[58,-62],[60,-78],[63,-90],[68,-95],[72,-105],[74,-120],[71,-156]],
    // Greenland
    [[83,-30],[81,-15],[78,-20],[70,-22],[60,-43],[64,-52],[72,-58],[78,-65],[83,-50],[83,-30]],
    // Central America thin strip
    [[18,-94],[10,-83],[8,-78],[12,-72],[16,-86],[18,-94]],
    // South America
    [[12,-72],[10,-60],[5,-52],[-5,-35],[-15,-39],[-23,-42],[-34,-52],[-40,-62],[-50,-70],[-55,-71],[-50,-74],[-40,-72],[-30,-71],[-20,-70],[-10,-78],[-2,-80],[5,-78],[10,-77],[12,-72]],
    // Africa
    [[35,-6],[32,10],[31,32],[28,34],[20,36],[12,42],[3,42],[-3,40],[-12,40],[-25,32],[-34,25],[-34,18],[-30,15],[-20,12],[-10,8],[0,9],[6,3],[10,-12],[15,-17],[22,-17],[28,-12],[33,-9],[35,-6]],
    // Europe
    [[71,28],[68,40],[60,50],[55,40],[51,30],[45,28],[40,20],[36,14],[36,-9],[44,-9],[50,-5],[58,-5],[60,5],[68,15],[71,28]],
    // Middle East / Arabia
    [[36,40],[28,34],[20,42],[12,44],[12,52],[18,56],[24,57],[30,52],[36,46],[36,40]],
    // Russia / Siberia (massive)
    [[78,30],[78,150],[70,170],[65,178],[60,160],[55,140],[50,130],[42,128],[40,120],[42,108],[48,100],[50,88],[55,75],[60,60],[68,55],[71,40],[78,30]],
    // China / SE Asia
    [[50,88],[42,108],[40,120],[30,122],[22,116],[18,108],[10,108],[8,103],[2,101],[5,118],[12,124],[18,122],[22,114],[30,90],[40,80],[50,88]],
    // India
    [[36,70],[30,76],[24,72],[18,72],[12,76],[8,78],[12,82],[20,87],[26,90],[30,80],[36,72],[36,70]],
    // Indonesia / Malaysia archipelago (rough)
    [[2,95],[-2,100],[-7,108],[-9,118],[-8,128],[-3,130],[2,128],[5,118],[5,105],[2,95]],
    // Australia
    [[-12,130],[-12,142],[-18,146],[-25,153],[-34,151],[-37,147],[-38,141],[-35,138],[-32,130],[-32,122],[-26,114],[-22,114],[-15,123],[-12,130]],
    // New Zealand
    [[-35,173],[-41,174],[-46,168],[-44,170],[-41,172],[-35,173]],
    // Japan
    [[44,142],[40,141],[34,135],[32,130],[34,131],[36,136],[40,140],[44,142]],
    // UK / Ireland
    [[58,-5],[56,-2],[54,-1],[51,1],[51,-5],[55,-7],[58,-5]],
    // Iceland
    [[66,-23],[65,-13],[63,-19],[66,-23]],
    // Antarctica
    [[-66,-180],[-66,180],[-90,180],[-90,-180],[-66,-180]],
    // Madagascar
    [[-12,49],[-26,46],[-25,44],[-15,46],[-12,49]],
  ];

  function pointInPoly(lat, lng, poly) {
    let inside = false;
    for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
      const yi = poly[i][0], xi = poly[i][1];
      const yj = poly[j][0], xj = poly[j][1];
      const intersect = ((yi > lat) !== (yj > lat)) &&
        (lng < (xj - xi) * (lat - yi) / (yj - yi + 1e-9) + xi);
      if (intersect) inside = !inside;
    }
    return inside;
  }

  const cols = 120, rows = 58, latStep = 3, lngStep = 3, top = 87;
  const grid = [];
  for (let r = 0; r < rows; r++) {
    const lat = top - r * latStep;
    let row = "";
    for (let c = 0; c < cols; c++) {
      const lng = -180 + c * lngStep + lngStep / 2;
      let land = false;
      for (const p of polys) { if (pointInPoly(lat, lng, p)) { land = true; break; } }
      row += land ? "1" : "0";
    }
    grid.push(row);
  }
  return { grid, cols, rows, latStep, lngStep, top };
})();

function landAtHi(lat, lng) {
  const { grid, cols, rows, latStep, top } = LAND_BITS_HI;
  const row = Math.floor((top - lat) / latStep);
  if (row < 0 || row >= rows) return false;
  const col = Math.floor(((lng + 180) % 360 + 360) % 360 / (360 / cols));
  return grid[row][col] === "1";
}

// ─── Texture: dotted (Variation A style) ──────────────────────────────────
function makeDotTexture(size = 2048, dotColor = "rgba(255,255,255,0.85)") {
  const c = document.createElement("canvas");
  c.width = size; c.height = size / 2;
  const ctx = c.getContext("2d");
  ctx.clearRect(0, 0, c.width, c.height);
  const lngStep = 1.4, latStep = 1.6;
  for (let lat = -85; lat <= 85; lat += latStep) {
    const cosL = Math.cos(lat * Math.PI / 180);
    const stepAdj = Math.max(lngStep / Math.max(cosL, 0.15), lngStep);
    for (let lng = -180; lng <= 180; lng += stepAdj) {
      if (!landAtHi(lat, lng)) continue;
      const x = ((lng + 180) / 360) * c.width;
      const y = ((90 - lat) / 180) * c.height;
      ctx.beginPath();
      ctx.arc(x, y, 2.6, 0, Math.PI * 2);
      ctx.fillStyle = dotColor;
      ctx.fill();
    }
  }
  const tex = new THREE.CanvasTexture(c);
  tex.anisotropy = 4;
  return tex;
}

// ─── Texture: filled map (Variation B style) ──────────────────────────────
function makeMapTexture(size = 2048, opts = {}) {
  const {
    ocean = "#0f1620",
    land = "#1c2734",
    outline = "rgba(255,255,255,0.55)",
    grid = "rgba(255,255,255,0.05)",
  } = opts;
  const c = document.createElement("canvas");
  c.width = size; c.height = size / 2;
  const ctx = c.getContext("2d");

  // Ocean fill
  ctx.fillStyle = ocean;
  ctx.fillRect(0, 0, c.width, c.height);

  // Optional graticule (very subtle)
  ctx.strokeStyle = grid;
  ctx.lineWidth = 1;
  for (let lng = -180; lng <= 180; lng += 30) {
    const x = ((lng + 180) / 360) * c.width;
    ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, c.height); ctx.stroke();
  }
  for (let lat = -60; lat <= 60; lat += 30) {
    const y = ((90 - lat) / 180) * c.height;
    ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(c.width, y); ctx.stroke();
  }

  // Render land cells as filled rects
  const { grid: bits, cols, rows, latStep, lngStep, top } = LAND_BITS_HI;
  const cellW = c.width / cols;
  const cellH = c.height / (180 / latStep);
  ctx.fillStyle = land;
  for (let r = 0; r < rows; r++) {
    for (let col = 0; col < cols; col++) {
      if (bits[r][col] !== "1") continue;
      const lat = top - r * latStep;
      const x = col * cellW;
      const y = ((90 - lat) / 180) * c.height;
      // Draw slightly oversized to avoid hairline gaps
      ctx.fillRect(x - 0.5, y - 0.5, cellW + 1, cellH + 1);
    }
  }

  // Outline pass: trace cells where neighbor is water → draw a line on that edge
  ctx.strokeStyle = outline;
  ctx.lineWidth = 1.2;
  for (let r = 0; r < rows; r++) {
    for (let col = 0; col < cols; col++) {
      if (bits[r][col] !== "1") continue;
      const lat = top - r * latStep;
      const x = col * cellW;
      const y = ((90 - lat) / 180) * c.height;
      // Wrap longitude for left/right neighbor lookup
      const left = bits[r][(col - 1 + cols) % cols] === "1";
      const right = bits[r][(col + 1) % cols] === "1";
      const up = r > 0 && bits[r - 1][col] === "1";
      const down = r < rows - 1 && bits[r + 1][col] === "1";
      ctx.beginPath();
      if (!left)  { ctx.moveTo(x, y); ctx.lineTo(x, y + cellH); }
      if (!right) { ctx.moveTo(x + cellW, y); ctx.lineTo(x + cellW, y + cellH); }
      if (!up)    { ctx.moveTo(x, y); ctx.lineTo(x + cellW, y); }
      if (!down)  { ctx.moveTo(x, y + cellH); ctx.lineTo(x + cellW, y + cellH); }
      ctx.stroke();
    }
  }

  const tex = new THREE.CanvasTexture(c);
  tex.anisotropy = 4;
  return tex;
}

// ─── Texture: country-border map (Variation B refined style) ─────────────
function makeCountryTexture(size = 2048, opts = {}) {
  const {
    ocean = "#10100f",
    land = "rgba(255,255,255,0.09)",
    coastline = "rgba(255,255,255,0.24)",
    border = "rgba(255,255,255,0.16)",
    grid = "rgba(255,255,255,0.035)",
  } = opts;
  const c = document.createElement("canvas");
  c.width = size; c.height = size / 2;
  const ctx = c.getContext("2d");

  function resetBase() {
    ctx.fillStyle = ocean;
    ctx.fillRect(0, 0, c.width, c.height);
    ctx.strokeStyle = grid;
    ctx.lineWidth = 1;
    for (let lng = -180; lng <= 180; lng += 30) {
      const x = ((lng + 180) / 360) * c.width;
      ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, c.height); ctx.stroke();
    }
    for (let lat = -60; lat <= 60; lat += 30) {
      const y = ((90 - lat) / 180) * c.height;
      ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(c.width, y); ctx.stroke();
    }
  }

  function project(lng, lat) {
    return [((lng + 180) / 360) * c.width, ((90 - lat) / 180) * c.height];
  }

  function decodeArcs(topology) {
    const { scale = [1, 1], translate = [0, 0] } = topology.transform || {};
    return topology.arcs.map((arc) => {
      let x = 0, y = 0;
      return arc.map((p) => {
        x += p[0]; y += p[1];
        return [x * scale[0] + translate[0], y * scale[1] + translate[1]];
      });
    });
  }

  function arcPoints(arcs, ref) {
    const arc = arcs[ref < 0 ? ~ref : ref];
    const pts = ref < 0 ? [...arc].reverse() : arc;
    return pts;
  }

  function drawRing(arcs, ringRefs) {
    let first = true;
    let lastX = null;
    ringRefs.forEach((ref) => {
      const pts = arcPoints(arcs, ref);
      pts.forEach(([lng, lat], i) => {
        const [x, y] = project(lng, lat);
        if ((first && i === 0) || (lastX !== null && Math.abs(x - lastX) > c.width * 0.5)) ctx.moveTo(x, y);
        else ctx.lineTo(x, y);
        lastX = x;
      });
      first = false;
    });
  }

  function drawTopology(topology) {
    const arcs = decodeArcs(topology);
    const countries = topology.objects?.countries?.geometries || [];
    resetBase();

    ctx.fillStyle = land;
    ctx.strokeStyle = coastline;
    ctx.lineWidth = 1.25;
    countries.forEach((geo) => {
      const polys = geo.type === "Polygon" ? [geo.arcs] : geo.arcs;
      if (!polys) return;
      ctx.beginPath();
      polys.forEach((poly) => poly.forEach((ring) => drawRing(arcs, ring)));
      ctx.fill();
      ctx.stroke();
    });

    ctx.strokeStyle = border;
    ctx.lineWidth = 0.8;
    countries.forEach((geo) => {
      const polys = geo.type === "Polygon" ? [geo.arcs] : geo.arcs;
      if (!polys) return;
      polys.forEach((poly) => {
        poly.forEach((ring) => {
          ctx.beginPath();
          drawRing(arcs, ring);
          ctx.stroke();
        });
      });
    });
  }

  resetBase();
  const tex = new THREE.CanvasTexture(c);
  tex.anisotropy = 4;

  fetch("https://cdn.jsdelivr.net/npm/world-atlas@2.0.2/countries-110m.json")
    .then((res) => res.ok ? res.json() : null)
    .then((topology) => {
      if (!topology) return;
      drawTopology(topology);
      tex.needsUpdate = true;
    })
    .catch(() => {
      const fallback = makeMapTexture(size, { ocean, land, outline: coastline, grid });
      tex.image = fallback.image;
      tex.needsUpdate = true;
    });

  return tex;
}

window.NasaGlobe = function NasaGlobe({
  size = 520,
  dark = true,
  accent = "#ff6b35",
  onActiveChange,
  onEventSelect,
  selectedEventId,
  events = [],
  rotationSpeed = 0.00095,
  showAtmosphere = true,
  mapStyle = "dots", // "dots" | "map" | "country"
  mapColors = null,  // optional override for "map" style
  atmosphereScale = 1.12,
  atmosphereIntensity = 0.85,
}) {
  const mountRef = useRef(null);
  const committedActiveRef = useRef(null);  // last index we actually fired onActiveChange for
  const candidateRef = useRef(null);        // current front-runner index
  const candidateSinceRef = useRef(0);      // when did this candidate take the lead
  const selectedRef = useRef(selectedEventId || null);

  useEffect(() => {
    selectedRef.current = selectedEventId || null;
  }, [selectedEventId]);

  useEffect(() => {
    const mount = mountRef.current;
    if (!mount) return;

    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(35, 1, 0.1, 100);
    camera.position.set(0, 0, 7);

    const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    renderer.setSize(size, size);
    renderer.setClearColor(0x000000, 0);
    renderer.domElement.style.cursor = "pointer";
    mount.appendChild(renderer.domElement);

    const globeGroup = new THREE.Group();
    scene.add(globeGroup);
    const radius = 2;

    if (mapStyle === "map" || mapStyle === "country") {
      // Filled map texture as the entire globe surface — opaque ocean + land
      const makeTexture = mapStyle === "country" ? makeCountryTexture : makeMapTexture;
      const mapTex = makeTexture(2048, mapColors || {
        ocean: dark ? "#0e1722" : "#dde6ee",
        land: dark ? "#202b3a" : "#f5efe4",
        outline: dark ? "rgba(255,255,255,0.55)" : "rgba(0,0,0,0.45)",
        coastline: dark ? "rgba(255,255,255,0.35)" : "rgba(0,0,0,0.35)",
        border: dark ? "rgba(255,255,255,0.22)" : "rgba(0,0,0,0.24)",
        grid: dark ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.05)",
      });
      const sphereGeo = new THREE.SphereGeometry(radius, 96, 96);
      const sphereMat = new THREE.MeshBasicMaterial({ map: mapTex, transparent: true });
      globeGroup.add(new THREE.Mesh(sphereGeo, sphereMat));

      // Subtle mesh overlay only for the generated map; country-border mode
      // already has its own graticule and this shell can read as a stray ring.
      if (mapStyle === "map") {
        const wireGeo = new THREE.SphereGeometry(radius * 1.001, 36, 18);
        const wireMat = new THREE.LineBasicMaterial({
          color: dark ? 0xffffff : 0x000000,
          transparent: true,
          opacity: dark ? 0.04 : 0.05,
        });
        globeGroup.add(new THREE.LineSegments(new THREE.WireframeGeometry(wireGeo), wireMat));
      }
    } else {
      // Dotted style
      const baseMat = new THREE.MeshBasicMaterial({
        color: dark ? 0x0a0a0a : 0xfaf7f2,
        transparent: true, opacity: dark ? 0.85 : 0.6,
      });
      globeGroup.add(new THREE.Mesh(new THREE.SphereGeometry(radius * 0.985, 64, 64), baseMat));
      const dotTex = makeDotTexture(2048, dark ? "rgba(255,255,255,0.55)" : "rgba(20,20,20,0.6)");
      const dotMat = new THREE.MeshBasicMaterial({ map: dotTex, transparent: true, opacity: 0.95 });
      globeGroup.add(new THREE.Mesh(new THREE.SphereGeometry(radius, 96, 96), dotMat));
      const wireGeo = new THREE.SphereGeometry(radius * 1.001, 36, 18);
      const wireMat = new THREE.LineBasicMaterial({
        color: dark ? 0xffffff : 0x222222, transparent: true, opacity: dark ? 0.06 : 0.08,
      });
      globeGroup.add(new THREE.LineSegments(new THREE.WireframeGeometry(wireGeo), wireMat));
    }

    // Atmosphere glow
    if (showAtmosphere) {
      const atmoGeo = new THREE.SphereGeometry(radius * atmosphereScale, 64, 64);
      const atmoMat = new THREE.ShaderMaterial({
        transparent: true, side: THREE.BackSide,
        uniforms: { uColor: { value: new THREE.Color(accent) }, uIntensity: { value: atmosphereIntensity } },
        vertexShader: `varying vec3 vNormal; void main(){ vNormal=normalize(normalMatrix*normal); gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0);} `,
        fragmentShader: `varying vec3 vNormal; uniform vec3 uColor; uniform float uIntensity; void main(){ float i=pow(0.7-dot(vNormal,vec3(0.0,0.0,1.0)),2.5); gl_FragColor=vec4(uColor,1.0)*i*uIntensity;} `,
      });
      scene.add(new THREE.Mesh(atmoGeo, atmoMat));
    }

    // Event markers
    const markerGroup = new THREE.Group();
    globeGroup.add(markerGroup);
    const markers = [];
    const markerTargets = [];
    const markerColor = new THREE.Color(accent);
    const selectedColor = new THREE.Color("#ff9a3d");
    const inactiveColor = new THREE.Color(accent).lerp(new THREE.Color("#ffffff"), 0.12);
    events.forEach((ev, i) => {
      const { lat, lng } = ev;
      if (lat == null || lng == null) return;
      const pos = latLngToVec3(lat, lng, radius);
      const pinPos = latLngToVec3(lat, lng, radius * 1.052);
      const ringPos = latLngToVec3(lat, lng, radius * 1.052);
      const hitPos = latLngToVec3(lat, lng, radius * 1.075);
      const pin = new THREE.Mesh(
        new THREE.CircleGeometry(0.046, 6),
        new THREE.MeshBasicMaterial({
          color: new THREE.Color(accent),
          transparent: true,
          opacity: 0.62,
          side: THREE.DoubleSide,
          depthWrite: false,
          depthTest: false,
        })
      );
      pin.userData.eventId = ev.id;
      pin.position.set(pinPos.x, pinPos.y, pinPos.z);
      pin.lookAt(0, 0, 0);
      pin.rotateZ(Math.PI / 6);
      pin.renderOrder = 3;
      markerGroup.add(pin);
      const ring = new THREE.Mesh(
        new THREE.RingGeometry(0.064, 0.086, 6),
        new THREE.MeshBasicMaterial({
          color: new THREE.Color(accent),
          transparent: true,
          opacity: 0.28,
          side: THREE.DoubleSide,
          depthWrite: false,
          depthTest: false,
        })
      );
      ring.userData.eventId = ev.id;
      ring.position.set(ringPos.x, ringPos.y, ringPos.z);
      ring.lookAt(0, 0, 0);
      ring.rotateZ(Math.PI / 6);
      ring.renderOrder = 2;
      markerGroup.add(ring);
      const hit = new THREE.Mesh(
        new THREE.SphereGeometry(0.1, 16, 16),
        new THREE.MeshBasicMaterial({ transparent: true, opacity: 0 })
      );
      hit.userData.eventId = ev.id;
      hit.position.set(hitPos.x, hitPos.y, hitPos.z);
      markerGroup.add(hit);
      markers.push({ ev, pin, ring, hit, basePos: pos, index: i });
      markerTargets.push(hit, pin, ring);
    });

    const raycaster = new THREE.Raycaster();
    const pointer = new THREE.Vector2();
    const onPointerDown = (e) => {
      const rect = renderer.domElement.getBoundingClientRect();
      pointer.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
      pointer.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
      raycaster.setFromCamera(pointer, camera);
      const hits = raycaster.intersectObjects(markerTargets, false);
      const hit = hits.find((h) => h.object?.userData?.eventId);
      if (!hit) return;
      const eventId = hit.object.userData.eventId;
      const marker = markers.find((m) => m.ev.id === eventId);
      if (!marker) return;
      selectedRef.current = eventId;
      if (onEventSelect) onEventSelect(marker.ev);
    };
    renderer.domElement.addEventListener("pointerdown", onPointerDown);

    let raf, t0 = performance.now();
    function tick(now) {
      const dt = (now - t0) / 1000; t0 = now;
      globeGroup.rotation.y += rotationSpeed * 60 * (dt || 0.016);

      let bestIdx = -1, bestZ = -Infinity;
      const camDir = new THREE.Vector3(0, 0, 1);
      markers.forEach((m, i) => {
        const world = m.pin.getWorldPosition(new THREE.Vector3());
        const z = world.dot(camDir);
        const s = 1 + Math.sin(now * 0.003 + i) * 0.5;
        const isSelected = selectedRef.current === m.ev.id;
        const frontness = THREE.MathUtils.clamp((z + 1.2) / 3.0, 0, 1);
        const ringPulse = 1 + (s - 1) * 0.5;
        m.ring.scale.setScalar(isSelected ? 1.34 : ringPulse);
        m.ring.material.opacity = frontness * (isSelected ? 0.42 : Math.max(0, 0.20 - (ringPulse - 1) * 0.18));
        m.pin.material.opacity = frontness * (isSelected ? 0.86 : 0.58);
        m.pin.material.color.copy(isSelected ? selectedColor : inactiveColor);
        m.ring.material.color.copy(isSelected ? selectedColor : markerColor);
        m.pin.scale.setScalar(isSelected ? 2.15 : 1);
        m.pin.lookAt(camera.position);
        m.pin.rotateZ(Math.PI / 6);
        m.ring.lookAt(camera.position);
        m.ring.rotateZ(Math.PI / 6);
        if (z > bestZ) { bestZ = z; bestIdx = i; }
      });
      if (bestIdx >= 0) {
        const m = markers[bestIdx];
        const isSelected = selectedRef.current === m.ev.id;
        const world = m.pin.getWorldPosition(new THREE.Vector3());
        const frontness = THREE.MathUtils.clamp((world.dot(camDir) + 1.2) / 3.0, 0, 1);
        m.pin.scale.setScalar(isSelected ? 2.35 : 1.75);
        m.ring.scale.setScalar(isSelected ? 1.42 : 1.18);
        m.ring.material.opacity = frontness * (isSelected ? 0.38 : 0.22);
        // Track candidate — reset dwell timer when the front-runner changes
        if (candidateRef.current !== bestIdx) {
          candidateRef.current = bestIdx;
          candidateSinceRef.current = now;
        }
        // Commit and fire callback only once the candidate has held long enough
        // for the adjacent feed item to be readable.
        if (now - candidateSinceRef.current >= 1600 && committedActiveRef.current !== bestIdx) {
          committedActiveRef.current = bestIdx;
          if (onActiveChange) onActiveChange(m.ev, bestIdx);
        }
      }
      renderer.render(scene, camera);
      raf = requestAnimationFrame(tick);
    }
    raf = requestAnimationFrame(tick);

    return () => {
      cancelAnimationFrame(raf);
      renderer.domElement.removeEventListener("pointerdown", onPointerDown);
      renderer.dispose();
      if (renderer.domElement && renderer.domElement.parentNode) renderer.domElement.parentNode.removeChild(renderer.domElement);
    };
    // eslint-disable-next-line
  }, [size, dark, accent, JSON.stringify(events.map(e => [e.id, e.lat, e.lng])), rotationSpeed, showAtmosphere, mapStyle, JSON.stringify(mapColors || {}), atmosphereScale, atmosphereIntensity]);

  return <div ref={mountRef} style={{ width: size, height: size }} />;
};

window.useEonetEvents = function useEonetEvents(limit = 80) {
  const [events, setEvents] = useState([]);
  const [loading, setLoading] = useState(true);
  useEffect(() => {
    let alive = true;
    async function run() {
      try {
        const res = await fetch(`https://eonet.gsfc.nasa.gov/api/v3/events?limit=200&days=120&status=all`);
        const json = await res.json();
        if (!alive) return;
        const evs = (json.events || []).map(e => {
          const g = e.geometry && e.geometry[e.geometry.length - 1];
          if (!g || !g.coordinates) return null;
          const [lng, lat] = g.coordinates.flat ? g.coordinates.flat(Infinity).slice(0, 2) : g.coordinates;
          return {
            id: e.id, title: e.title,
            category: (e.categories && e.categories[0] && e.categories[0].title) || "Event",
            date: g.date, lat, lng, link: e.link, sources: e.sources,
          };
        }).filter(Boolean);

        evs.sort((a, b) => (a.date < b.date ? 1 : -1));
        setEvents(evs.slice(0, limit));
        setLoading(false);
      } catch (err) {
        const fallback = [
          { id: "f1", title: "Wildfire near Cascade Range", category: "Wildfires", date: "2026-04-29T12:00:00Z", lat: 44.2, lng: -121.7 },
          { id: "f2", title: "Tropical Storm Hilary", category: "Severe Storms", date: "2026-04-28T08:30:00Z", lat: 14.5, lng: -110.2 },
          { id: "f3", title: "Volcanic Activity — Mt. Etna", category: "Volcanoes", date: "2026-04-27T03:15:00Z", lat: 37.75, lng: 14.99 },
          { id: "f4", title: "Iceberg A-83 calving event", category: "Sea and Lake Ice", date: "2026-04-26T22:00:00Z", lat: -75.1, lng: -28.4 },
          { id: "f5", title: "Dust Storm — Sahara", category: "Dust and Haze", date: "2026-04-26T11:40:00Z", lat: 22.1, lng: 8.7 },
          { id: "f6", title: "Flooding — Bangladesh Delta", category: "Floods", date: "2026-04-25T15:20:00Z", lat: 23.7, lng: 90.4 },
          { id: "f7", title: "Wildfire — Western Australia", category: "Wildfires", date: "2026-04-25T05:00:00Z", lat: -31.9, lng: 116.0 },
          { id: "f8", title: "Severe Storm — North Atlantic", category: "Severe Storms", date: "2026-04-24T19:10:00Z", lat: 48.5, lng: -35.0 },
          { id: "f9", title: "Volcano — Sakurajima", category: "Volcanoes", date: "2026-04-24T02:25:00Z", lat: 31.59, lng: 130.66 },
          { id: "f10", title: "Wildfire — Canadian Boreal", category: "Wildfires", date: "2026-04-23T17:30:00Z", lat: 55.8, lng: -110.1 },
          { id: "f11", title: "Severe Storm — Bay of Bengal", category: "Severe Storms", date: "2026-04-22T08:00:00Z", lat: 16.5, lng: 88.0 },
          { id: "f12", title: "Wildfire — Patagonia", category: "Wildfires", date: "2026-04-22T14:30:00Z", lat: -42.8, lng: -71.5 },
          { id: "f13", title: "Volcano — Iceland Reykjanes", category: "Volcanoes", date: "2026-04-21T09:15:00Z", lat: 63.9, lng: -22.5 },
          { id: "f14", title: "Drought — Horn of Africa", category: "Drought", date: "2026-04-20T00:00:00Z", lat: 5.5, lng: 42.5 },
          { id: "f15", title: "Iceberg D-44 drifting", category: "Sea and Lake Ice", date: "2026-04-19T11:00:00Z", lat: -65.4, lng: 95.2 },
          { id: "f16", title: "Wildfire — Siberia Taiga", category: "Wildfires", date: "2026-04-18T13:20:00Z", lat: 62.0, lng: 105.0 },
          { id: "f17", title: "Tropical Cyclone — South Pacific", category: "Severe Storms", date: "2026-04-17T07:00:00Z", lat: -15.0, lng: -165.0 },
          { id: "f18", title: "Flooding — Amazon Basin", category: "Floods", date: "2026-04-16T16:45:00Z", lat: -3.5, lng: -62.0 },
        ];
        setEvents(fallback);
        setLoading(false);
      }
    }
    run();
    return () => { alive = false; };
  }, [limit]);
  return { events, loading };
};
