/* global React */
/* Interactive global network — 3D globe (canvas) + LIST fallback, side panel.
   The globe reuses the hero backdrop's engine (app.jsx HeroBackdrop): the same
   inverse-Mercator land sampling of world-map.png + japan.png and the same
   orthographic projection / lighting, plus pointer interaction — drag to
   rotate, click a port for the office panel, chip row for direct selection. */
const { useState: useStateNM, useRef: useRefNM, useEffect: useEffectNM } = React;

// Pixel coordinates on the legacy flat basemap (world-map.png, 1198x784).
// Still the source of truth for office positions: the globe derives lat/lon
// from these via the same projection used to sample the land texture, so a
// marker is guaranteed to sit on its country no matter how non-standard the
// basemap's projection is.
const NM_COORDS = {
  gr:    { x: 625, y: 315 },
  cy:    { x: 640, y: 325 },
  nl:    { x: 560, y: 255 },
  'kr-in': { x: 992, y: 305 },
  'kr-se': { x: 988, y: 298 },
  'jp-tk': { x: 1058, y: 302 },
  'jp-os': { x: 1052, y: 312 },
  cn:    { x: 962, y: 308 },
  sg:    { x: 940, y: 475 },
};

// Basemap -> sphere projection params. MUST stay identical to HeroBackdrop in
// app.jsx and to the land sampling below — changing one side desyncs the
// office dots from the continents.
const NM_PROJ = {
  X0: 549.7,          // basemap x pixel where lon = 0
  X_PER_LON: 3.56,    // px per degree of longitude
  Y0: 488.3,          // basemap y pixel where lat = 0
  Y_PER_MERC: -224.3, // px per Mercator unit (negative: y grows southward)
};
const nmPixelToLatLon = (mx, my) => {
  const lon = (mx - NM_PROJ.X0) / NM_PROJ.X_PER_LON;
  const ymerc = (NM_PROJ.Y0 - my) / -NM_PROJ.Y_PER_MERC;
  const lat = (2 * Math.atan(Math.exp(ymerc)) - Math.PI / 2) * 180 / Math.PI;
  return { lat, lon };
};
const NM_LATLON = {};
Object.keys(NM_COORDS).forEach((id) => {
  NM_LATLON[id] = nmPixelToLatLon(NM_COORDS[id].x, NM_COORDS[id].y);
});

// True city coordinates — used ONLY for the on-screen readout. Rendering uses
// the basemap-derived NM_LATLON above (consistent with the sampled land, but
// a couple of degrees off geographically because the basemap projection is
// approximate — not something to show users as a fact).
const NM_REAL_LL = {
  gr:      { lat: 37.94, lon: 23.65 },
  cy:      { lat: 34.68, lon: 33.04 },
  nl:      { lat: 51.92, lon: 4.48 },
  'kr-in': { lat: 37.46, lon: 126.71 },
  'kr-se': { lat: 37.57, lon: 126.98 },
  'jp-tk': { lat: 35.68, lon: 139.69 },
  'jp-os': { lat: 34.69, lon: 135.50 },
  cn:      { lat: 31.23, lon: 121.47 },
  sg:      { lat: 1.35,  lon: 103.82 },
};

// Per-port label placement. The Seoul/Incheon, Tokyo/Osaka and Piraeus/
// Limassol pairs sit a few degrees apart, so their labels fan out instead of
// overprinting. dx/dy in px around the marker; align 'right' puts the text on
// the marker's left side.
const NM_LABEL = {
  gr:      { dx: -12, dy: -5, align: 'right' },
  cy:      { dx: 9,   dy: 12 },
  nl:      { dx: 9,   dy: -4 },
  'kr-se': { dx: -9,  dy: -5, align: 'right' },
  'kr-in': { dx: 9,   dy: 11 },
  'jp-tk': { dx: 9,   dy: -3 },
  'jp-os': { dx: -9,  dy: 13, align: 'right' },
  cn:      { dx: -10, dy: -2, align: 'right' },
  sg:      { dx: 9,   dy: 4 },
};

