// Partners globe — orthographic projection with typed partner pins and
// animated arcs that route each partner to Phoenix HQ, hopping through the
// nearest VivaMed office hub when the partner is far away.
//
// Uses d3-geo + topojson-client (loaded at runtime). Hubs are read from
// window.VIVAMED_HUBS; partner pins are read from window.PARTNER_LOGOS
// (entries without lat/lon are skipped).

const { useState, useEffect, useRef } = React;

// Fallback hubs in case partners-data hasn't loaded.
const FALLBACK_HUBS = [
  { city:"Phoenix",  lat:33.45,  lon:-112.07, hq:true },
  { city:"New York", lat:40.71,  lon:-74.01 },
  { city:"Basel",    lat:47.56,  lon:7.59 },
  { city:"Bogotá",   lat:4.71,   lon:-74.07 },
  { city:"Riyadh",   lat:24.71,  lon:46.68 },
];

// Map each partner to a waypoint list ending at Phoenix. Routing groups:
//  • LATAM (south of equatorial NA, west) → via Bogotá
//  • Western/Central Europe & N. Africa → via Basel
//  • MENA / East-of-Basel → via Riyadh → Basel
//  • US East Coast → via NYC
//  • Anywhere else in the Americas → direct
function routeForPartner(partner, hubs){
  const byCity = Object.fromEntries(hubs.map(h => [h.city, h]));
  const phx = byCity["Phoenix"];
  const route = [{ lat: partner.lat, lon: partner.lon }];

  const { lat, lon } = partner;
  const isLatam      = lat < 15  && lon < -30 && lon > -90;
  const isEuropeAfr  = lon > -10 && lon < 35  && lat > -35;
  const isMena       = lon >= 35 && lon < 75;
  const isFarEast    = lon >= 75 && lon <= 180;
  const isUsEast     = lat >= 25 && lat <= 50 && lon >= -90 && lon <= -65;

  if (isLatam)         route.push(byCity["Bogotá"]);
  else if (isEuropeAfr) route.push(byCity["Basel"]);
  else if (isMena)     { route.push(byCity["Riyadh"]); route.push(byCity["Basel"]); }
  else if (isFarEast)  { route.push(byCity["Riyadh"]); route.push(byCity["Basel"]); }
  else if (isUsEast)    route.push(byCity["New York"]);

  // Always end at Phoenix unless the partner already is there (within ~50km).
  const last = route[route.length - 1];
  const dx = Math.abs(last.lon - phx.lon), dy = Math.abs(last.lat - phx.lat);
  if (dx > 0.5 || dy > 0.5) route.push(phx);
  return route;
}

// Densely sample a great-circle arc between two [lon,lat] points so canvas
// drawing renders the curve correctly under the orthographic projection.
function sampleGreatCircle(a, b, n = 48){
  if (!window.d3) return [a, b];
  const interp = window.d3.geoInterpolate([a.lon, a.lat], [b.lon, b.lat]);
  const out = [];
  for (let i = 0; i <= n; i++) out.push(interp(i/n));
  return out;
}

