/* =============================================================
   world-map.jsx — interactive world choropleth (d3-geo + topojson)
   ============================================================= */
const WORLD_URL = "https://cdn.jsdelivr.net/npm/world-atlas@2.0.2/countries-110m.json";

function useWorldTopology() {
  const [topo, setTopo] = React.useState(null);
  const [err, setErr] = React.useState(null);
  React.useEffect(() => {
    let alive = true;
    fetch(WORLD_URL)
      .then((r) => { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); })
      .then((j) => { if (alive) setTopo(j); })
      .catch((e) => { if (alive) setErr(e.message || String(e)); });
    return () => { alive = false; };
  }, []);
  return { topo, err };
}

function WorldMap({ countryData, metric = "links", onCountryClick, selectedCountry, height = 480 }) {
  const { topo, err } = useWorldTopology();
  const ref = React.useRef(null);
  const tooltipRef = React.useRef(null);
  const [hover, setHover] = React.useState(null);
  const metricTheme = React.useMemo(() => {
    const themes = {
      links: {
        label: "Canonical links by country",
        palette: ["#EAF4F0", "#BFE2D1", "#7CC7AC", "#3FA387", "#1F7568", "#0F4B46"],
        marker: "#13A389",
      },
      atRisk: {
        label: "Project at-risk links",
        palette: ["#FFF2D8", "#F8C77C", "#EE9954", "#DD6F40", "#C9472E", "#8F241B"],
        marker: "#D64A2F",
      },
      activated: {
        label: "Project activated links",
        palette: ["#E9F2FF", "#BCD7FA", "#7FB3EC", "#3F86CE", "#255FA4", "#173D73"],
        marker: "#2F76C2",
      },
      lifecycle: {
        label: "Lifecycle % complete (avg)",
        palette: ["#FFF3D0", "#FFE08A", "#FFC34A", "#9AD37B", "#5FB05C", "#236F3C"],
        marker: "#5FB05C",
      },
      readiness: {
        label: "New Underlay readiness by country",
        palette: ["#FFF4E4", "#FFE0A6", "#BFE8CF", "#82D0B6", "#48A88E", "#1F7568"],
        marker: "#2E9F88",
      },
      phase: {
        label: "Dominant lifecycle phase",
        palette: ["#EFEFEF", "#D5DEE6", "#BCC9D2", "#8CABBE", "#3F86CE", "#236F3C"],
        marker: "#3F86CE",
      },
    };
    return themes[metric] || themes.links;
  }, [metric]);

  // G12: map metric -> value extractor on a country row. Uses the canonical phase
  // order from PRData so the Map's colour gradient stays in sync with the lifecycle
  // model in data.js. "blocked" stays terminal (mapped to 0) so it does not appear
  // ahead of "complete".
  function valueForMetric(c) {
    if (!c) return 0;
    if (metric === "lifecycle") return c.averageLifecyclePercent || 0;
    if (metric === "readiness") return c.readinessPercent || 0;
    if (metric === "phase") {
      const order = (PRData && PRData._PHASE_ORDER) || ["no-data","underlay-not-started","underlay-ordering","underlay-delivery","underlay-activated","pre-change-readiness","change-management","link-migration","overlay-not-ready","overlay-ready","overlay-scheduled","overlay-migrated","post-migration","isp-termination","complete"];
      if (c.dominantLifecyclePhase === "blocked") return 0;
      const idx = c.dominantLifecyclePhase ? order.indexOf(c.dominantLifecyclePhase) : -1;
      return idx < 0 ? 0 : Math.round((idx / (order.length - 1)) * 100);
    }
    return c[metric] || 0;
  }

  // Group country data by ISO numeric code (handles UK aliases etc.)
  const byIso = React.useMemo(() => {
    const m = new Map();
    for (const c of countryData) {
      const iso = window.COUNTRY_ISO.toISO(c.key);
      if (!iso) continue;
      if (!m.has(iso)) m.set(iso, { iso, names: new Set(), value: 0, atRisk: 0, blocked: 0, sites: new Set(), activated: 0, total: 0, migrated: 0, hwDelivered: 0, lifecyclePercent: 0, dominantPhase: "no-data", dominantPhaseLabel: "No project data", sitesComplete: 0, readinessPercent: 0, readinessBlocked: 0, readinessTrackedSites: 0, sitesReadyForMigration: 0 });
      const e = m.get(iso);
      e.names.add(c.key);
      // For lifecycle/phase metrics use the average / mapped index. For numeric metrics, sum.
      if (metric === "lifecycle" || metric === "phase" || metric === "readiness") {
        e.value = Math.max(e.value, valueForMetric(c));
      } else {
        e.value += (c[metric] || 0);
      }
      e.atRisk += (c.atRisk || 0);
      e.blocked += (c.blocked || 0);
      e.activated += (c.activated || 0);
      e.total += (c.total || 0);
      // T022 — full effective-model dimensions (overlay migration + HW delivery)
      // were aggregated by countryAggregate but silently dropped during ISO
      // grouping. Roll them up here so the tooltip and any downstream consumer
      // can see workbook-truthful totals that respond to PMStore overrides.
      e.migrated += (c.migrated || 0);
      e.hwDelivered += (c.hwDelivered || 0);
      e.lifecyclePercent = Math.max(e.lifecyclePercent, c.averageLifecyclePercent || 0);
      if (c.dominantLifecyclePhase) e.dominantPhase = c.dominantLifecyclePhase;
      if (c.dominantLifecyclePhaseLabel) e.dominantPhaseLabel = c.dominantLifecyclePhaseLabel;
      e.sitesComplete += (c.sitesComplete || 0);
      e.readinessPercent = Math.max(e.readinessPercent, c.readinessPercent || 0);
      e.readinessBlocked += (c.blockedReadinessCount || 0);
      e.readinessTrackedSites += (c.readinessTrackedSites || 0);
      e.sitesReadyForMigration += (c.sitesReadyForMigration || 0);
      if (c.sites && c.sites.forEach) c.sites.forEach((s) => e.sites.add(s));
    }
    return m;
  }, [countryData, metric]);

  const maxVal = React.useMemo(() => {
    let mx = 0;
    byIso.forEach((v) => { if (v.value > mx) mx = v.value; });
    return mx || 1;
  }, [byIso]);

  const projection = React.useMemo(() => {
    if (!window.d3) return null;
    // naturalEarth1 fits all continents (incl. Australia/NZ) inside 960×540 at scale ~155.
    return d3.geoNaturalEarth1().scale(155).translate([480, 270]);
  }, []);
  const pathGen = React.useMemo(() => projection ? d3.geoPath(projection) : null, [projection]);

  const countries = React.useMemo(() => {
    if (!topo || !window.topojson) return [];
    return topojson.feature(topo, topo.objects.countries).features;
  }, [topo]);

  const graticulePath = React.useMemo(() => {
    if (!projection || !pathGen || !window.d3) return "";
    return pathGen(d3.geoGraticule10());
  }, [projection, pathGen]);

  const selectedIso = React.useMemo(() => {
    if (!selectedCountry) return null;
    return window.COUNTRY_ISO.toISO(selectedCountry);
  }, [selectedCountry]);

  const markerData = React.useMemo(() => {
    if (!countries.length || !pathGen) return [];
    return countries
      .map((d) => {
        const iso = String(d.id).padStart(3, "0");
        const entry = byIso.get(iso);
        if (!entry || !entry.value) return null;
        const [x, y] = pathGen.centroid(d);
        if (!isFinite(x) || !isFinite(y)) return null;
        const radius = 3 + Math.sqrt(entry.value / maxVal) * 10;
        return { iso, entry, x, y, radius };
      })
      .filter(Boolean)
      .sort((a, b) => b.entry.value - a.entry.value)
      .slice(0, 40);
  }, [countries, pathGen, byIso, maxVal]);

  // T024 — Spread the palette across the value distribution. Linear scaling
  // collapses every small-country tier into the lightest swatch when one
  // outlier dominates maxVal; for percentage metrics (lifecycle / readiness /
  // phase, already 0-100) keep linear so the legend ticks remain truthful.
  const isPercentMetric = metric === "lifecycle" || metric === "readiness" || metric === "phase";
  function fillFor(d) {
    const iso = String(d.id).padStart(3, "0");
    const entry = byIso.get(iso);
    if (!entry || !(entry.total || entry.sites.size || entry.value)) return "var(--bg-sunken)";
    const ratio = Math.min(1, entry.value / maxVal);
    const t = isPercentMetric ? ratio : Math.sqrt(ratio);
    const palette = metricTheme.palette;
    const idx = Math.min(palette.length - 1, Math.floor(t * palette.length));
    return palette[idx];
  }

  // T024 — Clamp tooltip position inside map bounds so it never overflows the
  // right or bottom edge on narrow widths. The tooltip is rendered with
  // translate(-50%, -120%) so it floats above the cursor centered horizontally;
  // we leave a 12 px gutter from each edge and shrink the budget when the wrap
  // itself is narrower than the tooltip.
  function clampTooltipPos(rect, clientX, clientY) {
    const gutter = 12;
    const tooltipBudget = Math.min(280, Math.max(160, rect.width - gutter * 2));
    const rawX = clientX - rect.left;
    const minX = tooltipBudget / 2 + gutter;
    const maxX = rect.width - tooltipBudget / 2 - gutter;
    const x = maxX < minX ? rect.width / 2 : Math.max(minX, Math.min(maxX, rawX));
    const rawY = clientY - rect.top;
    const y = Math.max(60, rawY);
    return { x, y };
  }

  function handleMove(e) {
    const rect = ref.current.getBoundingClientRect();
    setHover((h) => {
      if (!h) return h;
      const { x, y } = clampTooltipPos(rect, e.clientX, e.clientY);
      return { ...h, x, y };
    });
  }

  if (err) {
    return (
      <div className="map-wrap" style={{padding: 24}}>
        <Empty title="Couldn't load PR Global Entity map" sub={err + " — check your network connection."} />
      </div>
    );
  }
  if (!topo) {
    return (
      <div className="map-wrap" style={{padding: 24}}>
        <Skeleton height={height - 48}/>
      </div>
    );
  }

  return (
    <div className="map-wrap" ref={ref} style={height ? { height } : undefined} onMouseMove={handleMove}>
      <svg className="map-svg" viewBox="0 0 960 540" preserveAspectRatio="xMidYMid meet">
        <defs>
          <linearGradient id="mapOceanGradient" x1="0" x2="1" y1="0" y2="1">
            <stop offset="0%" stopColor="#F8FBFC"/>
            <stop offset="52%" stopColor="#E8F2F4"/>
            <stop offset="100%" stopColor="#F5F1E8"/>
          </linearGradient>
          <filter id="mapGlow" x="-35%" y="-35%" width="170%" height="170%">
            <feGaussianBlur stdDeviation="3" result="blur"/>
            <feMerge>
              <feMergeNode in="blur"/>
              <feMergeNode in="SourceGraphic"/>
            </feMerge>
          </filter>
        </defs>
        <rect width="960" height="540" fill="url(#mapOceanGradient)"/>
        {graticulePath && <path className="map-graticule" d={graticulePath}/>}
        <g>
          {countries.map((d) => {
            const iso = String(d.id).padStart(3, "0");
            const entry = byIso.get(iso);
            const has = !!(entry && (entry.total || entry.sites.size || entry.value));
            const selected = selectedIso && iso === selectedIso;
            return (
              <path
                key={d.id}
                d={pathGen(d)}
                className={"map-country" + (has ? " has-data" : "") + (selected ? " selected" : "")}
                fill={fillFor(d)}
                onMouseEnter={(e) => {
                  if (!has) return;
                  const rect = ref.current.getBoundingClientRect();
                  const { x, y } = clampTooltipPos(rect, e.clientX, e.clientY);
                  setHover({
                    iso,
                    names: [...entry.names],
                    value: entry.value,
                    activated: entry.activated,
                    total: entry.total,
                    atRisk: entry.atRisk,
                    blocked: entry.blocked,
                    migrated: entry.migrated,
                    hwDelivered: entry.hwDelivered,
                    sites: entry.sites.size,
                    lifecyclePercent: entry.lifecyclePercent,
                    dominantPhase: entry.dominantPhase,
                    dominantPhaseLabel: entry.dominantPhaseLabel,
                    sitesComplete: entry.sitesComplete,
                    readinessPercent: entry.readinessPercent,
                    readinessBlocked: entry.readinessBlocked,
                    readinessTrackedSites: entry.readinessTrackedSites,
                    sitesReadyForMigration: entry.sitesReadyForMigration,
                    x,
                    y,
                  });
                }}
                onMouseLeave={() => setHover(null)}
                onClick={() => {
                  if (!has) return;
                  const canonical = window.COUNTRY_ISO.canonical([...entry.names][0]);
                  onCountryClick && onCountryClick({
                    iso, canonical, names: [...entry.names],
                  });
                }}
              />
            );
          })}
        </g>
        <g className="map-markers">
          {markerData.map((m) => {
            const selected = selectedIso && m.iso === selectedIso;
            return (
              <circle
                key={m.iso}
                className={"map-marker" + (selected ? " selected" : "")}
                cx={m.x}
                cy={m.y}
                r={selected ? m.radius + 2 : m.radius}
                fill={metricTheme.marker}
                opacity={selected ? 0.92 : 0.34}
                filter={selected ? "url(#mapGlow)" : undefined}
              />
            );
          })}
        </g>
      </svg>
      <div className="map-legend">
        <div style={{color: "var(--ink-700)", fontWeight: 500, fontSize: 11.5, textTransform: "uppercase", letterSpacing: "0.06em"}}>
          {metricTheme.label}
        </div>
        <div className="swatches">
          {metricTheme.palette.map((c) => (
            <span key={c} className="sw" style={{ background: c }} />
          ))}
        </div>
        <div style={{display: "flex", justifyContent: "space-between", fontSize: 10, marginTop: 4, color: "var(--ink-400)"}}>
          <span>0</span><span>{maxVal}</span>
        </div>
      </div>
      <div ref={tooltipRef}
           className={"map-tooltip" + (hover ? " show" : "")}
           style={hover ? { left: hover.x, top: hover.y } : {}}>
        {hover && (
          <>
            <div className="ttl">{window.COUNTRY_ISO.canonical(hover.names[0])}</div>
            <div className="row">{hover.sites} canonical site{hover.sites === 1 ? "" : "s"} · {hover.total} canonical link{hover.total === 1 ? "" : "s"} · {hover.activated} activated · {hover.atRisk} at risk</div>
            {(hover.hwDelivered || hover.migrated || hover.blocked) ? (
              <div className="row">{hover.hwDelivered || 0} HW delivered · {hover.migrated || 0} migrated · {hover.blocked || 0} blocked</div>
            ) : null}
            {(metric === "lifecycle" || metric === "phase" || hover.lifecyclePercent) && (
              <div className="row">Lifecycle avg <strong>{hover.lifecyclePercent || 0}%</strong> · phase <strong>{hover.dominantPhaseLabel || hover.dominantPhase}</strong> · {hover.sitesComplete} complete</div>
            )}
            {(metric === "readiness" || hover.readinessPercent || hover.readinessBlocked) && (
              <div className="row">Readiness <strong>{hover.readinessPercent || 0}%</strong> · {hover.sitesReadyForMigration || 0}/{hover.readinessTrackedSites || 0} sites ready · {hover.readinessBlocked || 0} blockers</div>
            )}
          </>
        )}
      </div>
    </div>
  );
}

Object.assign(window, { WorldMap, useWorldTopology });