// Land grid — sampled once per page life and shared across remounts
// (MAP <-> LIST toggles, language switches re-use the cache).
let nmLandCache = null;
let nmLandPromise = null;
function nmLoadLand() {
  if (nmLandCache) return Promise.resolve(nmLandCache);
  if (nmLandPromise) return nmLandPromise;
  const loadImg = (src) => new Promise((resolve) => {
    const im = new Image();
    im.onload = () => resolve(im);
    im.onerror = () => resolve(null);
    im.src = src;
  });
  const readPixels = (im) => {
    const w = im.naturalWidth, h = im.naturalHeight;
    const off = document.createElement('canvas');
    off.width = w; off.height = h;
    const oc = off.getContext('2d');
    oc.drawImage(im, 0, 0);
    try { return { w, h, data: oc.getImageData(0, 0, w, h).data }; }
    catch (e) { return null; }
  };
  nmLandPromise = Promise.all([loadImg('/world-map.png'), loadImg('/japan.png')]).then(([wmImg, jpImg]) => {
    if (!wmImg) return null;
    const wm = readPixels(wmImg);
    if (!wm) return null;
    const jp = jpImg ? readPixels(jpImg) : null;

    const { X0, X_PER_LON, Y0, Y_PER_MERC } = NM_PROJ;
    // japan.png overlay placement in basemap coordinates — the basemap PNG is
    // missing Japan; keep in sync with HeroBackdrop in app.jsx.
    const JP_X = 965, JP_Y = 231, JP_W = 150, JP_H = 150;
    const LAT_TOP = 77, LAT_BOT = -60; // sphere coverage of the basemap
    const STEP = 0.7;                  // land texture density

    const grid = [];
    for (let lat = LAT_BOT + 0.5; lat <= LAT_TOP - 0.5; lat += STEP) {
      const ymerc = Math.log(Math.tan(Math.PI / 4 + lat * Math.PI / 360));
      const my = Math.floor(Y0 + Y_PER_MERC * ymerc);
      if (my < 0 || my >= wm.h) continue;
      for (let lon = -180; lon < 180; lon += STEP) {
        const mx = Math.floor(X0 + lon * X_PER_LON);
        if (mx < 0 || mx >= wm.w) continue;
        // Basemap: land pixels are near-white; ocean is light gray.
        let isLand = wm.data[(my * wm.w + mx) * 4] > 233;
        if (!isLand && jp &&
            mx >= JP_X && mx < JP_X + JP_W &&
            my >= JP_Y && my < JP_Y + JP_H) {
          const px = Math.floor((mx - JP_X) / JP_W * jp.w);
          const py = Math.floor((my - JP_Y) / JP_H * jp.h);
          const a = jp.data[(py * jp.w + px) * 4 + 3];
          if (a > 60) isLand = true;
        }
        if (isLand) grid.push([lat, lon]);
      }
    }
    nmLandCache = grid;
    return grid;
  });
  return nmLandPromise;
}