function Globe({ size = 720 }){
  const canvasRef = useRef(null);
  const overlayRef = useRef(null);
  const rotRef = useRef([ 80, -20, 0 ]); // start over Atlantic so US + EU both partly visible
  const dragRef = useRef(null);
  const pausedRef = useRef(false);
  const landRef = useRef(null);
  const gratRef = useRef(null);
  const rafRef = useRef(0);
  const startedAtRef = useRef(0);

  const [ready, setReady] = useState(false);
  const [paused, setPaused] = useState(false);
  const [hovered, setHovered] = useState(null);

  const hubs = (window.VIVAMED_HUBS && window.VIVAMED_HUBS.length) ? window.VIVAMED_HUBS : FALLBACK_HUBS;
  const partners = (window.PARTNER_LOGOS || []).filter(p => typeof p.lat === "number" && typeof p.lon === "number");
  const TYPE_COLORS = window.PARTNER_TYPE_COLORS || { biotech:"#3393F0", academic:"#7FBFFF", data:"#FFB55C", investor:"#9DDB7C" };

  // Pre-compute each partner's route (waypoints) and sampled segments.
  const routesRef = useRef(null);
  if (!routesRef.current && window.d3) {
    routesRef.current = partners.map(p => {
      const waypoints = routeForPartner(p, hubs);
      const segments  = [];
      for (let i = 0; i < waypoints.length - 1; i++) {
        segments.push(sampleGreatCircle(waypoints[i], waypoints[i+1], 48));
      }
      return { partner: p, waypoints, segments };
    });
  }

  // Bootstrap d3 + topojson + world atlas.
  useEffect(() => {
    const loadScript = (src) => new Promise((res, rej) => {
      if ([...document.scripts].some(s => s.src === src)) { res(); return; }
      const s = document.createElement("script");
      s.src = src; s.onload = res; s.onerror = rej;
      document.head.appendChild(s);
    });
    (async () => {
      try {
        await loadScript("https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js");
        await loadScript("https://cdn.jsdelivr.net/npm/topojson-client@3/dist/topojson-client.min.js");
        const res = await fetch("https://cdn.jsdelivr.net/npm/world-atlas@2/land-110m.json");
        const topo = await res.json();
        const land = window.topojson.feature(topo, topo.objects.land);
        landRef.current = land;
        gratRef.current = window.d3.geoGraticule10();
        // Routes need d3 — populate now if they weren't ready earlier.
        if (!routesRef.current) {
          routesRef.current = partners.map(p => {
            const waypoints = routeForPartner(p, hubs);
            const segments = [];
            for (let i = 0; i < waypoints.length - 1; i++) {
              segments.push(sampleGreatCircle(waypoints[i], waypoints[i+1], 48));
            }
            return { partner: p, waypoints, segments };
          });
        }
        startedAtRef.current = performance.now();
        setReady(true);
      } catch (e) {
        console.warn("Globe load failed:", e);
        setReady(true);
      }
    })();
  }, []);

  // Main draw loop.
  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas || !window.d3) return;

    const dpr = window.devicePixelRatio || 1;
    canvas.width = size * dpr;
    canvas.height = size * dpr;
    canvas.style.width = size + "px";
    canvas.style.height = size + "px";
    const ctx = canvas.getContext("2d");
    ctx.scale(dpr, dpr);

    const projection = window.d3.geoOrthographic()
      .scale(size / 2 - 20)
      .translate([size/2, size/2])
      .clipAngle(90)
      .rotate(rotRef.current);
    const pathGen = window.d3.geoPath(projection, ctx);

    // Per-partner cycle timing (ms).
    const SEG_MS   = 1200;  // travel per segment
    const HOP_MS   = 220;   // pause at each hub
    const REST_MS  = 1200;  // pause at Phoenix before restarting
    const STAGGER  = 380;   // stagger between partners

    let last = performance.now();

    const frame = (now) => {
      const dt = Math.min(0.1, (now - last) / 1000);
      last = now;

      // Auto-rotate when idle.
      if (!pausedRef.current && !dragRef.current) {
        rotRef.current = [ rotRef.current[0] + dt * 3.6, rotRef.current[1], 0 ];
      }
      projection.rotate(rotRef.current);

      // Clear
      ctx.clearRect(0, 0, size, size);

      // Sphere bg
      const cx = size/2, cy = size/2, R = size/2 - 20;
      const sphereGrad = ctx.createRadialGradient(cx - R*0.35, cy - R*0.4, 10, cx, cy, R);
      sphereGrad.addColorStop(0, "#1f4b73");
      sphereGrad.addColorStop(0.55, "#0d2640");
      sphereGrad.addColorStop(1, "#04111c");
      ctx.fillStyle = sphereGrad;
      ctx.beginPath();
      ctx.arc(cx, cy, R, 0, Math.PI*2);
      ctx.fill();

      // Graticule
      if (gratRef.current) {
        ctx.beginPath();
        pathGen(gratRef.current);
        ctx.strokeStyle = "rgba(51,147,240,0.18)";
        ctx.lineWidth = 0.6;
        ctx.stroke();
      }

      // Land
      if (landRef.current) {
        ctx.beginPath();
        pathGen(landRef.current);
        ctx.fillStyle = "#2C7DD8";
        ctx.globalAlpha = 0.78;
        ctx.fill();
        ctx.globalAlpha = 1;
        ctx.lineWidth = 0.4;
        ctx.strokeStyle = "rgba(255,255,255,0.16)";
        ctx.stroke();
      }

      // --- Arc layer ---
      const routes = routesRef.current || [];
      const tNow = now - startedAtRef.current;

      // geoOrthographic's projection() returns coordinates for BACK-facing
      // points too (it doesn't clip), so arcs/comets on the far hemisphere
      // would draw over the front. Cull by the point's z toward the viewer,
      // matching the manual visibility test used for the pins below.
      const [rotL, rotP] = rotRef.current;
      const frontFacing = (lon, lat) => {
        const φ = lat*Math.PI/180, λ = (lon + rotL)*Math.PI/180, φ0 = -rotP*Math.PI/180;
        return Math.sin(φ0)*Math.sin(φ) + Math.cos(φ0)*Math.cos(φ)*Math.cos(λ) > 0;
      };

      // Draw all static arc segments first (dim).
      ctx.lineWidth = 1.1;
      ctx.lineCap = "round";
      ctx.strokeStyle = "rgba(255,255,255,0.16)";
      routes.forEach(r => {
        r.segments.forEach(seg => {
          ctx.beginPath();
          let started = false;
          seg.forEach(pt => {
            const xy = projection(pt);
            if (!xy || !frontFacing(pt[0], pt[1])) { started = false; return; }
            if (!started) { ctx.moveTo(xy[0], xy[1]); started = true; }
            else ctx.lineTo(xy[0], xy[1]);
          });
          ctx.stroke();
        });
      });

      // Comet heads (one per partner) on currently active segment.
      routes.forEach((r, idx) => {
        if (r.segments.length === 0) return;
        const cycleDuration = r.segments.length * SEG_MS + (r.segments.length - 1) * HOP_MS + REST_MS;
        const localT = ((tNow + idx * STAGGER) % cycleDuration + cycleDuration) % cycleDuration;

        // Determine which segment + progress.
        let t = localT, segIdx = -1, segProgress = 0;
        for (let i = 0; i < r.segments.length; i++) {
          if (t < SEG_MS) { segIdx = i; segProgress = t / SEG_MS; break; }
          t -= SEG_MS;
          if (t < HOP_MS) { segIdx = i; segProgress = 1; break; }  // paused at hub i+1
          t -= HOP_MS;
        }
        if (segIdx === -1) return; // resting at destination

        // Sample comet position via geoInterpolate.
        const seg = r.segments[segIdx];
        const a = seg[0];
        const b = seg[seg.length - 1];
        const interp = window.d3.geoInterpolate(a, b);
        const pt = interp(Math.max(0, Math.min(1, segProgress)));
        const xy = projection(pt);
        if (!xy || !frontFacing(pt[0], pt[1])) return;  // comet on the far side

        // Comet color tracks partner type.
        const color = TYPE_COLORS[r.partner.type] || "#3393F0";

        // Trail (short fading tail).
        const trailSteps = 6;
        for (let k = 1; k <= trailSteps; k++) {
          const tt = Math.max(0, segProgress - k * 0.04);
          const ptT = interp(tt);
          const xyT = projection(ptT);
          if (!xyT || !frontFacing(ptT[0], ptT[1])) continue;
          ctx.beginPath();
          ctx.arc(xyT[0], xyT[1], 1.6, 0, Math.PI*2);
          ctx.fillStyle = color;
          ctx.globalAlpha = (1 - k/trailSteps) * 0.35;
          ctx.fill();
        }
        ctx.globalAlpha = 1;

        // Glow halo
        const halo = ctx.createRadialGradient(xy[0], xy[1], 0, xy[0], xy[1], 14);
        halo.addColorStop(0, color);
        halo.addColorStop(1, "rgba(0,0,0,0)");
        ctx.fillStyle = halo;
        ctx.globalAlpha = 0.55;
        ctx.beginPath();
        ctx.arc(xy[0], xy[1], 14, 0, Math.PI*2);
        ctx.fill();
        ctx.globalAlpha = 1;

        // Head
        ctx.beginPath();
        ctx.arc(xy[0], xy[1], 2.6, 0, Math.PI*2);
        ctx.fillStyle = "#fff";
        ctx.fill();
      });

      // Gloss + atmosphere on top of everything globe-surface but BELOW pins.
      const gloss = ctx.createRadialGradient(cx - R*0.4, cy - R*0.5, 0, cx - R*0.2, cy - R*0.2, R*0.7);
      gloss.addColorStop(0, "rgba(255,255,255,0.12)");
      gloss.addColorStop(1, "rgba(255,255,255,0)");
      ctx.fillStyle = gloss;
      ctx.beginPath();
      ctx.arc(cx, cy, R, 0, Math.PI*2);
      ctx.fill();

      const atmo = ctx.createRadialGradient(cx, cy, R, cx, cy, R + 22);
      atmo.addColorStop(0, "rgba(51,147,240,0)");
      atmo.addColorStop(0.5, "rgba(51,147,240,0.22)");
      atmo.addColorStop(1, "rgba(51,147,240,0)");
      ctx.fillStyle = atmo;
      ctx.beginPath();
      ctx.arc(cx, cy, R + 22, 0, Math.PI*2);
      ctx.arc(cx, cy, R, 0, Math.PI*2, true);
      ctx.fill();

      rafRef.current = requestAnimationFrame(frame);
    };
    rafRef.current = requestAnimationFrame(frame);
    return () => cancelAnimationFrame(rafRef.current);
  }, [ready, size]);

  // Project a [lon, lat] using current rotation, with visibility flag.
  const projectPin = (lon, lat) => {
    if (!window.d3) return { x: 0, y: 0, visible: false };
    const proj = window.d3.geoOrthographic()
      .scale(size/2 - 20)
      .translate([size/2, size/2])
      .clipAngle(90)
      .rotate(rotRef.current);
    const p = proj([lon, lat]);
    if (!p || Number.isNaN(p[0])) return { x: 0, y: 0, visible: false };
    const [lambda, phi] = rotRef.current;
    const φ = lat * Math.PI/180, λ = (lon + lambda) * Math.PI/180;
    const φ0 = -phi * Math.PI/180;
    const z = Math.sin(φ0)*Math.sin(φ) + Math.cos(φ0)*Math.cos(φ)*Math.cos(λ);
    return { x: p[0], y: p[1], visible: z > 0 };
  };

  // Drag rotation
  const onPointerDown = (e) => {
    dragRef.current = { x: e.clientX, y: e.clientY, rot: [...rotRef.current] };
    e.currentTarget.setPointerCapture(e.pointerId);
  };
  const onPointerMove = (e) => {
    if (!dragRef.current) return;
    const dx = e.clientX - dragRef.current.x;
    const dy = e.clientY - dragRef.current.y;
    const [l0, p0] = dragRef.current.rot;
    rotRef.current = [ l0 + dx * 0.4, Math.max(-80, Math.min(80, p0 - dy * 0.3)), 0 ];
  };
  const onPointerUp = () => { dragRef.current = null; };

  // Force overlay re-render at ~30fps so pin positions track auto-rotation.
  const [, tick] = useState(0);
  useEffect(() => {
    const id = setInterval(() => tick(n => n + 1), 1000 / 30);
    return () => clearInterval(id);
  }, []);

  return (
    <div className="partners-globe-wrap">
      <div className="partners-globe-stage" style={{width:size, height:size}}>
        <canvas
          ref={canvasRef}
          onPointerDown={onPointerDown}
          onPointerMove={onPointerMove}
          onPointerUp={onPointerUp}
          onPointerLeave={onPointerUp}
          style={{cursor: dragRef.current ? "grabbing" : "grab", touchAction:"none"}}
        />
        <svg ref={overlayRef} className="partners-globe-pins" width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
          {/* Partner pins (small, typed) */}
          {partners.map((p, i) => {
            const proj = projectPin(p.lon, p.lat);
            const color = TYPE_COLORS[p.type] || "#3393F0";
            return (
              <g key={`p-${i}`}
                 transform={`translate(${proj.x},${proj.y})`}
                 style={{opacity: proj.visible ? 1 : 0, pointerEvents: proj.visible ? "auto" : "none", transition:"opacity .2s"}}
                 onMouseEnter={() => setHovered({ kind:"partner", data:p, x:proj.x, y:proj.y })}
                 onMouseLeave={() => setHovered(null)}>
                <circle r={3.6} fill="#0F1F2E" opacity="0.6"/>
                <circle r={2.8} fill={color} stroke="#fff" strokeWidth="1"/>
              </g>
            );
          })}
          {/* Hub pins (larger, ringed) */}
          {hubs.map((h, i) => {
            const proj = projectPin(h.lon, h.lat);
            const isHQ = !!h.hq;
            const r = isHQ ? 7 : 5.5;
            return (
              <g key={`h-${i}`}
                 transform={`translate(${proj.x},${proj.y})`}
                 style={{opacity: proj.visible ? 1 : 0, pointerEvents: proj.visible ? "auto" : "none", transition:"opacity .2s"}}
                 onMouseEnter={() => setHovered({ kind:"hub", data:h, x:proj.x, y:proj.y })}
                 onMouseLeave={() => setHovered(null)}>
                {isHQ && proj.visible && (
                  <circle r={r+5} fill="#fff" opacity="0.35">
                    <animate attributeName="r" values={`${r+5};${r+14};${r+5}`} dur="2.4s" repeatCount="indefinite"/>
                    <animate attributeName="opacity" values="0.45;0;0.45" dur="2.4s" repeatCount="indefinite"/>
                  </circle>
                )}
                <circle r={r+3} fill="#0F1F2E" opacity="0.6"/>
                <circle r={r} fill="#fff" stroke="#3393F0" strokeWidth={isHQ?2.4:1.8}/>
                {isHQ && <circle r={2.5} fill="#3393F0"/>}
              </g>
            );
          })}
        </svg>

        {hovered && (
          <div
            className="partners-globe-tip"
            style={{ left: hovered.x, top: hovered.y - 14 }}
          >
            <div className="ptip-name">{hovered.data.alt || hovered.data.city}</div>
            <div className="ptip-meta">
              {hovered.kind === "hub"
                ? `VivaMed · ${hovered.data.label || hovered.data.city}`
                : (hovered.data.type ? hovered.data.type.replace(/^./, c => c.toUpperCase()) : "Partner")}
            </div>
          </div>
        )}

        <button className="partners-globe-pause" onClick={()=>{ pausedRef.current = !paused; setPaused(!paused); }}>
          {paused ? "▶" : "❚❚"}
        </button>
      </div>

      <div className="partners-globe-legend">
        <div className="pgl-row">
          <span className="pgl-swatch hub"></span>
          <span>VivaMed offices</span>
        </div>
        <div className="pgl-row">
          <span className="pgl-swatch" style={{background:TYPE_COLORS.biotech}}></span>
          <span>Biotech & pharma</span>
        </div>
        <div className="pgl-row">
          <span className="pgl-swatch" style={{background:TYPE_COLORS.academic}}></span>
          <span>Academic & research</span>
        </div>
      </div>
    </div>
  );
}

window.PartnersGlobe = Globe;
// Keep the legacy export name for any consumers we may have missed.
window.OfficesGlobe = Globe;
