// Globe orthographique canvas — rotation, zoom, survol, clic, survols d'arcs
function Globe({ accent, glowOn, autoRotate, overlays, filtres, selected, extras, onSelect, onHover, registerApi }) {
  const wrapRef = React.useRef(null);
  const canvasRef = React.useRef(null);
  const st = React.useRef({
    rot: [95, -28], k: 1, w: 800, h: 600, land: null,
    drag: null, moved: 0, lastInteract: 0, anim: null, hits: [], hover: null, dashT: 0,
  }).current;
  const baseNodes = React.useMemo(() => window.computeGlobeNodes(), []);
  // nodes courants (base curée + tickers recherchés) dans des refs lus par la boucle de rendu
  const nodesRef = React.useRef(baseNodes);
  const byIdRef = React.useRef(Object.fromEntries(baseNodes.map((n) => [n.soc.id, n])));
  React.useEffect(() => {
    const extra = (extras || []).map((e) => ({ soc: e.soc, lon: e.lon, lat: e.lat, clusterKey: "__dyn" }));
    nodesRef.current = baseNodes.concat(extra);
    byIdRef.current = Object.fromEntries(nodesRef.current.map((n) => [n.soc.id, n]));
  }, [extras]);

  // état restauré (validé : on ignore une vue corrompue)
  React.useEffect(() => {
    try {
      const v = JSON.parse(localStorage.getItem("carteIA.globe.view") || "null");
      if (v && Array.isArray(v.rot) && v.rot.length === 2 && v.rot.every(Number.isFinite)) {
        st.rot = v.rot;
        st.k = Number.isFinite(v.k) && v.k >= 0.7 && v.k <= 14 ? v.k : 1;
      }
    } catch (e) {}
  }, []);

  const propsRef = React.useRef({});
  propsRef.current = { accent, glowOn, autoRotate, overlays, filtres, selected, onSelect, onHover };

  React.useEffect(() => {
    const canvas = canvasRef.current, wrap = wrapRef.current;
    const ctx = canvas.getContext("2d");
    const projection = d3.geoOrthographic().clipAngle(90);
    const path = d3.geoPath(projection, ctx);
    const graticule = d3.geoGraticule10();
    let raf, saveTimer;

    // terre (contours) — fallback : graticule seul
    const urls = [
      window.__resources && window.__resources.landTopo, // version embarquée (export autonome)
      "https://cdn.jsdelivr.net/npm/world-atlas@2/land-110m.json",
      "https://unpkg.com/world-atlas@2/land-110m.json",
    ].filter(Boolean);
    (async () => {
      for (const u of urls) {
        try {
          const topo = await (await fetch(u)).json();
          st.land = topojson.feature(topo, topo.objects.land);
          break;
        } catch (e) {}
      }
    })();

    const resize = () => {
      const r = wrap.getBoundingClientRect();
      const dpr = window.devicePixelRatio || 1;
      st.w = r.width; st.h = r.height;
      canvas.width = r.width * dpr; canvas.height = r.height * dpr;
      canvas.style.width = r.width + "px"; canvas.style.height = r.height + "px";
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    };
    resize();
    const ro = new ResizeObserver(resize);
    ro.observe(wrap);

    const saveView = () => {
      clearTimeout(saveTimer);
      saveTimer = setTimeout(() => {
        try { localStorage.setItem("carteIA.globe.view", JSON.stringify({ rot: st.rot, k: st.k })); } catch (e) {}
      }, 400);
    };

    const NODE_PX = { MEGA: 9, LARGE: 6.5, MID: 4.6, SMALL: 3.2 };
    const CLUSTER_K = 2.1; // sous ce zoom, les foyers denses sont regroupés en bulle
    const bigClusterKeys = new Set((window.GLOBE_CLUSTERS || []).map((c) => c.key));
    const center = () => [-st.rot[0], -st.rot[1]];

    const drawArc = (a, b, color, width, dash, dashOffset) => {
      const interp = d3.geoInterpolate(a, b);
      ctx.beginPath();
      let pen = false;
      for (let i = 0; i <= 48; i++) {
        const p = interp(i / 48);
        if (d3.geoDistance(p, center()) > 1.5) { pen = false; continue; }
        const xy = projection(p);
        if (!xy) { pen = false; continue; }
        if (!pen) { ctx.moveTo(xy[0], xy[1]); pen = true; }
        else ctx.lineTo(xy[0], xy[1]);
      }
      ctx.strokeStyle = color; ctx.lineWidth = width;
      ctx.setLineDash(dash); ctx.lineDashOffset = dashOffset;
      ctx.stroke(); ctx.setLineDash([]);
    };

    const render = () => {
      const P = propsRef.current;
      const { w, h } = st;
      const R = (Math.min(w, h) / 2 - 24) * st.k;
      projection.rotate(st.rot).translate([w / 2, h / 2]).scale(R);
      ctx.clearRect(0, 0, w, h);

      // halo atmosphérique
      const grdA = ctx.createRadialGradient(w / 2, h / 2, R * 0.96, w / 2, h / 2, R * 1.12);
      grdA.addColorStop(0, P.accent + "33"); grdA.addColorStop(1, "transparent");
      ctx.fillStyle = grdA;
      ctx.beginPath(); ctx.arc(w / 2, h / 2, R * 1.12, 0, Math.PI * 2); ctx.fill();

      // océan
      const grd = ctx.createRadialGradient(w / 2 - R * 0.35, h / 2 - R * 0.4, R * 0.1, w / 2, h / 2, R);
      grd.addColorStop(0, "oklch(0.26 0.055 255)");
      grd.addColorStop(0.65, "oklch(0.2 0.055 258)");
      grd.addColorStop(1, "oklch(0.15 0.05 262)");
      ctx.fillStyle = grd;
      ctx.beginPath(); ctx.arc(w / 2, h / 2, R, 0, Math.PI * 2); ctx.fill();

      // graticule
      ctx.beginPath(); path(graticule);
      ctx.strokeStyle = "oklch(0.4 0.05 245 / 0.25)"; ctx.lineWidth = 0.5; ctx.stroke();

      // terres
      if (st.land) {
        ctx.beginPath(); path(st.land);
        ctx.fillStyle = "oklch(0.31 0.05 250)";
        ctx.fill();
        ctx.strokeStyle = "oklch(0.52 0.08 235 / 0.55)"; ctx.lineWidth = 0.8; ctx.stroke();
      }

      // limbe
      ctx.beginPath(); ctx.arc(w / 2, h / 2, R, 0, Math.PI * 2);
      ctx.strokeStyle = P.accent + "55"; ctx.lineWidth = 1.2; ctx.stroke();

      st.dashT = (st.dashT + 0.35) % 1000;

      // ─── overlays : arcs ───
      if (P.overlays.nvidia) {
        const from = byIdRef.current["nvidia"];
        window.NVIDIA_LINKS.forEach((l) => {
          const to = byIdRef.current[l.to];
          if (to) drawArc([from.lon, from.lat], [to.lon, to.lat], "oklch(0.78 0.14 65 / 0.8)", 1.4, [5, 6], -st.dashT);
        });
      }
      if (P.overlays.signal) {
        for (let i = 0; i < window.SIGNAL_IDS.length - 1; i++) {
          const a = byIdRef.current[window.SIGNAL_IDS[i]], b = byIdRef.current[window.SIGNAL_IDS[i + 1]];
          if (a && b) drawArc([a.lon, a.lat], [b.lon, b.lat], P.accent + "cc", 1.8, [8, 7], -st.dashT * 2);
        }
      }

      // ─── points ───
      const f = P.filtres;
      const filtreActif = f.tailles.length || f.profils.length || f.couches.length || f.q;
      const match = (s) =>
        (!f.tailles.length || f.tailles.includes(s.taille)) &&
        (!f.profils.length || f.profils.includes(s.profil)) &&
        (!f.couches.length || f.couches.includes(s.couche)) &&
        (!f.q || (s.nom + " " + s.ticker).toLowerCase().includes(f.q.toLowerCase()));

      // foyers regroupés tant que le zoom est faible et qu'aucun filtre n'est actif
      const clusterMode = st.k < CLUSTER_K && !filtreActif && !P.overlays.signal && !P.overlays.nvidia;

      st.hits = [];
      const labels = [];
      const screenXY = {}; // soc.id → position écran (pour les badges du tracé signal)
      nodesRef.current.forEach((n) => {
        // en vue regroupée : on masque TOUT point situé dans l'emprise d'un gros foyer
        // (par clé pour les sociétés curées, par proximité géo pour les tickers recherchés)
        // → plus aucun petit point ne dépasse derrière la bulle chiffrée
        if (clusterMode) {
          const inBig = bigClusterKeys.has(n.clusterKey) ||
            (window.GLOBE_CLUSTERS || []).some((c) => d3.geoDistance([n.lon, n.lat], [c.lon, c.lat]) < 0.05);
          if (inBig) return;
        }
        if (d3.geoDistance([n.lon, n.lat], center()) > 1.5208) return;
        const xy = projection([n.lon, n.lat]);
        if (!xy) return;
        screenXY[n.soc.id] = xy;
        const s = n.soc;
        const dim = filtreActif && !match(s);
        const r = NODE_PX[s.taille] * (0.85 + st.k * 0.08);
        const hue = window.LAYER_HUES[s.couche] || 200;
        const isSel = P.selected && P.selected.id === s.id;
        const isHov = st.hover === s.id;
        st.hits.push({ x: xy[0], y: xy[1], r, soc: s });

        ctx.globalAlpha = dim ? 0.13 : 1;
        if (P.glowOn && !dim && (s.taille === "MEGA" || s.taille === "LARGE" || isSel || isHov)) {
          ctx.shadowBlur = isSel || isHov ? 22 : 10;
          ctx.shadowColor = s.profil === "DISRUPTE" ? "oklch(0.6 0.18 25)" : `oklch(0.75 0.14 ${hue})`;
        }
        ctx.beginPath(); ctx.arc(xy[0], xy[1], r, 0, Math.PI * 2);
        ctx.fillStyle = s.profil === "DISRUPTE" ? "oklch(0.42 0.1 25)" : `oklch(${isSel || isHov ? 0.85 : 0.72} 0.13 ${hue})`;
        ctx.fill();
        ctx.shadowBlur = 0;
        if (s.profil === "SPEC") {
          ctx.setLineDash([2.5, 2.5]);
          ctx.beginPath(); ctx.arc(xy[0], xy[1], r + 2.4, 0, Math.PI * 2);
          ctx.strokeStyle = P.accent; ctx.lineWidth = 1; ctx.stroke();
          ctx.setLineDash([]);
        }
        if (s.profil === "DISRUPTE") {
          ctx.beginPath(); ctx.arc(xy[0], xy[1], r + 2.2, 0, Math.PI * 2);
          ctx.strokeStyle = "oklch(0.6 0.17 25 / 0.8)"; ctx.lineWidth = 1; ctx.stroke();
        }
        if (isSel) {
          ctx.beginPath(); ctx.arc(xy[0], xy[1], r + 6, 0, Math.PI * 2);
          ctx.strokeStyle = P.accent; ctx.lineWidth = 1.5; ctx.stroke();
        }
        ctx.globalAlpha = 1;

        // libellés selon zoom sémantique
        const show =
          isSel || isHov ||
          (!dim && (s.taille === "MEGA" ||
            (s.taille === "LARGE" && st.k >= 2.2) ||
            (s.taille === "MID" && st.k >= 4) ||
            (s.taille === "SMALL" && st.k >= 6)));
        if (show) labels.push({ x: xy[0], y: xy[1] - r - 6, s, dim, strong: isSel || isHov });
      });

      // libellés (par-dessus tous les points) — anti-chevauchement glouton :
      // sélection/survol d'abord, puis par taille ; un libellé qui en percute un autre est tu
      const LABEL_PRIO = { MEGA: 1, LARGE: 2, MID: 3, SMALL: 4 };
      labels.sort((a, b) => ((a.strong ? 0 : LABEL_PRIO[a.s.taille]) - (b.strong ? 0 : LABEL_PRIO[b.s.taille])));
      const placed = [];
      labels.forEach((L) => {
        const fs = L.s.taille === "MEGA" ? 12.5 : 11;
        const w = L.s.nom.length * fs * 0.6 + 6;
        const hh = fs + 5;
        const x0 = L.x - w / 2, y0 = L.y - hh;
        if (!L.strong && placed.some((p) => x0 < p.x1 && x0 + w > p.x0 && y0 < p.y1 && y0 + hh > p.y0)) return;
        placed.push({ x0, x1: x0 + w, y0, y1: y0 + hh });
        ctx.globalAlpha = L.dim ? 0.3 : 1;
        ctx.font = (L.strong ? "600 " : "500 ") + fs + "px 'Space Grotesk', sans-serif";
        ctx.textAlign = "center"; ctx.textBaseline = "bottom";
        ctx.lineWidth = 3.5; ctx.strokeStyle = "oklch(0.13 0.04 260 / 0.9)";
        ctx.strokeText(L.s.nom, L.x, L.y);
        ctx.fillStyle = L.strong ? P.accent : "oklch(0.9 0.02 240)";
        ctx.fillText(L.s.nom, L.x, L.y);
        ctx.globalAlpha = 1;
      });

      // bulles des foyers denses (Silicon Valley, Houston, Boston…)
      if (clusterMode) {
        (window.GLOBE_CLUSTERS || []).forEach((c) => {
          if (d3.geoDistance([c.lon, c.lat], center()) > 1.45) return;
          const xy = projection([c.lon, c.lat]);
          if (!xy) return;
          const r = 11 + Math.sqrt(c.count) * 2.6;
          const isHov = st.hover === "cluster:" + c.key;
          if (P.glowOn) { ctx.shadowBlur = isHov ? 24 : 14; ctx.shadowColor = P.accent + "aa"; }
          ctx.beginPath(); ctx.arc(xy[0], xy[1], r, 0, Math.PI * 2);
          ctx.fillStyle = isHov ? "oklch(0.32 0.08 250 / 0.95)" : "oklch(0.26 0.07 252 / 0.9)";
          ctx.fill();
          ctx.shadowBlur = 0;
          ctx.strokeStyle = P.accent + (isHov ? "" : "99"); ctx.lineWidth = isHov ? 1.6 : 1.1;
          ctx.stroke();
          ctx.setLineDash([2, 3]);
          ctx.beginPath(); ctx.arc(xy[0], xy[1], r + 3.5, 0, Math.PI * 2);
          ctx.strokeStyle = P.accent + "44"; ctx.lineWidth = 1; ctx.stroke();
          ctx.setLineDash([]);
          ctx.font = "600 " + (c.count >= 10 ? 12 : 11) + "px 'IBM Plex Mono', monospace";
          ctx.textAlign = "center"; ctx.textBaseline = "middle";
          ctx.fillStyle = "oklch(0.93 0.015 240)";
          ctx.fillText(String(c.count), xy[0], xy[1] + 0.5);
          if (c.name) {
            ctx.font = "500 10px 'IBM Plex Mono', monospace";
            ctx.textBaseline = "top";
            ctx.lineWidth = 3.5; ctx.strokeStyle = "oklch(0.13 0.04 260 / 0.9)";
            ctx.strokeText(c.name.toUpperCase(), xy[0], xy[1] + r + 7);
            ctx.fillStyle = isHov ? P.accent : "oklch(0.72 0.05 240)";
            ctx.fillText(c.name.toUpperCase(), xy[0], xy[1] + r + 7);
          }
          st.hits.push({ x: xy[0], y: xy[1], r, cluster: c });
        });
      }

      // badges numérotés du tracé signal — à la position écran réelle du point
      if (P.overlays.signal) {
        window.SIGNAL_IDS.forEach((id, i) => {
          const xy = screenXY[id];
          if (!xy) return;
          ctx.beginPath(); ctx.arc(xy[0] + 11, xy[1] - 11, 7.5, 0, Math.PI * 2);
          ctx.fillStyle = P.accent; ctx.fill();
          ctx.font = "600 9px 'IBM Plex Mono', monospace";
          ctx.textAlign = "center"; ctx.textBaseline = "middle";
          ctx.fillStyle = "oklch(0.15 0.04 260)";
          ctx.fillText(String(i + 1), xy[0] + 11, xy[1] - 10.5);
        });
      }

      // rotation automatique en repos
      if (P.autoRotate && !st.drag && !st.anim && Date.now() - st.lastInteract > 3500 && !P.selected) {
        st.rot[0] += 0.018;
      }
      if (st.anim) {
        const t = Math.min(1, (Date.now() - st.anim.t0) / st.anim.dur);
        const e = t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
        st.rot = [
          st.anim.from[0] + (st.anim.to[0] - st.anim.from[0]) * e,
          st.anim.from[1] + (st.anim.to[1] - st.anim.from[1]) * e,
        ];
        st.k = st.anim.fromK + (st.anim.toK - st.anim.fromK) * e;
        if (t >= 1) { st.anim = null; saveView(); }
      }
      raf = requestAnimationFrame(render);
    };
    raf = requestAnimationFrame(render);

    // ─── interactions ───
    const hitTest = (mx, my) => {
      let best = null, bd = 1e9;
      for (const h of st.hits) {
        const d = Math.hypot(mx - h.x, my - h.y);
        if (d < Math.max(9, h.r + 5) && d < bd) { bd = d; best = h; }
      }
      return best;
    };
    const pos = (e) => {
      const r = canvas.getBoundingClientRect();
      return [e.clientX - r.left, e.clientY - r.top];
    };
    const down = (e) => {
      st.drag = { x: e.clientX, y: e.clientY, rot: [...st.rot] };
      st.moved = 0; st.anim = null; st.lastInteract = Date.now();
      canvas.setPointerCapture(e.pointerId);
    };
    const move = (e) => {
      st.lastInteract = Date.now();
      if (st.drag) {
        const dx = e.clientX - st.drag.x, dy = e.clientY - st.drag.y;
        st.moved = Math.max(st.moved, Math.abs(dx) + Math.abs(dy));
        const sens = 0.28 / st.k;
        st.rot[0] = st.drag.rot[0] + dx * sens;
        st.rot[1] = Math.max(-85, Math.min(85, st.drag.rot[1] - dy * sens));
        saveView();
        return;
      }
      const [mx, my] = pos(e);
      const hit = hitTest(mx, my);
      st.hover = hit ? (hit.cluster ? "cluster:" + hit.cluster.key : hit.soc.id) : null;
      canvas.style.cursor = hit ? "pointer" : "grab";
      propsRef.current.onHover(hit && hit.soc ? { soc: hit.soc, x: hit.x, y: hit.y } : null);
    };
    const up = (e) => {
      if (st.drag && st.moved < 5) {
        const [mx, my] = pos(e);
        const hit = hitTest(mx, my);
        if (hit && hit.cluster) {
          // clic sur un foyer dense → zoom dessus, les points s'écartent
          st.anim = { t0: Date.now(), dur: 950, from: [...st.rot], to: [-hit.cluster.lon, -hit.cluster.lat], fromK: st.k, toK: Math.max(3.2, st.k) };
          st.lastInteract = Date.now();
        } else if (hit) {
          propsRef.current.onSelect(hit.soc);
        }
      }
      st.drag = null;
    };
    const wheel = (e) => {
      e.preventDefault();
      st.lastInteract = Date.now();
      st.k = Math.max(0.7, Math.min(14, st.k * Math.exp(-e.deltaY * 0.0016)));
      saveView();
    };
    canvas.addEventListener("pointerdown", down);
    canvas.addEventListener("pointermove", move);
    canvas.addEventListener("pointerup", up);
    canvas.addEventListener("pointerleave", () => { st.hover = null; propsRef.current.onHover(null); });
    canvas.addEventListener("wheel", wheel, { passive: false });

    // ─── API exposée ───
    registerApi({
      addNode: (node) => {
        nodesRef.current = nodesRef.current.filter((n) => n.soc.id !== node.soc.id).concat(node);
        byIdRef.current[node.soc.id] = node;
      },
      flyToSoc: (id, k) => {
        const n = byIdRef.current[id];
        if (!n) return;
        st.lastInteract = Date.now();
        st.anim = { t0: Date.now(), dur: 950, from: [...st.rot], to: [-n.lon, -n.lat], fromK: st.k, toK: Math.max(st.k, k || 4.5) };
      },
      flyToRegion: (lon, lat, k) => {
        st.lastInteract = Date.now();
        st.anim = { t0: Date.now(), dur: 950, from: [...st.rot], to: [-lon, -lat], fromK: st.k, toK: k };
      },
      zoomBy: (f) => { st.k = Math.max(0.7, Math.min(14, st.k * f)); st.lastInteract = Date.now(); saveView(); },
      reset: () => {
        st.anim = { t0: Date.now(), dur: 800, from: [...st.rot], to: [95, -28], fromK: st.k, toK: 1 };
        st.lastInteract = Date.now();
      },
    });

    return () => {
      cancelAnimationFrame(raf);
      ro.disconnect();
    };
  }, []);

  return (
    <div ref={wrapRef} style={{ position: "absolute", inset: 0 }}>
      <canvas ref={canvasRef} style={{ display: "block", touchAction: "none" }}></canvas>
    </div>
  );
}

Object.assign(window, { Globe });