function NetworkGlobe({ locations, activeId, onSelect, hint, canvasLabel }) {
  const canvasRef = useRefNM(null);
  const engineRef = useRefNM(null);
  const propsRef = useRefNM({});
  const [hoverLoc, setHoverLoc] = useStateNM(null);
  propsRef.current = { locations, activeId, onSelect, setHoverLoc };

  const activeLoc = locations.find((l) => l.id === activeId) || null;
  const readout = activeLoc || hoverLoc;
  const fmtLL = (l) => {
    const r = NM_REAL_LL[l.id] || l;
    return Math.abs(r.lat).toFixed(2) + '°' + (r.lat >= 0 ? 'N' : 'S') + ' ' +
      Math.abs(r.lon).toFixed(2) + '°' + (r.lon >= 0 ? 'E' : 'W');
  };

  useEffectNM(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext('2d');
    const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
    const D2R = Math.PI / 180;
    let W = 0, H = 0, DPR = window.devicePixelRatio || 1;

    // View state — lon/lat at the globe's screen center.
    const view = { lon: 23.6, lat: 24 };
    const vel = { lon: 0, lat: 0 };
    let drag = null;
    let tween = null;
    let hoveredId = null;
    let parked = false, running = false, rafPending = false, rafId = 0;
    let lastInteraction = -1e9; // auto-sweep active from the first frame
    let land = nmLandCache;
    let stars = [];
    const t0 = performance.now();
    let prevNow = t0;

    // Idle pendulum — sweeps the view center between Europe and East Asia
    // (Piraeus stays on or near the visible face for the whole sweep).
    const P_MID = 61, P_AMP = 51, P_W = 0.12, IDLE_MS = 4500;
    const ph0 = Math.acos(Math.min(1, Math.max(-1, (P_MID - view.lon) / P_AMP)));

    const geom = () => ({
      cx: W * 0.5,
      cy: H * 0.54,
      R: Math.max(10, Math.min(W * 0.44, H * 0.46)),
    });

    const wrapLon = (v) => { v %= 360; if (v > 180) v -= 360; if (v < -180) v += 360; return v; };
    const clampLat = (v) => Math.max(-55, Math.min(62, v));
    const shortest = (a, b) => ((b - a + 540) % 360) - 180;
    const easeInOut = (p) => (p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2);

    // Orthographic projection with two-axis rotation (view lon + tilt).
    const project = (lat, lon, cx, cy, R) => {
      const φ = lat * D2R, λ = (lon - view.lon) * D2R;
      const cφ0 = Math.cos(view.lat * D2R), sφ0 = Math.sin(view.lat * D2R);
      const cφ = Math.cos(φ), sφ = Math.sin(φ);
      const x = cφ * Math.sin(λ);
      const y = cφ0 * sφ - sφ0 * cφ * Math.cos(λ);
      const z = sφ0 * sφ + cφ0 * cφ * Math.cos(λ);
      return { x: cx + x * R, y: cy - y * R, z, visible: z > 0 };
    };
    // Same, lifted off the surface by `lift` (fraction of R) — flight paths.
    const projectLifted = (lat, lon, cx, cy, R, lift) => {
      const φ = lat * D2R, λ = (lon - view.lon) * D2R;
      const cφ0 = Math.cos(view.lat * D2R), sφ0 = Math.sin(view.lat * D2R);
      const cφ = Math.cos(φ), sφ = Math.sin(φ);
      const x = cφ * Math.sin(λ);
      const y = cφ0 * sφ - sφ0 * cφ * Math.cos(λ);
      const z = sφ0 * sφ + cφ0 * cφ * Math.cos(λ);
      const m = 1 + lift;
      return { x: cx + x * R * m, y: cy - y * R * m, z, visible: z > 0 || (x * x + y * y) * m * m > 1 };
    };

    // Great-circle arc between two lat/lon points, sampled.
    const arcPoints = (lat1, lon1, lat2, lon2, n) => {
      const pts = [];
      const φ1 = lat1 * D2R, λ1 = lon1 * D2R;
      const φ2 = lat2 * D2R, λ2 = lon2 * D2R;
      const d = 2 * Math.asin(Math.sqrt(
        Math.sin((φ2 - φ1) / 2) ** 2 +
        Math.cos(φ1) * Math.cos(φ2) * Math.sin((λ2 - λ1) / 2) ** 2
      ));
      if (d === 0) return [[lat1, lon1]];
      for (let i = 0; i <= n; i++) {
        const f = i / n;
        const A = Math.sin((1 - f) * d) / Math.sin(d);
        const B = Math.sin(f * d) / Math.sin(d);
        const x = A * Math.cos(φ1) * Math.cos(λ1) + B * Math.cos(φ2) * Math.cos(λ2);
        const y = A * Math.cos(φ1) * Math.sin(λ1) + B * Math.cos(φ2) * Math.sin(λ2);
        const z = A * Math.sin(φ1) + B * Math.sin(φ2);
        pts.push([Math.atan2(z, Math.sqrt(x * x + y * y)) / D2R, Math.atan2(y, x) / D2R]);
      }
      return pts;
    };

    // Trade-route arcs HQ -> each office (geometry is language-independent).
    const CYCLE = 7;
    const hqLoc = propsRef.current.locations.find((l) => l.tier === 'hq');
    const arcs = hqLoc ? propsRef.current.locations
      .filter((l) => l !== hqLoc && l.lat != null)
      .map((l, i) => {
        const φ1 = hqLoc.lat * D2R, λ1 = hqLoc.lon * D2R;
        const φ2 = l.lat * D2R, λ2 = l.lon * D2R;
        const d = 2 * Math.asin(Math.sqrt(
          Math.sin((φ2 - φ1) / 2) ** 2 +
          Math.cos(φ1) * Math.cos(φ2) * Math.sin((λ2 - λ1) / 2) ** 2
        ));
        return {
          pts: arcPoints(hqLoc.lat, hqLoc.lon, l.lat, l.lon, 60),
          delay: i * 0.9,
          targetId: l.id,
          altPeak: 0.035 + Math.min(0.075, d * 0.045),
        };
      }) : [];

    // Directional light + land colour LUT — same values as the hero globe.
    const LIGHT = [-0.45, -0.52, 0.727];
    const LAND_LUT = [];
    for (let i = 0; i < 32; i++) {
      const s = i / 31;
      LAND_LUT.push(`rgba(${Math.round(88 + 97 * s)},${Math.round(150 + 75 * s)},${Math.round(205 + 45 * s)},${(0.09 + 0.52 * s).toFixed(3)})`);
    }

    const seedStars = () => {
      const n = Math.round((W * H) / 10000);
      stars = Array.from({ length: n }, () => ({
        fx: Math.random(), fy: Math.random(),
        r: 0.5 + Math.random() * 1.3,
        a: 0.16 + Math.random() * 0.4,
        tw: 0.4 + Math.random() * 1.3,
        ph: Math.random() * Math.PI * 2,
      }));
    };

    const hitTest = (mx, my) => {
      const { cx, cy, R } = geom();
      let best = null, bestZ = -1;
      for (const l of propsRef.current.locations) {
        if (l.lat == null) continue;
        const p = project(l.lat, l.lon, cx, cy, R);
        if (!p.visible) continue;
        const dx = mx - p.x, dy = my - p.y;
        if (dx * dx + dy * dy <= 18 * 18 && p.z > bestZ) { best = l; bestZ = p.z; }
      }
      return best;
    };

    const draw = (t) => {
      const { cx, cy, R } = geom();
      const activeNow = propsRef.current.activeId;
      const hx = cx - R * 0.32, hy = cy - R * 0.36; // light highlight point

      ctx.clearRect(0, 0, W, H);

      // Background — hero palette so the stage reads as one family with the home hero.
      const bg = ctx.createLinearGradient(0, 0, 0, H);
      bg.addColorStop(0, '#061634');
      bg.addColorStop(0.5, '#0A1F3D');
      bg.addColorStop(1, '#163b66');
      ctx.fillStyle = bg;
      ctx.fillRect(0, 0, W, H);

      // Star field
      for (let i = 0; i < stars.length; i++) {
        const st = stars[i];
        const al = st.a * (0.55 + 0.45 * Math.sin(t * st.tw + st.ph));
        if (al < 0.02) continue;
        ctx.fillStyle = `rgba(205,228,250,${al})`;
        ctx.fillRect(st.fx * W, st.fy * H, st.r, st.r);
      }

      // Atmosphere glow
      const atm = ctx.createRadialGradient(cx - R * 0.06, cy - R * 0.07, R * 0.95, cx, cy, R * 1.3);
      atm.addColorStop(0, 'rgba(127,179,213,0.28)');
      atm.addColorStop(0.5, 'rgba(127,179,213,0.09)');
      atm.addColorStop(1, 'rgba(127,179,213,0)');
      ctx.fillStyle = atm;
      ctx.beginPath();
      ctx.arc(cx, cy, R * 1.3, 0, Math.PI * 2);
      ctx.fill();

      // Sphere — dark base, highlight aligned with LIGHT
      const sphere = ctx.createRadialGradient(hx, hy, 0, cx, cy, R);
      sphere.addColorStop(0, '#16365e');
      sphere.addColorStop(0.65, '#0A1F3D');
      sphere.addColorStop(1, '#03101f');
      ctx.fillStyle = sphere;
      ctx.beginPath();
      ctx.arc(cx, cy, R, 0, Math.PI * 2);
      ctx.fill();

      // Graticule (every 20°)
      ctx.strokeStyle = 'rgba(127,179,213,0.08)';
      ctx.lineWidth = 0.6;
      for (let lat = -80; lat <= 80; lat += 20) {
        ctx.beginPath();
        let started = false;
        for (let lon = -180; lon <= 180; lon += 4) {
          const p = project(lat, lon, cx, cy, R);
          if (p.visible) {
            if (!started) { ctx.moveTo(p.x, p.y); started = true; }
            else ctx.lineTo(p.x, p.y);
          } else { started = false; }
        }
        ctx.stroke();
      }
      for (let lon = -180; lon < 180; lon += 20) {
        ctx.beginPath();
        let started = false;
        for (let lat = -80; lat <= 80; lat += 4) {
          const p = project(lat, lon, cx, cy, R);
          if (p.visible) {
            if (!started) { ctx.moveTo(p.x, p.y); started = true; }
            else ctx.lineTo(p.x, p.y);
          } else { started = false; }
        }
        ctx.stroke();
      }

      // Land texture, lit by LIGHT (see HeroBackdrop for the derivation)
      if (land) {
        const dotSize = Math.max(1.2, R * 0.006);
        let lastIdx = -1;
        for (let i = 0; i < land.length; i++) {
          const p = project(land[i][0], land[i][1], cx, cy, R);
          if (!p.visible) continue;
          const nx = (p.x - cx) / R, ny = (p.y - cy) / R;
          const diff = nx * LIGHT[0] + ny * LIGHT[1] + p.z * LIGHT[2];
          let s = 0.10 + 0.58 * (diff > 0 ? diff : 0) + 0.28 * p.z;
          if (s > 1) s = 1;
          const idx = (s * 31) | 0;
          if (idx !== lastIdx) { ctx.fillStyle = LAND_LUT[idx]; lastIdx = idx; }
          ctx.fillRect(p.x - dotSize / 2, p.y - dotSize / 2, dotSize, dotSize);
        }
      }

      // Night-side shading — arcs and ports draw after this, staying emissive.
      const shade = ctx.createRadialGradient(hx, hy, R * 0.5, hx, hy, R * 1.62);
      shade.addColorStop(0, 'rgba(2,8,20,0)');
      shade.addColorStop(0.55, 'rgba(2,8,20,0)');
      shade.addColorStop(1, 'rgba(2,8,20,0.34)');
      ctx.fillStyle = shade;
      ctx.beginPath();
      ctx.arc(cx, cy, R, 0, Math.PI * 2);
      ctx.fill();

      // Trade-route arcs — static dashed route + comet + arrival ripple.
      const animate = !mq.matches;
      arcs.forEach((arc) => {
        const n = arc.pts.length - 1;
        const lifted = arc.pts.map((pt, k) =>
          projectLifted(pt[0], pt[1], cx, cy, R, Math.sin(k / n * Math.PI) * arc.altPeak));

        ctx.strokeStyle = 'rgba(127,179,213,0.20)';
        ctx.lineWidth = 0.8;
        ctx.setLineDash([2, 5]);
        ctx.beginPath();
        let started = false;
        for (const p of lifted) {
          if (p.visible) {
            if (!started) { ctx.moveTo(p.x, p.y); started = true; }
            else ctx.lineTo(p.x, p.y);
          } else { started = false; }
        }
        ctx.stroke();
        ctx.setLineDash([]);
        if (!animate) return;

        const u = (t - arc.delay) % CYCLE;
        if (u < 0) return;
        const phase = u / CYCLE;
        const headIdx = Math.min(n, Math.floor(phase * n));
        const tailIdx = Math.max(0, Math.floor((phase - 0.22) * n));
        const head = lifted[headIdx];
        if (headIdx > tailIdx && head.visible) {
          const grad = ctx.createLinearGradient(lifted[tailIdx].x, lifted[tailIdx].y, head.x, head.y);
          grad.addColorStop(0, 'rgba(127,179,213,0)');
          grad.addColorStop(1, 'rgba(214,238,252,0.9)');
          ctx.strokeStyle = grad;
          ctx.lineWidth = 1.6;
          ctx.lineCap = 'round';
          ctx.beginPath();
          let ss = false;
          for (let k = tailIdx; k <= headIdx; k++) {
            const p = lifted[k];
            if (p.visible) {
              if (!ss) { ctx.moveTo(p.x, p.y); ss = true; }
              else ctx.lineTo(p.x, p.y);
            } else { ss = false; }
          }
          ctx.stroke();

          ctx.fillStyle = '#dff1fc';
          ctx.shadowColor = '#7FB3D5';
          ctx.shadowBlur = 10;
          ctx.beginPath();
          ctx.arc(head.x, head.y, 2.5, 0, Math.PI * 2);
          ctx.fill();
          ctx.shadowBlur = 0;
        }

        // Arrival ripple, skipped during the first cycle.
        if (t - arc.delay >= CYCLE && u < 0.9) {
          const tl = propsRef.current.locations.find((l) => l.id === arc.targetId);
          if (tl && tl.lat != null) {
            const p = project(tl.lat, tl.lon, cx, cy, R);
            if (p.visible) {
              const k = u / 0.9;
              ctx.strokeStyle = `rgba(170,215,245,${0.45 * (1 - k)})`;
              ctx.lineWidth = 1.1;
              ctx.beginPath();
              ctx.arc(p.x, p.y, 3 + k * 15, 0, Math.PI * 2);
              ctx.stroke();
            }
          }
        }
      });

      // Office ports — sonar rings + core + label; hover/active highlights.
      const visible = [];
      for (const l of propsRef.current.locations) {
        if (l.lat == null) continue;
        const p = project(l.lat, l.lon, cx, cy, R);
        if (p.visible) visible.push({ l, p });
      }
      visible.sort((a, b) => a.p.z - b.p.z);
      visible.forEach(({ l, p }) => {
        const isHQ = l.tier === 'hq';
        const isActive = activeNow === l.id;
        const isHover = hoveredId === l.id;
        const limb = 0.35 + p.z * 0.65;

        for (let ring = 0; ring < 2; ring++) {
          // Phase offset per office (lon-based) so the pings don't fire in sync
          const rp = animate ? (t * 0.42 + ((l.lon || 0) * 0.013 + ring * 0.5)) % 1 : 0.45;
          const ra = (1 - rp) * (isHQ || isActive ? 0.5 : 0.3) * limb;
          if (ra < 0.02) continue;
          ctx.strokeStyle = isActive
            ? `rgba(235,160,120,${ra})`
            : `rgba(159,208,238,${ra})`;
          ctx.lineWidth = isHQ ? 1.2 : 1;
          ctx.beginPath();
          ctx.arc(p.x, p.y, 2.5 + rp * (isHQ || isActive ? 17 : 11), 0, Math.PI * 2);
          ctx.stroke();
        }

        // Core
        const coreR = (isHQ ? 3.4 : 2.6) + (isHover || isActive ? 0.6 : 0);
        ctx.fillStyle = isHQ ? '#E0784A' : '#cfe6f5';
        ctx.shadowColor = isHQ ? '#E0784A' : '#7FB3D5';
        ctx.shadowBlur = isHQ ? 10 : 6;
        ctx.beginPath();
        ctx.arc(p.x, p.y, coreR, 0, Math.PI * 2);
        ctx.fill();
        ctx.shadowBlur = 0;
        if (isHQ) {
          ctx.fillStyle = '#fff';
          ctx.beginPath();
          ctx.arc(p.x, p.y, 1.3, 0, Math.PI * 2);
          ctx.fill();
        }

        // Hover / active rings
        if (isHover && !isActive) {
          ctx.strokeStyle = 'rgba(255,255,255,0.85)';
          ctx.lineWidth = 1.3;
          ctx.beginPath();
          ctx.arc(p.x, p.y, coreR + 4, 0, Math.PI * 2);
          ctx.stroke();
        }
        if (isActive) {
          ctx.strokeStyle = 'rgba(224,140,100,0.95)';
          ctx.lineWidth = 1.6;
          ctx.beginPath();
          ctx.arc(p.x, p.y, coreR + 5, 0, Math.PI * 2);
          ctx.stroke();
          ctx.strokeStyle = 'rgba(224,140,100,0.30)';
          ctx.lineWidth = 1;
          ctx.beginPath();
          ctx.arc(p.x, p.y, coreR + 9, 0, Math.PI * 2);
          ctx.stroke();
        }

        // Label
        const lab = NM_LABEL[l.id] || { dx: 9, dy: 4 };
        const bright = isHover || isActive;
        ctx.fillStyle = bright
          ? 'rgba(255,255,255,0.98)'
          : `rgba(207,230,245,${(isHQ ? 0.5 : 0.34) + p.z * 0.5})`;
        ctx.font = (isHQ || bright)
          ? '600 10.5px "Geist Mono", "JetBrains Mono", monospace'
          : '10px "Geist Mono", "JetBrains Mono", monospace';
        ctx.textAlign = lab.align === 'right' ? 'right' : 'left';
        ctx.fillText(l.code, p.x + lab.dx, p.y + lab.dy);
        ctx.textAlign = 'left';
      });

      // Globe edge + rim light
      ctx.strokeStyle = 'rgba(127,179,213,0.20)';
      ctx.lineWidth = 1;
      ctx.beginPath();
      ctx.arc(cx, cy, R, 0, Math.PI * 2);
      ctx.stroke();
      const rim = ctx.createLinearGradient(cx - 0.654 * R, cy - 0.756 * R, cx + 0.39 * R, cy + 0.45 * R);
      rim.addColorStop(0, 'rgba(176,218,248,0.55)');
      rim.addColorStop(0.45, 'rgba(176,218,248,0.10)');
      rim.addColorStop(0.75, 'rgba(176,218,248,0)');
      ctx.strokeStyle = rim;
      ctx.lineWidth = 1.4;
      ctx.beginPath();
      ctx.arc(cx, cy, R - 0.5, 0, Math.PI * 2);
      ctx.stroke();
    };

    const step = (now) => {
      const dt = Math.min(0.05, Math.max(0.001, (now - prevNow) / 1000));
      prevNow = now;
      const t = (now - t0) / 1000;

      if (tween) {
        const p = Math.min(1, (now - tween.start) / tween.dur);
        const e = easeInOut(p);
        view.lon = wrapLon(tween.fromLon + tween.dLon * e);
        view.lat = tween.fromLat + (tween.toLat - tween.fromLat) * e;
        if (p >= 1) { tween = null; lastInteraction = now; }
      } else if (!drag) {
        if (vel.lon !== 0 || vel.lat !== 0) {
          // Inertia after a fling
          view.lon = wrapLon(view.lon + vel.lon * dt);
          view.lat = clampLat(view.lat + vel.lat * dt);
          const d = Math.exp(-2.6 * dt);
          vel.lon *= d; vel.lat *= d;
          if (Math.abs(vel.lon) < 0.4 && Math.abs(vel.lat) < 0.4) {
            vel.lon = 0; vel.lat = 0; lastInteraction = now;
          }
        } else if (!hoveredId && !propsRef.current.activeId && now - lastInteraction > IDLE_MS) {
          // Ease back onto the idle pendulum, then follow it
          const target = P_MID - P_AMP * Math.cos(P_W * t + ph0);
          const k = 1 - Math.exp(-1.3 * dt);
          view.lon = wrapLon(view.lon + shortest(view.lon, target) * k);
          view.lat = view.lat + (24 - view.lat) * k;
        }
      }

      draw(t);
      if (!parked && !mq.matches) rafId = requestAnimationFrame(step);
      else running = false;
    };

    // Under reduced motion: no loop — single frames per event. Otherwise the
    // loop runs whenever the stage is on screen.
    const kick = () => {
      if (parked) return;
      if (!mq.matches) {
        if (!running) {
          running = true;
          prevNow = performance.now();
          rafId = requestAnimationFrame(step);
        }
        return;
      }
      if (!rafPending) {
        rafPending = true;
        rafId = requestAnimationFrame((n) => { rafPending = false; prevNow = n; step(n); });
      }
    };

    const resize = () => {
      DPR = window.devicePixelRatio || 1;
      const r = canvas.getBoundingClientRect();
      W = r.width; H = r.height;
      canvas.width = W * DPR; canvas.height = H * DPR;
      ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
      seedStars();
      kick();
    };

    nmLoadLand().then((g) => { land = g; kick(); });

    // --- Pointer interaction ---
    const onDown = (e) => {
      if (e.pointerType === 'mouse' && e.button !== 0) return;
      if (canvas.setPointerCapture) { try { canvas.setPointerCapture(e.pointerId); } catch (err) {} }
      drag = { lastX: e.clientX, lastY: e.clientY, moved: 0, t: e.timeStamp };
      vel.lon = 0; vel.lat = 0; tween = null;
      lastInteraction = performance.now();
      canvas.style.cursor = 'grabbing';
      kick();
    };
    const onMove = (e) => {
      const rect = canvas.getBoundingClientRect();
      if (drag) {
        const dx = e.clientX - drag.lastX, dy = e.clientY - drag.lastY;
        if (dx === 0 && dy === 0) return;
        const dtE = Math.max(1, e.timeStamp - drag.t);
        const { R } = geom();
        const k = 62 / R; // ~finger-tracking speed at the globe's equator
        const dLon = -dx * k, dLat = dy * k;
        view.lon = wrapLon(view.lon + dLon);
        view.lat = clampLat(view.lat + dLat);
        drag.moved += Math.abs(dx) + Math.abs(dy);
        vel.lon = 0.72 * vel.lon + 0.28 * (dLon * 1000 / dtE);
        vel.lat = 0.72 * vel.lat + 0.28 * (dLat * 1000 / dtE);
        drag.lastX = e.clientX; drag.lastY = e.clientY; drag.t = e.timeStamp;
        lastInteraction = performance.now();
        kick();
      } else {
        const hit = hitTest(e.clientX - rect.left, e.clientY - rect.top);
        const id = hit ? hit.id : null;
        if (id !== hoveredId) {
          hoveredId = id;
          canvas.style.cursor = id ? 'pointer' : 'grab';
          propsRef.current.setHoverLoc(hit || null);
          lastInteraction = performance.now();
          kick();
        }
      }
    };
    const onUp = (e) => {
      if (!drag) return;
      const wasClick = drag.moved < 6;
      drag = null;
      canvas.style.cursor = hoveredId ? 'pointer' : 'grab';
      lastInteraction = performance.now();
      if (wasClick) {
        vel.lon = 0; vel.lat = 0;
        const rect = canvas.getBoundingClientRect();
        const hit = hitTest(e.clientX - rect.left, e.clientY - rect.top);
        propsRef.current.onSelect(hit ? hit.id : null);
      }
      kick();
    };
    const onLeave = () => {
      if (hoveredId) {
        hoveredId = null;
        propsRef.current.setHoverLoc(null);
        canvas.style.cursor = 'grab';
        lastInteraction = performance.now();
        kick();
      }
    };
    canvas.addEventListener('pointerdown', onDown);
    canvas.addEventListener('pointermove', onMove);
    canvas.addEventListener('pointerup', onUp);
    canvas.addEventListener('pointercancel', onUp);
    canvas.addEventListener('pointerleave', onLeave);

    // Methods the React side calls when activeId changes.
    engineRef.current = {
      focusOn: (loc) => {
        if (loc.lat == null) return;
        lastInteraction = performance.now();
        const toLat = Math.max(2, Math.min(46, loc.lat));
        if (mq.matches) {
          view.lon = wrapLon(loc.lon); view.lat = toLat;
          tween = null; kick();
          return;
        }
        tween = {
          start: performance.now(), dur: 900,
          fromLon: view.lon, dLon: shortest(view.lon, loc.lon),
          fromLat: view.lat, toLat,
        };
        vel.lon = 0; vel.lat = 0;
        kick();
      },
      released: () => { lastInteraction = performance.now(); kick(); },
    };

    resize();
    window.addEventListener('resize', resize);
    const ro = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(resize) : null;
    if (ro) ro.observe(canvas);
    // Park the loop while the stage is scrolled out of view.
    const io = typeof IntersectionObserver !== 'undefined' ? new IntersectionObserver((entries) => {
      const vis = entries.some((en) => en.isIntersecting);
      if (vis === !parked) return;
      parked = !vis;
      cancelAnimationFrame(rafId);
      running = false; rafPending = false;
      if (!parked) kick();
    }) : null;
    if (io) io.observe(canvas);
    const onMq = () => kick();
    if (mq.addEventListener) mq.addEventListener('change', onMq);

    kick();

    return () => {
      cancelAnimationFrame(rafId);
      window.removeEventListener('resize', resize);
      if (ro) ro.disconnect();
      if (io) io.disconnect();
      if (mq.removeEventListener) mq.removeEventListener('change', onMq);
      canvas.removeEventListener('pointerdown', onDown);
      canvas.removeEventListener('pointermove', onMove);
      canvas.removeEventListener('pointerup', onUp);
      canvas.removeEventListener('pointercancel', onUp);
      canvas.removeEventListener('pointerleave', onLeave);
      engineRef.current = null;
    };
  }, []);

  // Glide to the selected office whenever the selection changes (marker click
  // or chip click); when it clears, arm the idle timer so the sweep resumes.
  useEffectNM(() => {
    const eng = engineRef.current;
    if (!eng) return;
    if (activeId) {
      const loc = propsRef.current.locations.find((l) => l.id === activeId);
      if (loc) eng.focusOn(loc);
    } else {
      eng.released();
    }
  }, [activeId]);

  return (
    <div className="netmap-globe">
      <canvas ref={canvasRef} className="netmap-globe-canvas" role="img" aria-label={canvasLabel}></canvas>
      <div className="netmap-readout mono" aria-hidden="true">
        {readout
          ? `${readout.code} · ${readout.city} — ${fmtLL(readout)}`
          : hint}
      </div>
    </div>
  );
}

