const { useState, useEffect, useRef, useMemo, useCallback } = React;

// — Minimal localStorage-backed tweak state (replaces the in-editor host
// version of useTweaks once deployed). Keys persist across page loads so the
// kid's chosen colour / settings stick.
const TWEAK_STORAGE_KEY = "atw-tweaks-v1";
function useTweaks(defaults) {
  const [values, setValues] = React.useState(() => {
    try {
      const raw = localStorage.getItem(TWEAK_STORAGE_KEY);
      if (raw) return { ...defaults, ...JSON.parse(raw) };
    } catch (e) {}
    return defaults;
  });
  const setTweak = React.useCallback((keyOrEdits, val) => {
    const edits = typeof keyOrEdits === "object" && keyOrEdits !== null
      ? keyOrEdits : { [keyOrEdits]: val };
    setValues((prev) => {
      const next = { ...prev, ...edits };
      try { localStorage.setItem(TWEAK_STORAGE_KEY, JSON.stringify(next)); } catch (e) {}
      return next;
    });
  }, []);
  return [values, setTweak];
}

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "accent": "#ff8a3d",
  "autoRotate": true,
  "rotateSpeed": 0.06,
  "showGraticule": false,
  "dayNight": false,
  "timeOfDay": -1
}/*EDITMODE-END*/;

const ACCENT_OPTIONS = [
  "#ff8a3d", // orange
  "#ffd166", // amber
  "#f4d35e", // gold
  "#9ee37d", // lime
  "#3ecbb1", // teal
  "#4dabf7", // sky
  "#7c83ff", // indigo
  "#a07bff", // violet
  "#ff7ad9", // pink
  "#ff5d6c", // red
];

// derive a lighter "soft" color from accent for stroke
function softenHex(hex) {
  const m = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
  if (!m) return hex;
  const [r,g,b] = [parseInt(m[1],16), parseInt(m[2],16), parseInt(m[3],16)];
  const mix = (c) => Math.round(c + (255 - c) * 0.45);
  const hx = (n) => n.toString(16).padStart(2,"0");
  return `#${hx(mix(r))}${hx(mix(g))}${hx(mix(b))}`;
}

// Continent inference from longitude+latitude centroid (rough but useful)
function inferContinent(lon, lat) {
  if (lat < -60) return "Antarctica";
  if (lon >= -170 && lon <= -30 && lat >= 7) return "North America";
  if (lon >= -85 && lon <= -34 && lat < 13) return "South America";
  if (lon >= -25 && lon <= 60 && lat >= 35) return "Europe";
  if (lon >= -20 && lon <= 55 && lat < 35 && lat >= -35) return "Africa";
  if (lon >= 25 && lon <= 180 && lat >= 0) return "Asia";
  if (lon >= 110 && lon <= 180 && lat < 0) return "Oceania";
  return "—";
}

function formatLatLon(lon, lat) {
  const ns = lat >= 0 ? "N" : "S";
  const ew = lon >= 0 ? "E" : "W";
  return `${Math.abs(lat).toFixed(1)}° ${ns} latitude, ${Math.abs(lon).toFixed(1)}° ${ew} longitude`;
}

// Compute time-related info for a country given an IANA timezone name.
// Returns null if the timezone is missing or invalid.
function timeInfoFor(timezone) {
  if (!timezone) return null;
  try {
    const now = new Date();
    // Local time in that zone (e.g. "2:34 PM")
    const local = new Intl.DateTimeFormat('en-US', {
      timeZone: timezone, hour: 'numeric', minute: '2-digit', hour12: true,
    }).format(now);
    // Offset string like "GMT+05:30"
    const off = new Intl.DateTimeFormat('en-US', {
      timeZone: timezone, timeZoneName: 'longOffset',
    }).formatToParts(now).find(p => p.type === 'timeZoneName')?.value || 'GMT+00:00';
    const m = off.match(/GMT([+-])(\d{1,2})(?::(\d{2}))?/);
    const countryMin = m ? (m[1] === '-' ? -1 : 1) * (parseInt(m[2], 10) * 60 + parseInt(m[3] || '0', 10)) : 0;
    const userMin = -now.getTimezoneOffset(); // user's offset vs UTC, in minutes
    const diffMin = countryMin - userMin;     // ahead (+) / behind (-) the user

    const formatOffset = (min) => {
      const sign = min >= 0 ? '+' : '−';
      const a = Math.abs(min);
      const h = Math.floor(a / 60), mm = a % 60;
      return mm === 0 ? `UTC${sign}${h}` : `UTC${sign}${h}:${String(mm).padStart(2, '0')}`;
    };
    const formatDiff = (min) => {
      if (min === 0) return 'Same as your time';
      const sign = min > 0 ? 'ahead of' : 'behind';
      const a = Math.abs(min);
      const h = Math.floor(a / 60), mm = a % 60;
      const parts = [];
      if (h) parts.push(`${h}h`);
      if (mm) parts.push(`${mm}m`);
      return `${parts.join(' ')} ${sign} you`;
    };
    return {
      local,
      utc: formatOffset(countryMin),
      diff: formatDiff(diffMin),
    };
  } catch {
    return null;
  }
}