function NetworkMapInteractive({ lang }) {
  const t = window.FL_CONTENT[lang].networkPage;
  const [activeId, setActive] = useStateNM(null);
  const [view, setView] = useStateNM('map'); // 'map' | 'list' for mobile fallback
  const locs = t.locations.map((l) => ({ ...l, ...(NM_LATLON[l.id] || {}) }));
  const active = locs.find((l) => l.id === activeId);

  return (
    <section className="netmap">
      <div className="container">
        <div className="netmap-head reveal">
          <div className="section-eyebrow mono">{t.eyebrow}</div>
          <p className="netmap-lede">{t.lede}</p>
        </div>

        <div className="netmap-toolbar reveal">
          <div className="netmap-legend mono">
            <span><i className="dot dot-hq"></i>{t.tiers.hq}</span>
            <span><i className="dot dot-branch"></i>{t.tiers.branch}</span>
            <span><i className="dot dot-agent"></i>{t.tiers.agent}</span>
          </div>
          <div className="netmap-viewtoggle mono">
            <button className={view==='map'?'on':''} onClick={()=>setView('map')}>MAP</button>
            <button className={view==='list'?'on':''} onClick={()=>setView('list')}>LIST</button>
          </div>
        </div>

        {view === 'map' ? (
          <div>
            <div className="netmap-stagewrap reveal in">
              <div className="netmap-stage netmap-stage--globe">
                <NetworkGlobe
                  locations={locs}
                  activeId={activeId}
                  onSelect={setActive}
                  hint={t.hint}
                  canvasLabel={t.canvasLabel}
                />
              </div>

              {active && (
                <aside className="netmap-panel">
                  <div className="netmap-panel-head">
                    <div>
                      <div className="netmap-panel-tier mono">{t.tiers[active.tier]}</div>
                      <h3>{active.city}{active.city ? ', ' : ''}{active.country}</h3>
                      {active.note && <div className="netmap-panel-note">{active.note}</div>}
                    </div>
                    <button className="netmap-panel-close" onClick={() => setActive(null)} aria-label={t.panel.close}>×</button>
                  </div>
                  <div className="netmap-panel-row">
                    <div className="lab mono">{t.panel.services}</div>
                    <ul className="netmap-services">
                      {active.services.map((s, i) => <li key={i}>{s}</li>)}
                    </ul>
                  </div>
                  <div className="netmap-panel-row">
                    <div className="lab mono">{t.panel.contact}</div>
                    <div className="netmap-panel-val">
                      {active.contactName && <div>{active.contactName}</div>}
                      <div><a href={`mailto:${active.contactEmail}`}>{active.contactEmail}</a></div>
                      {active.contactPhone && <div><a href={`tel:${active.contactPhone}`}>{active.contactPhone}</a></div>}
                    </div>
                  </div>
                  <div className="netmap-panel-row">
                    <div className="lab mono">{t.panel.hours}</div>
                    <div className="netmap-panel-val">{active.hours}</div>
                  </div>
                  <div className="netmap-panel-row">
                    <div className="lab mono">{t.panel.languages}</div>
                    <div className="netmap-panel-val">{active.languages.join(' · ')}</div>
                  </div>
                </aside>
              )}
            </div>

            <div className="netmap-chips reveal in" role="group" aria-label={t.eyebrow}>
              {locs.map((l) => (
                <button
                  key={l.id}
                  type="button"
                  className={`netmap-chip${activeId === l.id ? ' on' : ''}`}
                  aria-pressed={activeId === l.id}
                  onClick={() => setActive(activeId === l.id ? null : l.id)}
                >
                  <i className={`dot dot-${l.tier}`}></i>{l.city || l.country}
                </button>
              ))}
            </div>
          </div>
        ) : (
          <div className="netmap-list reveal in">
            {locs.map((l, i) => (
              <article key={i} className={`netmap-listitem netmap-listitem-${l.tier}`}>
                <div className="li-head">
                  <span className={`li-tier li-tier-${l.tier} mono`}>{t.tiers[l.tier]}</span>
                  <span className="li-code mono">{l.code}</span>
                </div>
                <h3>{l.city}{l.city ? ', ' : ''}{l.country}</h3>
                {l.note && <div className="li-note">{l.note}</div>}
                <div className="li-row"><span className="lab mono">{t.panel.services}</span><span>{l.services.join(' · ')}</span></div>
                <div className="li-row"><span className="lab mono">{t.panel.contact}</span><span>{l.contactName && <>{l.contactName}<br/></>}<a href={`mailto:${l.contactEmail}`}>{l.contactEmail}</a>{l.contactPhone && <><br/><a href={`tel:${l.contactPhone}`}>{l.contactPhone}</a></>}</span></div>
                <div className="li-row"><span className="lab mono">{t.panel.hours}</span><span>{l.hours}</span></div>
                <div className="li-row"><span className="lab mono">{t.panel.languages}</span><span>{l.languages.join(' · ')}</span></div>
              </article>
            ))}
          </div>
        )}
      </div>
    </section>
  );
}

window.NetworkMapInteractive = NetworkMapInteractive;