// Rough sea / ocean lookup for an open-water lat/lon. Order matters — specific
// seas first, oceans last. Bounds are approximate but good enough to give
// kids a meaningful "you're over the [Mediterranean]" hint.
function inferSea(lon, lat) {
  // Normalise longitude to [-180, 180]
  let L = lon;
  while (L > 180) L -= 360;
  while (L < -180) L += 360;
  const inBox = (W, E, S, N) => L >= W && L <= E && lat >= S && lat <= N;

  // Specific seas / gulfs / bays
  if (inBox(-5, 36, 30, 46)) return "Mediterranean Sea";
  if (inBox(-88, -60, 9, 22)) return "Caribbean Sea";
  if (inBox(32, 44, 12, 30)) return "Red Sea";
  if (inBox(27, 42, 41, 47)) return "Black Sea";
  if (inBox(47, 55, 36, 47)) return "Caspian Sea";
  if (inBox(48, 57, 24, 30.5)) return "Persian Gulf";
  if (inBox(50, 70, 8, 25)) return "Arabian Sea";
  if (inBox(78, 95, 5, 22)) return "Bay of Bengal";
  if (inBox(105, 122, 0, 23)) return "South China Sea";
  if (inBox(120, 130, 23, 33)) return "East China Sea";
  if (inBox(128, 142, 33, 52)) return "Sea of Japan";
  if (inBox(162, 180, 52, 66) || inBox(-180, -160, 52, 66)) return "Bering Sea";
  if (inBox(-4, 9, 50, 62)) return "North Sea";
  if (inBox(9, 30, 53, 66)) return "Baltic Sea";
  if (inBox(-95, -75, 50, 65)) return "Hudson Bay";
  if (inBox(-98, -80, 18, 31)) return "Gulf of Mexico";
  if (inBox(110, 155, -20, 0)) return "Arafura / Timor Sea";
  if (inBox(140, 160, -45, -30)) return "Tasman Sea";
  if (inBox(110, 155, -10, 8)) return "Banda / Java Sea";

  // Polar caps
  if (lat > 70) return "Arctic Ocean";
  if (lat < -60) return "Southern Ocean";

  // Major oceans by longitude band
  if (L >= -70 && L <= 20) return "Atlantic Ocean";
  if (L >= 20 && L <= 110) return "Indian Ocean";
  return "Pacific Ocean";
}
function fieldLabel(key) {
  const spaced = String(key).replace(/[_-]+/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2");
  return spaced.split(" ").map((w) => {
    if (!w) return w;
    // Keep all-caps short tokens (GDP, USA, EU) uppercase.
    if (w.length <= 4 && w === w.toUpperCase()) return w;
    return w[0].toUpperCase() + w.slice(1).toLowerCase();
  }).join(" ");
}

// Render a yaml value as plain text — numbers get thousands separators,
// arrays become comma lists. Objects fall through to JSON for visibility.
function formatFieldValue(v) {
  if (v == null || v === "") return "—";
  if (typeof v === "number") return v.toLocaleString();
  if (Array.isArray(v)) return v.map(formatFieldValue).join(", ");
  if (typeof v === "object") return JSON.stringify(v);
  return String(v);
}

// Compute approximate sub-solar point (where the sun is directly overhead).
// `hourUTC`: 0-24 (real-time if -1).
function subsolarPoint(hourUTC) {
  const now = new Date();
  const start = Date.UTC(now.getUTCFullYear(), 0, 0);
  const dayOfYear = (now.getTime() - start) / 86400000;
  // Solar declination (degrees)
  const decl = -23.44 * Math.cos((2 * Math.PI / 365.25) * (dayOfYear + 10));
  const h = hourUTC < 0
    ? now.getUTCHours() + now.getUTCMinutes() / 60 + now.getUTCSeconds() / 3600
    : hourUTC;
  // Subsolar longitude: at 12:00 UTC, sun is at 0° lon; -15° per UTC hour
  const lon = -15 * (h - 12);
  return [lon, decl];
}

// Format hour 0-24 as "HH:MM UTC"
function fmtHour(h) {
  if (h < 0) return "Now";
  const hh = Math.floor(h);
  const mm = Math.round((h - hh) * 60);
  return `${String(hh).padStart(2,"0")}:${String(mm).padStart(2,"0")} UTC`;
}

// Generate a deterministic starfield (stable across re-renders)
function makeStars(count, seed = 1) {
  const rand = mulberry32(seed);
  const stars = [];
  for (let i = 0; i < count; i++) {
    stars.push({
      x: rand(),
      y: rand(),
      r: 0.3 + rand() * 1.3,
      o: 0.25 + rand() * 0.75,
      tw: 2 + rand() * 5, // twinkle period (s)
      td: rand() * 5,     // twinkle delay
    });
  }
  return stars;
}
function mulberry32(a) {
  return function() {
    let t = (a += 0x6D2B79F5);
    t = Math.imul(t ^ (t >>> 15), t | 1);
    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

function Starfield({ count = 220 }) {
  const stars = useMemo(() => makeStars(count, 7), [count]);
  return (
    <svg className="starfield" preserveAspectRatio="none" viewBox="0 0 100 100" aria-hidden="true">
      {stars.map((s, i) => (
        <circle
          key={i}
          cx={s.x * 100}
          cy={s.y * 100}
          r={s.r * 0.12}
          fill="white"
          opacity={s.o}
          style={{ animation: `twinkle ${s.tw}s ease-in-out ${s.td}s infinite alternate` }}
        />
      ))}
    </svg>
  );
}

function SearchBox({ world, onPick }) {
  const [query, setQuery] = useState("");
  const [open, setOpen] = useState(false);
  const [highlight, setHighlight] = useState(0);
  const inputRef = useRef(null);
  const wrapRef = useRef(null);

  const results = useMemo(() => {
    if (!world || !query.trim()) return [];
    const q = query.trim().toLowerCase();
    // exact + startsWith first, then contains
    const exact = [], starts = [], contains = [];
    for (const f of world.features) {
      const n = (f.properties.name || "").toLowerCase();
      if (!n) continue;
      if (n === q) exact.push(f);
      else if (n.startsWith(q)) starts.push(f);
      else if (n.includes(q)) contains.push(f);
    }
    const sortByName = (a, b) => a.properties.name.localeCompare(b.properties.name);
    return [...exact, ...starts.sort(sortByName), ...contains.sort(sortByName)].slice(0, 7);
  }, [world, query]);

  useEffect(() => { setHighlight(0); }, [query]);

  // Close when clicking outside
  useEffect(() => {
    const onDocClick = (e) => {
      if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false);
    };
    document.addEventListener("mousedown", onDocClick);
    return () => document.removeEventListener("mousedown", onDocClick);
  }, []);

  // Esc clears the search box, closes the dropdown, and blurs the input —
  // works whether or not the input currently has focus.
  useEffect(() => {
    const onKey = (e) => {
      if (e.key === "Escape") {
        setQuery("");
        setOpen(false);
        inputRef.current?.blur();
      }
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, []);

  const pick = (f) => {
    if (!f) return;
    onPick(f);
    setQuery("");
    setOpen(false);
    inputRef.current?.blur();
  };

  const onKeyDown = (e) => {
    if (e.key === "ArrowDown") {
      e.preventDefault();
      setOpen(true);
      setHighlight(h => Math.min(results.length - 1, h + 1));
    } else if (e.key === "ArrowUp") {
      e.preventDefault();
      setHighlight(h => Math.max(0, h - 1));
    } else if (e.key === "Enter") {
      e.preventDefault();
      pick(results[highlight]);
    }
    // Esc is handled by the global listener above so a single press always
    // clears the query + closes the dropdown, even when input is unfocused.
  };

  const capitalFor = (name) => {
    const info = (window.COUNTRIES || {})[(name || "").toLowerCase()];
    return (info && info.capital) || "";
  };

  return (
    <div className="searchbox" ref={wrapRef} onClick={(e) => e.stopPropagation()}>
      <svg className="search-icon" viewBox="0 0 16 16" width="14" height="14" aria-hidden="true">
        <path fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"
              d="M7 12.5A5.5 5.5 0 1 1 7 1.5a5.5 5.5 0 0 1 0 11Zm4.2-1.4 3.3 3.3"/>
      </svg>
      <input
        ref={inputRef}
        type="text"
        value={query}
        placeholder="Search a country…"
        onChange={(e) => { setQuery(e.target.value); setOpen(true); }}
        onFocus={() => setOpen(true)}
        onKeyDown={onKeyDown}
        spellCheck={false}
        autoComplete="off"
      />
      <kbd className="search-kbd" style={{ display: "none" }}>/</kbd>
      {open && results.length > 0 && (
        <ul className="search-results" role="listbox">
          {results.map((f, i) => {
            const name = f.properties.name;
            const cap = capitalFor(name);
            return (
              <li
                key={f.id}
                role="option"
                aria-selected={i === highlight}
                className={"search-item" + (i === highlight ? " is-active" : "")}
                onMouseEnter={() => setHighlight(i)}
                onMouseDown={(e) => { e.preventDefault(); pick(f); }}
              >
                <span className="search-name">{name}</span>
                {cap && <span className="search-cap">{cap}</span>}
              </li>
            );
          })}
        </ul>
      )}
      {open && query.trim() && results.length === 0 && (
        <div className="search-empty">No country matches “{query}”</div>
      )}
    </div>
  );
}

// ===== Quiz overlay =====
// Two quick question types kids love:
//   • "What is the capital of X?" — 4-way multiple choice.
//   • "Which has more people?"   — 2-way binary.
// Both pull from window.COUNTRIES (loaded from countries.yml) plus the live
// world feature list (so country names match what's on the globe).
function QuizOverlay({ world, onClose }) {
  const [score, setScore] = React.useState(0);
  const [streak, setStreak] = React.useState(0);
  const [q, setQ] = React.useState(null);
  const [picked, setPicked] = React.useState(null);
  const [best, setBest] = React.useState(() => {
    const n = Number(localStorage.getItem("globe-quiz-best") || 0);
    return Number.isFinite(n) ? n : 0;
  });

  // Quizzable pool: features whose YAML entry has capital + population + iso2.
  const pool = useMemo(() => {
    if (!world) return [];
    const C = window.COUNTRIES || {};
    return world.features
      .map((f) => {
        const name = f.properties.name;
        const info = C[name.toLowerCase()];
        if (!info) return null;
        if (!info.capital || info.capital === "—") return null;
        if (!info.population || info.population < 100000) return null;
        return { feature: f, name, ...info };
      })
      .filter(Boolean);
  }, [world]);

  const shuffle = (arr) => {
    const a = arr.slice();
    for (let i = a.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [a[i], a[j]] = [a[j], a[i]];
    }
    return a;
  };

  const newQuestion = React.useCallback(() => {
    setPicked(null);
    if (pool.length < 5) { setQ(null); return; }
    const type = Math.random() < 0.6 ? "capital" : "population";
    if (type === "capital") {
      const correct = pool[Math.floor(Math.random() * pool.length)];
      // Pick 3 unique distractor capitals
      const distractors = [];
      const used = new Set([correct.capital]);
      let safety = 0;
      while (distractors.length < 3 && safety++ < 200) {
        const cand = pool[Math.floor(Math.random() * pool.length)];
        if (!used.has(cand.capital)) {
          used.add(cand.capital);
          distractors.push(cand);
        }
      }
      const options = shuffle([correct, ...distractors]);
      setQ({
        type, country: correct,
        correctIdx: options.indexOf(correct),
        options: options.map((c) => ({ text: c.capital, iso2: c.iso2 })),
      });
    } else {
      // Pick two countries with meaningfully different populations
      let a, b, safety = 0;
      do {
        a = pool[Math.floor(Math.random() * pool.length)];
        b = pool[Math.floor(Math.random() * pool.length)];
        safety++;
      } while ((a === b || Math.abs(a.population - b.population) < a.population * 0.2) && safety < 80);
      const options = [a, b];
      setQ({
        type, correctIdx: a.population > b.population ? 0 : 1,
        a, b,
        options: options.map((c) => ({ text: c.name, iso2: c.iso2, population: c.population })),
      });
    }
  }, [pool]);

  React.useEffect(() => { newQuestion(); }, [newQuestion]);

  if (!q) {
    return (
      <div className="quiz-backdrop" onClick={onClose}>
        <div className="quiz-card" onClick={(e) => e.stopPropagation()}>
          <div className="quiz-top">
            <div className="quiz-score"><span className="label">Loading quiz…</span></div>
            <button className="quiz-close" onClick={onClose}>✕</button>
          </div>
        </div>
      </div>
    );
  }

  const pick = (i) => {
    if (picked != null) return;
    setPicked(i);
    if (i === q.correctIdx) {
      const nextScore = score + 1;
      const nextStreak = streak + 1;
      setScore(nextScore);
      setStreak(nextStreak);
      if (nextScore > best) {
        setBest(nextScore);
        localStorage.setItem("globe-quiz-best", String(nextScore));
      }
    } else {
      setStreak(0);
    }
  };

  const isCorrect = picked != null && picked === q.correctIdx;
  const flag = (iso2, size = 40) =>
    iso2 ? <img className="flag-inline"
                  src={`https://flagcdn.com/w${size}/${iso2.toLowerCase()}.png`}
                  alt="" /> : null;

  return (
    <div className="quiz-backdrop" onClick={onClose}>
      <div className="quiz-card" onClick={(e) => e.stopPropagation()}>
        <div className="quiz-top">
          <div className="quiz-score">
            <div><span className="label">Score</span><span className="value">{score}</span></div>
            <div><span className="label">Streak</span><span className="value streak">{streak}{streak >= 3 ? " 🔥" : ""}</span></div>
            <div><span className="label">Best</span><span className="value">{best}</span></div>
          </div>
          <button className="quiz-close" onClick={onClose} title="Close quiz">✕</button>
        </div>
        <div className="quiz-kind">{q.type === "capital" ? "Capital City" : "Bigger Population"}</div>
        <div className="quiz-q">
          {q.type === "capital" ? (
            <>What is the capital of <span className="country-strong">{q.country.name}</span>?
              {flag(q.country.iso2, 80)}</>
          ) : (
            <>Which country has more people?</>
          )}
        </div>
        <div className="quiz-options" role="radiogroup">
          {q.options.map((opt, i) => {
            let cls = "quiz-option";
            if (picked != null) {
              if (i === q.correctIdx) cls += " correct";
              else if (i === picked) cls += " wrong";
            }
            return (
              <button key={i} className={cls} disabled={picked != null} onClick={() => pick(i)}>
                <span className="opt-letter">{String.fromCharCode(65 + i)}</span>
                {q.type === "population" && flag(opt.iso2, 40)}
                <span>{opt.text}</span>
              </button>
            );
          })}
        </div>
        <div className="quiz-feedback">
          {picked != null && (
            isCorrect ? (
              q.type === "capital"
                ? <>✅ Correct! {q.country.capital} is the capital of {q.country.name}.</>
                : <>✅ Correct! {q.options[q.correctIdx].text} — {q.options[q.correctIdx].population.toLocaleString()} people.</>
            ) : (
              q.type === "capital"
                ? <>❌ Not quite — the capital of {q.country.name} is <b>{q.country.capital}</b>.</>
                : <>❌ Not quite — <b>{q.options[q.correctIdx].text}</b> has more people ({q.options[q.correctIdx].population.toLocaleString()} vs {q.options[1 - q.correctIdx].population.toLocaleString()}).</>
            )
          )}
        </div>
        <div className="quiz-actions">
          {picked != null && (
            <button className="play-btn primary" onClick={newQuestion}>Next question →</button>
          )}
        </div>
      </div>
    </div>
  );
}

function GlobeApp() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const accent = t.accent || "#ff8a3d";
  const soft = useMemo(() => softenHex(accent), [accent]);

  const wrapRef = useRef(null);
  const svgRef = useRef(null);

  const [world, setWorld] = useState(null);
  const [error, setError] = useState(null);
  const [selected, setSelected] = useState(null);
  const [rotation, setRotation] = useState([20, -15, 0]);
  const [size, setSize] = useState(720);
  const [hoverInfo, setHoverInfo] = useState(null);
  const [quizOpen, setQuizOpen] = useState(false);

  const rotationRef = useRef(rotation);
  rotationRef.current = rotation;
  const autoRotateRef = useRef(t.autoRotate);
  autoRotateRef.current = t.autoRotate;
  const rotateSpeedRef = useRef(t.rotateSpeed);
  rotateSpeedRef.current = t.rotateSpeed;
  const interactingRef = useRef(false);
  const selectCountryRef = useRef(null);
  const pauseUntilRef = useRef(0);
  const clearTimerRef = useRef(null);

  // Load world data once
  useEffect(() => {
    let canceled = false;
    fetch("https://cdn.jsdelivr.net/npm/world-atlas@2.0.2/countries-110m.json")
      .then(r => {
        if (!r.ok) throw new Error("Failed to load world data");
        return r.json();
      })
      .then(topo => {
        if (canceled) return;
        const countries = topojson.feature(topo, topo.objects.countries);
        setWorld(countries);
      })
      .catch(e => !canceled && setError(e.message));
    return () => { canceled = true; };
  }, []);

  // Track size
  useEffect(() => {
    if (!wrapRef.current) return;
    const ro = new ResizeObserver(entries => {
      for (const e of entries) {
        setSize(Math.max(320, e.contentRect.width));
      }
    });
    ro.observe(wrapRef.current);
    return () => ro.disconnect();
  }, []);

  // Render via D3
  useEffect(() => {
    if (!world || !svgRef.current) return;

    const projection = d3.geoOrthographic()
      .scale(size * 0.46)
      .translate([size / 2, size / 2])
      .clipAngle(90)
      .rotate(rotation);
    const pathGen = d3.geoPath(projection);

    const svg = d3.select(svgRef.current);
    svg.selectAll("*").remove();

    const defs = svg.append("defs");

    const oceanGrad = defs.append("radialGradient")
      .attr("id", "ocean-grad")
      .attr("cx", "35%").attr("cy", "30%").attr("r", "75%");
    oceanGrad.append("stop").attr("offset", "0%").attr("stop-color", "#1e3458");
    oceanGrad.append("stop").attr("offset", "60%").attr("stop-color", "#142235");
    oceanGrad.append("stop").attr("offset", "100%").attr("stop-color", "#08111f");

    const glow = defs.append("radialGradient")
      .attr("id", "glow-grad")
      .attr("cx", "50%").attr("cy", "50%").attr("r", "50%");
    glow.append("stop").attr("offset", "70%").attr("stop-color", accent).attr("stop-opacity", "0");
    glow.append("stop").attr("offset", "92%").attr("stop-color", accent).attr("stop-opacity", "0.18");
    glow.append("stop").attr("offset", "100%").attr("stop-color", accent).attr("stop-opacity", "0");

    const spec = defs.append("radialGradient")
      .attr("id", "spec-grad")
      .attr("cx", "30%").attr("cy", "25%").attr("r", "55%");
    spec.append("stop").attr("offset", "0%").attr("stop-color", "#ffffff").attr("stop-opacity", "0.16");
    spec.append("stop").attr("offset", "55%").attr("stop-color", "#ffffff").attr("stop-opacity", "0");

    // Atmosphere glow
    svg.append("circle")
      .attr("cx", size / 2).attr("cy", size / 2)
      .attr("r", size * 0.49)
      .attr("fill", "url(#glow-grad)")
      .attr("pointer-events", "none");

    // Ocean
    svg.append("path")
      .datum({ type: "Sphere" })
      .attr("class", "ocean")
      .attr("d", pathGen)
      .attr("fill", "url(#ocean-grad)");

    if (t.showGraticule) {
      svg.append("path")
        .datum(d3.geoGraticule10())
        .attr("class", "graticule")
        .attr("d", pathGen);
    }

    // Day/Night terminator + sun glow
    if (t.dayNight) {
      const [sunLon, sunLat] = subsolarPoint(t.timeOfDay);
      const antipode = [sunLon + 180, -sunLat];
      const nightHemisphere = d3.geoCircle().center(antipode).radius(90)();
      const antipodePx = projection(antipode); // null if antipode is on far side
      const sunPx = projection([sunLon, sunLat]);

      // Night gradient — darkest at midnight (antipode), fading toward terminator
      const nightGrad = defs.append("radialGradient")
        .attr("id", "night-grad")
        .attr("gradientUnits", "userSpaceOnUse")
        .attr("cx", antipodePx ? antipodePx[0] : size / 2)
        .attr("cy", antipodePx ? antipodePx[1] : size / 2)
        .attr("r", size * 0.46);
      nightGrad.append("stop").attr("offset", "0%")
        .attr("stop-color", "#01030a").attr("stop-opacity", "0.88");
      nightGrad.append("stop").attr("offset", "55%")
        .attr("stop-color", "#0a1530").attr("stop-opacity", "0.72");
      nightGrad.append("stop").attr("offset", "92%")
        .attr("stop-color", "#1a2540").attr("stop-opacity", "0.18");
      nightGrad.append("stop").attr("offset", "100%")
        .attr("stop-color", "#2a3450").attr("stop-opacity", "0");

      svg.append("path")
        .datum(nightHemisphere)
        .attr("class", "night-shade")
        .attr("d", pathGen)
        .attr("fill", "url(#night-grad)")
        .attr("pointer-events", "none");

      // Day-side warm tint (subtle): bright where the sun is overhead
      if (sunPx) {
        const dayGrad = defs.append("radialGradient")
          .attr("id", "day-grad")
          .attr("gradientUnits", "userSpaceOnUse")
          .attr("cx", sunPx[0]).attr("cy", sunPx[1])
          .attr("r", size * 0.4);
        dayGrad.append("stop").attr("offset", "0%")
          .attr("stop-color", "#fff3d4").attr("stop-opacity", "0.18");
        dayGrad.append("stop").attr("offset", "60%")
          .attr("stop-color", "#ffd29a").attr("stop-opacity", "0.06");
        dayGrad.append("stop").attr("offset", "100%")
          .attr("stop-color", "#fff").attr("stop-opacity", "0");

        // Clip to sphere so day tint doesn't bleed outside the globe
        const dayClipId = "day-clip";
        const dayClip = defs.append("clipPath").attr("id", dayClipId);
        dayClip.append("path").datum({ type: "Sphere" }).attr("d", pathGen);
      }

      // Sun halo outside the globe — placed at the sun's screen direction
      const cx = size / 2, cy = size / 2;
      const radiusGlobe = size * 0.46;
      let haloX, haloY;
      if (sunPx) {
        // Sun is on the near side: place halo on the rim closest to the sun
        const dx = sunPx[0] - cx, dy = sunPx[1] - cy;
        const mag = Math.hypot(dx, dy) || 1;
        haloX = cx + (dx / mag) * radiusGlobe;
        haloY = cy + (dy / mag) * radiusGlobe;
      } else {
        // Sun is on the far side: no visible halo
        haloX = haloY = null;
      }
      if (haloX != null) {
        const sunHalo = defs.append("radialGradient")
          .attr("id", "sun-halo")
          .attr("gradientUnits", "userSpaceOnUse")
          .attr("cx", haloX).attr("cy", haloY)
          .attr("r", size * 0.32);
        sunHalo.append("stop").attr("offset", "0%")
          .attr("stop-color", "#fff4d4").attr("stop-opacity", "0.55");
        sunHalo.append("stop").attr("offset", "30%")
          .attr("stop-color", "#ffd29a").attr("stop-opacity", "0.25");
        sunHalo.append("stop").attr("offset", "100%")
          .attr("stop-color", "#ffb37a").attr("stop-opacity", "0");

        svg.append("circle")
          .attr("cx", haloX).attr("cy", haloY)
          .attr("r", size * 0.32)
          .attr("fill", "url(#sun-halo)")
          .attr("pointer-events", "none");

        // Small sun disc at the rim
        svg.append("circle")
          .attr("cx", haloX).attr("cy", haloY)
          .attr("r", size * 0.012)
          .attr("fill", "#fff5d8")
          .attr("opacity", 0.9)
          .attr("pointer-events", "none");
      }
    }

    // Countries
    const countriesG = svg.append("g").attr("class", "countries");
    countriesG.selectAll("path")
      .data(world.features)
      .join("path")
      .attr("class", d => "country" + (selected && selected.id === d.id ? " selected" : ""))
      .attr("d", pathGen)
      .attr("fill", d => (selected && selected.id === d.id) ? accent : "#2a3340")
      .attr("stroke", d => (selected && selected.id === d.id) ? soft : "#3d4856")
      .attr("stroke-width", d => (selected && selected.id === d.id) ? 1.0 : 0.5)
      .style("cursor", "pointer")
      .on("click", (event, d) => {
        event.stopPropagation();
        if (interactingRef.current) return;
        selectCountry(d);
      });

    // Specular highlight
    svg.append("circle")
      .attr("cx", size / 2).attr("cy", size / 2)
      .attr("r", size * 0.46)
      .attr("fill", "url(#spec-grad)")
      .attr("pointer-events", "none");

    // Sphere outline
    svg.append("path")
      .datum({ type: "Sphere" })
      .attr("class", "sphere-stroke")
      .attr("d", pathGen);

  }, [world, size, rotation, selected, t.showGraticule, t.dayNight, t.timeOfDay, accent, soft]);

  // Drag with versor for natural orthographic rotation
  useEffect(() => {
    if (!world || !svgRef.current) return;
    const svg = svgRef.current;

    let startXY = null;
    let movedPx = 0;

    const getXY = (e) => {
      const rect = svg.getBoundingClientRect();
      const scaleX = size / rect.width;
      const scaleY = size / rect.height;
      return [(e.clientX - rect.left) * scaleX, (e.clientY - rect.top) * scaleY];
    };

    let pointerId = null;

    const onPointerDown = (e) => {
      // IMPORTANT: don't setPointerCapture here — it would redirect the
      // subsequent click event away from the country path, breaking selection.
      // We capture lazily once the user actually starts dragging.
      pointerId = e.pointerId;
      interactingRef.current = false;
      movedPx = 0;
      const pt = getXY(e);
      startXY = pt;

      const proj = d3.geoOrthographic()
        .scale(size * 0.46)
        .translate([size / 2, size / 2])
        .clipAngle(90)
        .rotate(rotationRef.current);

      const inv = proj.invert(pt);
      if (!inv || isNaN(inv[0])) {
        svg._drag = { fallback: true, lastPt: pt, captured: false };
        return;
      }
      const v0 = versor.cartesian(inv);
      const r0 = rotationRef.current.slice();
      const q0 = versor(r0);
      svg._drag = { proj, v0, q0, r0, captured: false };
    };

    const onPointerMove = (e) => {
      if (!svg._drag) return;
      const pt = getXY(e);
      if (startXY) movedPx = Math.max(movedPx, Math.hypot(pt[0]-startXY[0], pt[1]-startXY[1]));
      if (movedPx > 3) {
        interactingRef.current = true;
        // Now capture so the drag continues even if pointer leaves the SVG.
        if (!svg._drag.captured && pointerId != null) {
          try { svg.setPointerCapture(pointerId); } catch (err) {}
          svg._drag.captured = true;
          svg.classList.add("dragging");
        }
      }
      if (movedPx <= 3) return; // don't rotate on micro-movements (treat as click)

      // Fallback drag (when pointer started outside sphere)
      if (svg._drag.fallback) {
        const last = svg._drag.lastPt;
        const dx = pt[0] - last[0];
        const dy = pt[1] - last[1];
        const r = rotationRef.current;
        const k = 0.25;
        const newR = [r[0] + dx * k, Math.max(-90, Math.min(90, r[1] - dy * k)), r[2]];
        rotationRef.current = newR;
        setRotation(newR);
        svg._drag.lastPt = pt;
        return;
      }

      const { proj, v0, q0, r0 } = svg._drag;
      proj.rotate(r0);
      const inv = proj.invert(pt);
      if (!inv || isNaN(inv[0])) return;
      const v1 = versor.cartesian(inv);
      const q1 = versor.multiply(q0, versor.delta(v0, v1));
      const newR = versor.rotation(q1);
      rotationRef.current = newR;
      setRotation(newR);
    };

    const onPointerUp = (e) => {
      svg.classList.remove("dragging");
      if (svg._drag && svg._drag.captured && pointerId != null) {
        try { svg.releasePointerCapture(pointerId); } catch (err) {}
      }
      const wasDrag = movedPx > 3;
      svg._drag = null;
      pointerId = null;
      // If this was a tap (not a drag), do geographic hit-testing here.
      // Native click events are unreliable while auto-rotate is rebuilding the
      // SVG every frame — the path under pointerdown can be destroyed before
      // click fires. Hit-testing on pointerup sidesteps the issue entirely.
      if (!wasDrag) {
        const pt = getXY(e);
        const proj = d3.geoOrthographic()
          .scale(size * 0.46)
          .translate([size / 2, size / 2])
          .clipAngle(90)
          .rotate(rotationRef.current);
        const inv = proj.invert(pt);
        if (inv && !isNaN(inv[0])) {
          const hit = world && world.features.find((f) => d3.geoContains(f, inv));
          if (hit) {
            selectCountryRef.current && selectCountryRef.current(hit);
          } else {
            setSelected(null);
          }
        } else {
          setSelected(null);
        }
      }
      // keep interactingRef true briefly so any stray click after a real drag is suppressed
      setTimeout(() => { interactingRef.current = false; }, 50);
    };

    svg.addEventListener("pointerdown", onPointerDown);
    svg.addEventListener("pointermove", onPointerMove);
    svg.addEventListener("pointerup", onPointerUp);
    svg.addEventListener("pointercancel", onPointerUp);

    // Hover tracker — separate from drag handler. Reports country / sea +
    // lat/lon under the cursor live, or null when the pointer leaves the sphere.
    const onHoverMove = (e) => {
      const pt = getXY(e);
      const proj = d3.geoOrthographic()
        .scale(size * 0.46)
        .translate([size / 2, size / 2])
        .clipAngle(90)
        .rotate(rotationRef.current);
      const inv = proj.invert(pt);
      if (!inv || isNaN(inv[0])) { setHoverInfo(null); return; }
      const [lon, lat] = inv;
      // Find which country (if any) contains this point.
      const hit = world && world.features.find((f) => d3.geoContains(f, [lon, lat]));
      if (hit) {
        const c = d3.geoCentroid(hit);
        setHoverInfo({
          lon, lat,
          kind: "country",
          name: hit.properties.name,
          continent: inferContinent(c[0], c[1]),
        });
      } else {
        setHoverInfo({
          lon, lat,
          kind: "sea",
          name: inferSea(lon, lat),
        });
      }
    };
    const onHoverLeave = () => setHoverInfo(null);
    svg.addEventListener("pointermove", onHoverMove);
    svg.addEventListener("pointerleave", onHoverLeave);

    return () => {
      svg.removeEventListener("pointerdown", onPointerDown);
      svg.removeEventListener("pointermove", onPointerMove);
      svg.removeEventListener("pointerup", onPointerUp);
      svg.removeEventListener("pointercancel", onPointerUp);
      svg.removeEventListener("pointermove", onHoverMove);
      svg.removeEventListener("pointerleave", onHoverLeave);
    };
  }, [world, size]);

  // Auto-rotate
  useEffect(() => {
    let raf;
    let last = performance.now();
    const tick = (now) => {
      const dt = Math.min(50, now - last);
      last = now;
      if (autoRotateRef.current && !interactingRef.current && now >= pauseUntilRef.current && !document.hidden) {
        const r = rotationRef.current;
        const speed = rotateSpeedRef.current;
        const newR = [r[0] + speed * (dt / 16.67), r[1], r[2]];
        rotationRef.current = newR;
        setRotation(newR);
      }
      raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, []);

  // Smooth rotate-to: aim a [lon, lat] toward the camera
  const rotateToCoords = useCallback((coords) => {
    const start = rotationRef.current.slice();
    const target = [-coords[0], -coords[1], 0];
    let dLam = target[0] - start[0];
    while (dLam > 180) dLam -= 360;
    while (dLam < -180) dLam += 360;
    const dPhi = target[1] - start[1];
    const duration = 700;
    const t0 = performance.now();
    const ease = (x) => 1 - Math.pow(1 - x, 3);
    const step = (now) => {
      const k = Math.min(1, (now - t0) / duration);
      const e = ease(k);
      const r = [start[0] + dLam * e, start[1] + dPhi * e, 0];
      rotationRef.current = r;
      setRotation(r);
      if (k < 1) requestAnimationFrame(step);
    };
    requestAnimationFrame(step);
  }, []);

  // Centralised: select a country feature, show info card, rotate to face it
  const selectCountry = useCallback((feature) => {
    if (!feature) return;
    const centroid = d3.geoCentroid(feature);
    const name = feature.properties.name;
    const info = (window.COUNTRIES || {})[name.toLowerCase()] || {};
    // Pull out fields we render specially; the rest become generic detail rows.
    const { capital = "—", iso2, fun_facts, timezone, ...extras } = info;
    const fact = Array.isArray(fun_facts) && fun_facts.length
      ? fun_facts[Math.floor(Math.random() * fun_facts.length)]
      : null;
    const time = timeInfoFor(timezone);
    setSelected({
      id: feature.id,
      name,
      capital,
      iso2: iso2 ? String(iso2).toLowerCase() : null,
      fact,
      time,
      extras,
      lon: centroid[0],
      lat: centroid[1],
      continent: inferContinent(centroid[0], centroid[1]),
    });
    rotateToCoords(centroid);
    pauseUntilRef.current = performance.now() + 20000;
    if (clearTimerRef.current) clearTimeout(clearTimerRef.current);
    clearTimerRef.current = setTimeout(() => {
      setSelected(null);
      clearTimerRef.current = null;
    }, 20000);
  }, [rotateToCoords]);

  selectCountryRef.current = selectCountry;

  // Pick a random country and fly to it.
  const randomCountry = useCallback(() => {
    if (!world || !world.features.length) return;
    // Prefer countries with at least basic data; fall back to any feature.
    const C = window.COUNTRIES || {};
    const withData = world.features.filter((f) => {
      const info = C[f.properties.name.toLowerCase()];
      return info && info.capital && info.capital !== "—";
    });
    const pool = withData.length ? withData : world.features;
    const pick = pool[Math.floor(Math.random() * pool.length)];
    selectCountry(pick);
  }, [world, selectCountry]);

  const onBgClick = (e) => {
    // Only treat as a "click background to clear" if the click was on the
    // stage backdrop itself — not on the globe SVG, country card, hud, etc.
    // (Country selection is handled directly in pointerup hit-testing.)
    if (e.target !== e.currentTarget) return;
    if (!interactingRef.current) setSelected(null);
  };

  // Escape closes any open popup/dialog: country card + quiz overlay.
  // Also cancels the country card's auto-clear timer and resumes rotation.
  useEffect(() => {
    const onKey = (e) => {
      if (e.key !== "Escape") return;
      if (clearTimerRef.current) {
        clearTimeout(clearTimerRef.current);
        clearTimerRef.current = null;
      }
      pauseUntilRef.current = 0;
      setSelected(null);
      setQuizOpen(false);
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, []);

  // Sync palette CSS vars
  useEffect(() => {
    document.documentElement.style.setProperty("--accent", accent);
    document.documentElement.style.setProperty("--accent-soft", soft);
  }, [accent, soft]);

  return (
    <div className="stage" onClick={onBgClick}>
      <Starfield count={260} />
      <div className="globe-wrap" ref={wrapRef}>
        {!world && !error && (
          <div className="loading"><span className="pulse"></span>Loading countries…</div>
        )}
        {error && <div className="loading" style={{color:"#ff8a8a"}}>Couldn't load world data</div>}
        <svg ref={svgRef} className="globe" viewBox={`0 0 ${size} ${size}`}
             onClick={(e) => e.stopPropagation()} />

        <div className={"country-card" + (selected ? " visible" : "")}>
          {selected && (
            <>
              <button className="card-close" aria-label="Close"
                      onClick={() => {
                        if (clearTimerRef.current) {
                          clearTimeout(clearTimerRef.current);
                          clearTimerRef.current = null;
                        }
                        pauseUntilRef.current = 0;
                        setSelected(null);
                      }}>×</button>
              <div className="card-head">
                {selected.iso2 && (
                  <img className="flag" alt=""
                       src={`https://flagcdn.com/w80/${selected.iso2}.png`}
                       srcSet={`https://flagcdn.com/w160/${selected.iso2}.png 2x`}
                       onError={(e) => { e.currentTarget.style.display = 'none'; }} />
                )}
                <div className="card-head-text">
                  <div className="label">Country</div>
                  <div className="name">{selected.name}</div>
                </div>
              </div>
              <div className="capital-row">
                <svg viewBox="0 0 16 16" width="13" height="13" aria-hidden="true">
                  <path fill="currentColor" d="M8 1.2a3.6 3.6 0 0 0-3.6 3.6c0 2.6 3.6 8 3.6 8s3.6-5.4 3.6-8A3.6 3.6 0 0 0 8 1.2zm0 5a1.4 1.4 0 1 1 0-2.8 1.4 1.4 0 0 1 0 2.8z"/>
                </svg>
                <span className="capital-label">Capital</span>
                <span className="capital-name">{selected.capital}</span>
              </div>
              {selected.extras && Object.keys(selected.extras).length > 0 && (
                <div className="details">
                  {Object.entries(selected.extras).map(([k, v]) => (
                    <div className="detail-row" key={k}>
                      <span className="detail-label">{fieldLabel(k)}</span>
                      <span className="detail-value">{formatFieldValue(v)}</span>
                    </div>
                  ))}
                </div>
              )}
              {selected.time && (
                <div className="time-block">
                  <div className="time-row">
                    <span className="time-icon" aria-hidden="true">🕒</span>
                    <span className="time-now">{selected.time.local}</span>
                    <span className="time-utc">{selected.time.utc}</span>
                  </div>
                  <div className="time-diff">{selected.time.diff}</div>
                </div>
              )}
              {selected.fact && (
                <div className="fun-fact">
                  <span className="fact-tag">Did you know?</span>
                  <span className="fact-text">{selected.fact}</span>
                </div>
              )}
              <div className="meta">
                <span>Continent <b>{selected.continent}</b></span>
                <span>{formatLatLon(selected.lon, selected.lat)}</span>
              </div>
            </>
          )}
        </div>
      </div>

      <div className="hud">
        <div className="top-left">
          <div className="brand-mark"></div>
          <div className="brand-text">
            <div className="eyebrow">Explore the World</div>
          </div>
        </div>
        <div className="top-center">
          <SearchBox world={world} onPick={selectCountry} />
        </div>
        {hoverInfo && (
          <div className="top-right">
            {hoverInfo.kind === "country" ? (
              <>
                <div className="hover-name">{hoverInfo.name}</div>
                <div className="hover-sub">{hoverInfo.continent}</div>
              </>
            ) : (
              <>
                <div className="hover-name">🌊 {hoverInfo.name}</div>
              </>
            )}
            <div className="coord">{formatLatLon(hoverInfo.lon, hoverInfo.lat)}</div>
          </div>
        )}
        <div className="right-rail">
          <div className="play-bar">
            <button className="play-btn primary" onClick={randomCountry} title="Spin the globe to a random country">
              <span className="icon">🎲</span><span>Random</span>
            </button>
            <button className="play-btn" onClick={() => setQuizOpen(true)} title="Test your geography">
              <span className="icon">🎯</span><span>Play Quiz</span>
            </button>
          </div>
          <ul className="hint">
            <li>Drag to spin</li>
            <li>Click a country for details</li>
            <li>🎲 Random — discover somewhere new</li>
            <li>🎯 Play Quiz — test what you know</li>
            <li>Press <kbd>Esc</kbd> to clear</li>
          </ul>
        </div>
        <div className="bottom-right">
          <div>Countries</div>
          <div className="count">{world ? world.features.length : "—"}</div>
        </div>
      </div>

      {/* Inline controls — always visible, sits above the Countries count.
          Replaces the host-toggled TweaksPanel so the colour picker can't be
          accidentally dismissed. */}
      <div className="controls-panel">
        <div className="controls-chips" role="radiogroup" aria-label="Theme color">
          {ACCENT_OPTIONS.map((c) => (
            <button key={c} type="button"
              className={"chip" + (accent.toLowerCase() === c.toLowerCase() ? " is-on" : "")}
              style={{ background: c }}
              aria-checked={accent.toLowerCase() === c.toLowerCase()}
              onClick={() => setTweak("accent", c)}
              title={c} />
          ))}
        </div>
        <div className="controls-row">
          <span className="controls-lbl">Auto-rotate</span>
          <button type="button"
            className={"controls-toggle" + (t.autoRotate ? " is-on" : "")}
            role="switch" aria-checked={!!t.autoRotate}
            onClick={() => setTweak("autoRotate", !t.autoRotate)}>
            <i />
          </button>
        </div>
        <div className="controls-row controls-row-col">
          <div className="controls-lbl-row">
            <span className="controls-lbl">Speed</span>
            <span className="controls-val">{t.rotateSpeed.toFixed(2)}</span>
          </div>
          <input type="range" className="controls-slider"
            min={0} max={0.3} step={0.01}
            value={t.rotateSpeed}
            onChange={(e) => setTweak("rotateSpeed", Number(e.target.value))} />
        </div>
        <div className="controls-row">
          <span className="controls-lbl">Lat / Long grid</span>
          <button type="button"
            className={"controls-toggle" + (t.showGraticule ? " is-on" : "")}
            role="switch" aria-checked={!!t.showGraticule}
            onClick={() => setTweak("showGraticule", !t.showGraticule)}>
            <i />
          </button>
        </div>
      </div>

      {quizOpen && <QuizOverlay world={world} onClose={() => setQuizOpen(false)} />}
    </div>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<GlobeApp />);
