// Ambient backgrounds: topography (light) + night sky (dark)
// Both tied to viewer's lat/lon. Animates slowly.

const { useEffect, useRef, useState } = React;

// ────────────────────────────────────────────────────────────────
// Geolocation hook — asks once, caches, falls back to Edinburgh
// ────────────────────────────────────────────────────────────────
const EDINBURGH = { lat: 55.9533, lon: -3.1883, label: 'Edinburgh' };

function useGeolocation() {
  const [loc, setLoc] = useState(() => {
    try {
      const cached = localStorage.getItem('jv_geo');
      if (cached) {
        const parsed = JSON.parse(cached);
        // Only trust the cache if it carries a real fix from the device
        // (label === 'you'). The previous version cached the Edinburgh
        // fallback under the same key, which caused the "always Edinburgh"
        // bug if the prompt ever timed out or was denied.
        if (parsed && parsed.label === 'you' && typeof parsed.lat === 'number' && typeof parsed.lon === 'number') {
          return parsed;
        }
      }
    } catch {}
    return EDINBURGH;
  });

  // Clean up the legacy lock flag from earlier builds. If a previous
  // visit ever timed out or was denied, jv_geo_asked stuck at "1" and
  // blocked every future attempt. Removing it lets the hook ask again
  // on the next mount when no real fix is cached.
  useEffect(() => {
    try { localStorage.removeItem('jv_geo_asked'); } catch {}
  }, []);

  useEffect(() => {
    if (!navigator.geolocation) return;
    // If we already have a real fix cached, don't re-ask.
    if (loc && loc.label === 'you') return;

    let cancelled = false;
    navigator.geolocation.getCurrentPosition(
      (pos) => {
        if (cancelled) return;
        const next = { lat: pos.coords.latitude, lon: pos.coords.longitude, label: 'you' };
        setLoc(next);
        try { localStorage.setItem('jv_geo', JSON.stringify(next)); } catch {}
      },
      (err) => {
        // Permission denied / timeout / unavailable — keep the Edinburgh
        // fallback for this session and try again on the next page load.
        // No persistent "don't ask again" flag.
        if (err && err.code === err.PERMISSION_DENIED) {
          // User actively said no — respect for the rest of the tab session
          // by remembering it on the in-memory ref, but not in storage.
        }
      },
      { enableHighAccuracy: false, timeout: 15000, maximumAge: 24 * 3600 * 1000 }
    );
    return () => { cancelled = true; };
  }, []);

  return loc;
}

// ────────────────────────────────────────────────────────────────
// Seeded PRNG so the same location gives the same horizon each visit
// ────────────────────────────────────────────────────────────────
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 seedFromLoc(lat, lon) {
  return Math.floor(Math.abs(lat * 1000) + Math.abs(lon * 1000) * 7);
}

// ────────────────────────────────────────────────────────────────
// Reverse-geocode (Nominatim, keyless) — cached in localStorage
// ────────────────────────────────────────────────────────────────
function usePlaceName(loc) {
  const [place, setPlace] = useState(() => {
    try {
      const cached = localStorage.getItem('jv_place');
      if (cached) {
        const p = JSON.parse(cached);
        // only trust cache if coords roughly match
        if (Math.abs(p.lat - loc.lat) < 0.05 && Math.abs(p.lon - loc.lon) < 0.05) return p.name;
      }
    } catch {}
    return loc.label || null;
  });
  useEffect(() => {
    // Edinburgh fallback already labelled
    if (loc.label && loc.label !== 'you') { setPlace(loc.label); return; }
    let cancelled = false;
    const url = `https://nominatim.openstreetmap.org/reverse?format=json&lat=${loc.lat}&lon=${loc.lon}&zoom=10`;
    fetch(url, { headers: { 'Accept-Language': 'en' } })
      .then(r => r.ok ? r.json() : null)
      .then(d => {
        if (cancelled || !d) return;
        const a = d.address || {};
        const name = a.city || a.town || a.village || a.suburb || a.county || a.state || d.display_name?.split(',')[0];
        if (name) {
          setPlace(name);
          try { localStorage.setItem('jv_place', JSON.stringify({ lat: loc.lat, lon: loc.lon, name })); } catch {}
        }
      })
      .catch(() => {});
    return () => { cancelled = true; };
  }, [loc.lat, loc.lon, loc.label]);
  return place || 'your location';
}

// ────────────────────────────────────────────────────────────────
// Landmark data — curated per city, generic fallback otherwise
// ────────────────────────────────────────────────────────────────
// Each landmark: { name, bearing (deg, 0=N, 90=E), prominence (0-1), dist (0-1 near-far) }
const LANDMARKS_BY_CITY = {
  edinburgh: [
    { name: "Arthur's Seat",    bearing: 110, prominence: 1.0, dist: 0.3 },
    { name: "Calton Hill",      bearing:  85, prominence: 0.55, dist: 0.45 },
    { name: "Castle Rock",      bearing: 255, prominence: 0.85, dist: 0.35 },
    { name: "Blackford Hill",   bearing: 185, prominence: 0.5, dist: 0.55 },
    { name: "Pentland Hills",   bearing: 205, prominence: 0.9, dist: 0.85 },
    { name: "Braid Hills",      bearing: 195, prominence: 0.45, dist: 0.7 },
    { name: "Corstorphine Hill", bearing: 280, prominence: 0.55, dist: 0.6 },
    { name: "Fife Coast",       bearing:   5, prominence: 0.4, dist: 0.95 },
    { name: "Firth of Forth",   bearing:  30, prominence: 0.15, dist: 0.9 },
    { name: "Salisbury Crags",  bearing: 120, prominence: 0.7, dist: 0.3 },
  ],
  london: [
    { name: "The Shard",        bearing: 120, prominence: 0.95, dist: 0.3 },
    { name: "St Paul's",        bearing:  90, prominence: 0.6, dist: 0.35 },
    { name: "BT Tower",         bearing:  20, prominence: 0.7, dist: 0.45 },
    { name: "Canary Wharf",     bearing:  95, prominence: 0.85, dist: 0.55 },
    { name: "Parliament Hill",  bearing: 350, prominence: 0.5, dist: 0.6 },
    { name: "Crystal Palace",   bearing: 180, prominence: 0.6, dist: 0.75 },
    { name: "Wembley Arch",     bearing: 300, prominence: 0.55, dist: 0.7 },
  ],
  'new york': [
    { name: "Empire State",     bearing: 190, prominence: 0.95, dist: 0.3 },
    { name: "One World",        bearing: 200, prominence: 0.95, dist: 0.5 },
    { name: "Brooklyn Bridge",  bearing: 160, prominence: 0.45, dist: 0.55 },
    { name: "Central Park",     bearing:   0, prominence: 0.3, dist: 0.4 },
    { name: "Chrysler Building",bearing: 160, prominence: 0.75, dist: 0.35 },
  ],
  'san francisco': [
    { name: "Golden Gate",      bearing: 305, prominence: 0.85, dist: 0.45 },
    { name: "Twin Peaks",       bearing: 220, prominence: 0.9, dist: 0.35 },
    { name: "Bay Bridge",       bearing:  95, prominence: 0.5, dist: 0.55 },
    { name: "Mount Tamalpais",  bearing: 340, prominence: 0.95, dist: 0.85 },
    { name: "Mount Diablo",     bearing:  65, prominence: 0.9, dist: 0.95 },
    { name: "Alcatraz",         bearing:  20, prominence: 0.25, dist: 0.6 },
  ],
};
function landmarksFor(placeName) {
  if (!placeName) return null;
  const key = placeName.toLowerCase();
  for (const k of Object.keys(LANDMARKS_BY_CITY)) {
    if (key.includes(k)) return LANDMARKS_BY_CITY[k];
  }
  return null;
}

// ────────────────────────────────────────────────────────────────
// LIGHT BG: topographic horizon that pans 360°
// ────────────────────────────────────────────────────────────────
// Generates three layered horizon silhouettes using seeded noise,
// overlaid on concentric contour rings. The silhouettes shift
// horizontally over ~8 min for a 360° pan.
function TopographyBackground({ loc }) {
  const canvasRef = useRef(null);
  const rafRef = useRef(null);
  const placeName = usePlaceName(loc);
  const landmarks = landmarksFor(placeName);

  // Bearing is lifted to state so compass clicks can drive it.
  // Auto-drifts 360° over ~30 min. Clicking compass tweens to a target.
  const [bearing, setBearing] = useState(0); // 0..360
  const bearingRef = useRef(0);
  const targetRef = useRef(null); // {from, to, startT, dur} when tweening
  const autoSpeedRef = useRef(360 / (30 * 60 * 1000)); // deg per ms

  // Active landmark (hovered or under the cursor bearing)
  const [activeLandmark, setActiveLandmark] = useState(null);

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext('2d');
    const dpr = Math.min(window.devicePixelRatio || 1, 2);

    const resize = () => {
      const w = window.innerWidth;
      const h = window.innerHeight;
      canvas.width = w * dpr;
      canvas.height = h * dpr;
      canvas.style.width = w + 'px';
      canvas.style.height = h + 'px';
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    };
    resize();
    let resizeRaf = 0;
    const onResize = () => {
      if (resizeRaf) return;
      resizeRaf = requestAnimationFrame(() => {
        resizeRaf = 0;
        resize();
      });
    };
    window.addEventListener('resize', onResize);

    const seed = seedFromLoc(loc.lat, loc.lon);
    const rand = mulberry32(seed);

    // Build 3 horizon profiles (far → near), each a dense array of heights 0..1
    const PROFILE_LEN = 720;
    const buildProfile = (octaves, roughness, peakiness) => {
      const arr = new Array(PROFILE_LEN).fill(0);
      for (let o = 0; o < octaves; o++) {
        const freq = 2 ** (o + 1);
        const amp = roughness / (o + 1);
        const phase = rand() * Math.PI * 2;
        for (let i = 0; i < PROFILE_LEN; i++) {
          const t = (i / PROFILE_LEN) * Math.PI * 2 * freq;
          arr[i] += Math.sin(t + phase) * amp;
        }
      }
      let min = Infinity, max = -Infinity;
      for (const v of arr) { if (v < min) min = v; if (v > max) max = v; }
      for (let i = 0; i < arr.length; i++) {
        const n = (arr[i] - min) / (max - min || 1);
        arr[i] = Math.pow(n, peakiness);
      }
      return arr;
    };

    // Stamp curated landmarks onto the nearest profile so peaks align with bearing
    const profiles = [
      { data: buildProfile(5, 1, 1.4), color: 'rgba(23,22,26,0.08)', baseY: 0.82, heightPx: 180, speed: 0.25, isLandmarkLayer: false },
      { data: buildProfile(6, 1, 1.2), color: 'rgba(23,22,26,0.14)', baseY: 0.86, heightPx: 160, speed: 0.5, isLandmarkLayer: false },
      { data: buildProfile(7, 1, 1.0), color: 'rgba(23,22,26,0.22)', baseY: 0.92, heightPx: 140, speed: 1.0, isLandmarkLayer: true },
    ];

    if (landmarks) {
      // Nearest profile (landmark layer) — blend a peak at each landmark's bearing
      const near = profiles[2];
      for (const lm of landmarks) {
        const center = (lm.bearing / 360) * PROFILE_LEN;
        const width = 28 + lm.prominence * 22;
        for (let off = -width; off <= width; off++) {
          const idx = ((Math.round(center + off) % PROFILE_LEN) + PROFILE_LEN) % PROFILE_LEN;
          const k = Math.cos((off / width) * Math.PI * 0.5); // smooth bump
          const bump = k * k * lm.prominence * 0.6;
          near.data[idx] = Math.max(near.data[idx], Math.min(1, near.data[idx] * 0.4 + bump));
        }
      }
    }

    const sample = (data, t) => {
      const x = ((t % 1) + 1) % 1 * data.length;
      const i = Math.floor(x);
      const f = x - i;
      const a = data[i];
      const b = data[(i + 1) % data.length];
      return a + (b - a) * f;
    };

    let lastNow = performance.now();

    const draw = (now) => {
      const w = canvas.width / dpr;
      const h = canvas.height / dpr;
      const dt = now - lastNow;
      lastNow = now;

      // ── Update bearing: tween if active, otherwise drift
      if (targetRef.current) {
        const t = targetRef.current;
        const p = Math.min(1, (now - t.startT) / t.dur);
        const eased = 1 - Math.pow(1 - p, 3);
        bearingRef.current = (t.from + (t.to - t.from) * eased + 360) % 360;
        if (p >= 1) { targetRef.current = null; }
      } else {
        bearingRef.current = (bearingRef.current + autoSpeedRef.current * dt) % 360;
      }
      // Reflect into React state at ~5Hz for label updates
      if (Math.abs(bearingRef.current - bearing) > 0.5) setBearing(bearingRef.current);

      const panT = bearingRef.current / 360;

      ctx.clearRect(0, 0, w, h);

      // ── Concentric contour rings
      const cx = w / 2;
      const cy = h * 0.78;
      ctx.strokeStyle = 'rgba(23,22,26,0.06)';
      ctx.lineWidth = 1;
      for (let r = 60; r < Math.max(w, h) * 1.2; r += 42) {
        const jitter = Math.sin((r + now * 0.0002) * 0.03) * 4;
        ctx.beginPath();
        ctx.ellipse(cx, cy, r + jitter, (r + jitter) * 0.42, 0, 0, Math.PI * 2);
        ctx.stroke();
      }

      // ── Radial compass ticks
      ctx.strokeStyle = 'rgba(23,22,26,0.05)';
      for (let a = 0; a < 360; a += 15) {
        const rad = (a * Math.PI) / 180;
        ctx.beginPath();
        ctx.moveTo(cx + Math.cos(rad) * 100, cy + Math.sin(rad) * 42);
        ctx.lineTo(cx + Math.cos(rad) * (w * 0.9), cy + Math.sin(rad) * (h * 0.6));
        ctx.stroke();
      }

      // ── Horizon silhouettes (back → front) + collect landmark label positions
      const landmarkHits = [];
      for (let pi = 0; pi < profiles.length; pi++) {
        const p = profiles[pi];
        ctx.fillStyle = p.color;
        ctx.beginPath();
        const baseY = h * p.baseY;
        const step = 4;
        ctx.moveTo(0, h);
        for (let x = 0; x <= w; x += step) {
          const t = panT * p.speed + x / w / 4;
          const v = sample(p.data, t);
          const y = baseY - v * p.heightPx;
          ctx.lineTo(x, y);
        }
        ctx.lineTo(w, h);
        ctx.closePath();
        ctx.fill();

        ctx.strokeStyle = p.color.replace(/[\d.]+\)$/, '0.35)');
        ctx.lineWidth = 0.6;
        ctx.beginPath();
        for (let x = 0; x <= w; x += step) {
          const t = panT * p.speed + x / w / 4;
          const v = sample(p.data, t);
          const y = baseY - v * p.heightPx;
          if (x === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
        }
        ctx.stroke();

        // For the landmark layer, compute screen positions of visible landmarks
        if (p.isLandmarkLayer && landmarks) {
          for (const lm of landmarks) {
            // Convert landmark bearing → screen x.
            // At panT (heading), our view covers ~120° FOV centered on bearing.
            // Compute relative bearing in [-180..180]
            let rel = lm.bearing - bearingRef.current;
            rel = ((rel + 540) % 360) - 180;
            const FOV = 120;
            if (Math.abs(rel) > FOV / 2) continue;
            const sx = w / 2 + (rel / (FOV / 2)) * (w / 2);
            // Height from same profile sample at landmark's profile index
            const profT = lm.bearing / 360;
            const v = sample(p.data, profT);
            const sy = baseY - v * p.heightPx;
            landmarkHits.push({ ...lm, sx, sy, rel });
          }
        }
      }

      // Expose landmarks for DOM overlay render
      canvas.__landmarks = landmarkHits;

      rafRef.current = requestAnimationFrame(draw);
    };
    rafRef.current = requestAnimationFrame(draw);

    return () => {
      cancelAnimationFrame(rafRef.current);
      if (resizeRaf) cancelAnimationFrame(resizeRaf);
      window.removeEventListener('resize', onResize);
    };
  }, [loc.lat, loc.lon, landmarks]);

  // Poll landmarks from canvas to drive DOM labels at 10Hz (cheaper than React render each frame)
  const [landmarkPositions, setLandmarkPositions] = useState([]);
  useEffect(() => {
    if (!landmarks) { setLandmarkPositions([]); return; }
    const id = setInterval(() => {
      const lm = canvasRef.current && canvasRef.current.__landmarks;
      if (lm) setLandmarkPositions(lm);
    }, 100);
    return () => clearInterval(id);
  }, [landmarks]);

  // Compass click handler — tween to bearing
  const snapTo = (deg) => {
    const from = bearingRef.current;
    let to = ((deg % 360) + 360) % 360;
    // Pick shortest path
    let diff = to - from;
    if (diff > 180) diff -= 360;
    if (diff < -180) diff += 360;
    targetRef.current = { from, to: from + diff, startT: performance.now(), dur: 1800 };
  };

  // Landmark label click → snap to that bearing
  const snapToLandmark = (lm) => snapTo(lm.bearing);

  return (
    <>
      <canvas ref={canvasRef} className="ambient-bg ambient-topo" aria-hidden="true" />
      {/* Landmark labels (DOM, crisp text) */}
      <div className="landmark-layer" aria-hidden="true">
        {landmarkPositions.map((lm) => {
          const edgeFade = 1 - Math.min(1, Math.abs(lm.rel) / 55);
          return (
            <button
              key={lm.name}
              className="landmark-label"
              style={{
                left: lm.sx + 'px',
                top: (lm.sy - 14) + 'px',
                opacity: 0.35 + edgeFade * 0.55,
                fontSize: (10 + lm.prominence * 2) + 'px',
              }}
              onClick={() => snapToLandmark(lm)}
              onMouseEnter={() => setActiveLandmark(lm.name)}
              onMouseLeave={() => setActiveLandmark(null)}
              title={`Face ${lm.name} · ${Math.round(lm.bearing)}°`}
            >
              <span className="landmark-tick" />
              <span className="landmark-name">{lm.name}</span>
            </button>
          );
        })}
      </div>
      <PlaceAndCompass
        placeName={placeName}
        loc={loc}
        bearing={bearing}
        onSnap={snapTo}
        activeLandmark={activeLandmark}
      />
    </>
  );
}

// ────────────────────────────────────────────────────────────────
// DOM overlay: place name + coords + clickable compass
// ────────────────────────────────────────────────────────────────
function PlaceAndCompass({ placeName, loc, bearing, onSnap, activeLandmark }) {
  const cardinals = [
    { label: 'N', deg: 0 },
    { label: 'E', deg: 90 },
    { label: 'S', deg: 180 },
    { label: 'W', deg: 270 },
  ];
  // Closest cardinal to current bearing
  const current = (() => {
    const b = ((bearing % 360) + 360) % 360;
    if (b < 45 || b >= 315) return 'N';
    if (b < 135) return 'E';
    if (b < 225) return 'S';
    return 'W';
  })();
  const fmt = (v, pos, neg) => `${Math.abs(v).toFixed(2)}°${v >= 0 ? pos : neg}`;
  return (
    <div className="place-panel" aria-hidden="true">
      <div className="place-name">{placeName}</div>
      <div className="place-coords">
        {fmt(loc.lat, 'N', 'S')} · {fmt(loc.lon, 'E', 'W')}
      </div>
      <div className="compass" role="group" aria-label="compass">
        <div className="compass-ring">
          <div className="compass-needle" style={{ transform: `rotate(${bearing}deg)` }} />
          {cardinals.map((c) => (
            <button
              key={c.label}
              className={'compass-cardinal' + (c.label === current ? ' is-current' : '')}
              style={{ transform: `rotate(${c.deg}deg) translateY(-32px) rotate(${-c.deg}deg)` }}
              onClick={() => onSnap(c.deg)}
              title={`Face ${c.label}`}
            >{c.label}</button>
          ))}
        </div>
        <div className="compass-readout">
          <span className="compass-bearing">{Math.round(bearing)}°</span>
          <span className="compass-current">{activeLandmark || `looking ${current}`}</span>
        </div>
      </div>
    </div>
  );
}

// ────────────────────────────────────────────────────────────────
// DARK BG: real night sky for viewer's location
// ────────────────────────────────────────────────────────────────
// Uses a small catalog of ~80 brightest stars + a few constellations.
// Computes alt/az from lat/lon/time, projects to canvas stereographically.
// Adds slow sidereal rotation, occasional satellite passes + meteors.

// ── Star catalog: [name, RA hours, Dec deg, magnitude]
const STARS = [
  // Ursa Major / Big Dipper
  ['Dubhe', 11.062, 61.751, 1.79],
  ['Merak', 11.031, 56.382, 2.37],
  ['Phecda', 11.897, 53.695, 2.44],
  ['Megrez', 12.257, 57.033, 3.32],
  ['Alioth', 12.900, 55.960, 1.77],
  ['Mizar', 13.399, 54.925, 2.04],
  ['Alkaid', 13.792, 49.313, 1.86],
  // Cassiopeia
  ['Schedar', 0.675, 56.537, 2.24],
  ['Caph', 0.153, 59.150, 2.28],
  ['Gamma Cas', 0.945, 60.717, 2.47],
  ['Ruchbah', 1.430, 60.235, 2.68],
  ['Segin', 1.907, 63.670, 3.38],
  // Orion
  ['Betelgeuse', 5.919, 7.407, 0.50],
  ['Rigel', 5.242, -8.202, 0.13],
  ['Bellatrix', 5.418, 6.350, 1.64],
  ['Saiph', 5.796, -9.670, 2.06],
  ['Mintaka', 5.533, -0.299, 2.23],
  ['Alnilam', 5.603, -1.202, 1.69],
  ['Alnitak', 5.679, -1.943, 1.79],
  // Canis Major / Minor
  ['Sirius', 6.752, -16.716, -1.46],
  ['Procyon', 7.655, 5.225, 0.38],
  // Taurus
  ['Aldebaran', 4.598, 16.509, 0.85],
  ['Elnath', 5.438, 28.608, 1.65],
  // Auriga
  ['Capella', 5.278, 45.998, 0.08],
  // Gemini
  ['Castor', 7.577, 31.888, 1.58],
  ['Pollux', 7.755, 28.026, 1.14],
  // Leo
  ['Regulus', 10.139, 11.967, 1.35],
  ['Denebola', 11.818, 14.572, 2.14],
  ['Algieba', 10.333, 19.842, 2.08],
  // Virgo
  ['Spica', 13.420, -11.161, 1.04],
  // Bootes
  ['Arcturus', 14.261, 19.182, -0.05],
  // Corona Borealis
  ['Alphecca', 15.578, 26.715, 2.23],
  // Scorpius
  ['Antares', 16.490, -26.432, 1.09],
  // Lyra
  ['Vega', 18.615, 38.784, 0.03],
  // Cygnus
  ['Deneb', 20.690, 45.280, 1.25],
  ['Albireo', 19.512, 27.960, 3.05],
  ['Sadr', 20.370, 40.257, 2.23],
  // Aquila
  ['Altair', 19.846, 8.868, 0.76],
  // Pisces Austrinus
  ['Fomalhaut', 22.961, -29.622, 1.16],
  // Andromeda
  ['Alpheratz', 0.140, 29.090, 2.07],
  ['Mirach', 1.162, 35.621, 2.07],
  ['Almach', 2.065, 42.330, 2.27],
  // Perseus
  ['Mirfak', 3.405, 49.861, 1.79],
  ['Algol', 3.136, 40.956, 2.12],
  // Pegasus
  ['Markab', 23.079, 15.205, 2.48],
  ['Scheat', 23.063, 28.083, 2.42],
  ['Algenib', 0.221, 15.184, 2.83],
  // Crux (S. hemisphere)
  ['Acrux', 12.443, -63.099, 0.77],
  ['Mimosa', 12.795, -59.689, 1.25],
  ['Gacrux', 12.520, -57.113, 1.63],
  // Centaurus
  ['Rigil Kent', 14.660, -60.835, -0.27],
  ['Hadar', 14.064, -60.373, 0.61],
  // Southern fillers
  ['Canopus', 6.399, -52.695, -0.74],
  ['Achernar', 1.629, -57.237, 0.46],
  // Polaris
  ['Polaris', 2.530, 89.264, 1.97],
  // Extra bright
  ['Mira', 2.322, -2.977, 3.0],
  ['Hamal', 2.120, 23.462, 2.00],
  ['Diphda', 0.726, -17.987, 2.04],
  ['Ankaa', 0.438, -42.306, 2.40],
  // Hercules
  ['Rasalhague', 17.582, 12.560, 2.08],
  ['Kornephoros', 16.504, 21.490, 2.77],
  // Draco
  ['Eltanin', 17.943, 51.489, 2.24],
  ['Thuban', 14.073, 64.376, 3.67],
];

// Constellation lines — pairs of star indices into STARS array
const CONSTELLATIONS = [
  [0,1,2,3,4,5,6],          // Big Dipper path
  [7,8,9,10,11],            // Cassiopeia W
  [12,14,16,17,18,13],      // Orion outline-ish
  [16,17,18],               // Orion belt
  [23,24,25],               // Gemini
  [26,27,28],               // Leo sickle
  [38,39,40],               // Andromeda line
  [44,45,46],               // Pegasus corner
];

// ── Astronomy: RA/Dec → Alt/Az for observer at lat/lon at given date
function raDecToAltAz(raHours, decDeg, latDeg, lonDeg, date) {
  // Julian Date
  const jd = date.getTime() / 86400000 + 2440587.5;
  const d = jd - 2451545.0;
  // Greenwich Mean Sidereal Time (hours)
  let gmst = 18.697374558 + 24.06570982441908 * d;
  gmst = ((gmst % 24) + 24) % 24;
  // Local Sidereal Time
  const lst = (gmst + lonDeg / 15 + 24) % 24;
  // Hour angle (hours → deg)
  let ha = (lst - raHours) * 15;
  const rad = Math.PI / 180;
  const haR = ha * rad;
  const decR = decDeg * rad;
  const latR = latDeg * rad;
  const sinAlt =
    Math.sin(decR) * Math.sin(latR) + Math.cos(decR) * Math.cos(latR) * Math.cos(haR);
  const alt = Math.asin(sinAlt);
  const cosA =
    (Math.sin(decR) - Math.sin(alt) * Math.sin(latR)) / (Math.cos(alt) * Math.cos(latR));
  let az = Math.acos(Math.max(-1, Math.min(1, cosA)));
  if (Math.sin(haR) > 0) az = 2 * Math.PI - az;
  return { alt: alt / rad, az: az / rad };
}

function StarfieldBackground({ loc }) {
  const canvasRef = useRef(null);
  const rafRef = useRef(null);

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext('2d');
    const dpr = Math.min(window.devicePixelRatio || 1, 2);

    let w = 0, h = 0;
    const resize = () => {
      w = window.innerWidth;
      h = window.innerHeight;
      canvas.width = w * dpr;
      canvas.height = h * dpr;
      canvas.style.width = w + 'px';
      canvas.style.height = h + 'px';
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    };
    resize();
    let resizeRaf = 0;
    const onResize = () => {
      if (resizeRaf) return;
      resizeRaf = requestAnimationFrame(() => {
        resizeRaf = 0;
        resize();
      });
    };
    window.addEventListener('resize', onResize);

    // Stereographic-ish projection: center of canvas = zenith.
    // radius = (90 - alt) / 90 * R  (alt 90 → center, alt 0 → edge)
    // az=0 → up (north), az=90 → right (east)
    const project = (alt, az) => {
      if (alt < -5) return null; // below horizon (small pad for fade)
      const R = Math.min(w, h) * 0.55;
      const r = ((90 - alt) / 90) * R;
      const theta = (az - 180) * (Math.PI / 180); // rotate so N at bottom, S at top (we'll flip)
      const x = w / 2 + r * Math.sin(theta);
      const y = h / 2 - r * Math.cos(theta);
      return { x, y, r, alt };
    };

    // Satellite passes: spawn every ~15s, slow straight line across sky
    const satellites = [];
    const spawnSatellite = () => {
      const startAngle = Math.random() * Math.PI * 2;
      const endAngle = startAngle + (Math.random() - 0.5) * 1.2 + Math.PI;
      const R = Math.min(w, h) * 0.45;
      satellites.push({
        x0: w / 2 + Math.cos(startAngle) * R,
        y0: h / 2 + Math.sin(startAngle) * R,
        x1: w / 2 + Math.cos(endAngle) * R,
        y1: h / 2 + Math.sin(endAngle) * R,
        t0: performance.now(),
        duration: 14000 + Math.random() * 8000,
      });
    };

    // Meteors: spawn every ~40s, fast short streak
    const meteors = [];
    const spawnMeteor = () => {
      const x = Math.random() * w;
      const y = Math.random() * h * 0.7;
      const angle = Math.PI * 0.25 + (Math.random() - 0.5) * 0.3;
      meteors.push({
        x, y,
        dx: Math.cos(angle) * 200,
        dy: Math.sin(angle) * 200,
        t0: performance.now(),
        duration: 1200 + Math.random() * 600,
      });
    };

    let lastSat = performance.now();
    let lastMeteor = performance.now();

    // Speed up sidereal rotation so it's perceptible in a session.
    // Real: 360°/24h. Ours: use date + timeOffset that advances ~2000× real.
    const startReal = Date.now();

    const draw = (now) => {
      // Sim time: accelerate by factor so sky visibly rotates (~1° per 10s real)
      const simMs = (Date.now() - startReal) * 120;
      const simDate = new Date(Date.now() + simMs);

      // ── Background: very deep night blue with soft radial glow
      const grad = ctx.createRadialGradient(w / 2, h / 2, 40, w / 2, h / 2, Math.max(w, h) * 0.7);
      grad.addColorStop(0, 'rgba(14,18,34,0)');
      grad.addColorStop(1, 'rgba(6,8,18,0.55)');
      ctx.fillStyle = grad;
      ctx.fillRect(0, 0, w, h);

      // ── Horizon ring (circle at alt=0)
      const R = Math.min(w, h) * 0.55;
      ctx.strokeStyle = 'rgba(160,180,230,0.06)';
      ctx.lineWidth = 1;
      ctx.beginPath();
      ctx.arc(w / 2, h / 2, R, 0, Math.PI * 2);
      ctx.stroke();
      // Zenith tick
      ctx.fillStyle = 'rgba(160,180,230,0.14)';
      ctx.font = '10px "Geist Mono", ui-monospace, monospace';
      ctx.textAlign = 'center';
      ctx.fillText('ZENITH', w / 2, h / 2 - 4);
      ctx.fillText('•', w / 2, h / 2 + 6);

      // ── Compute star positions for this moment
      const positions = STARS.map(([name, ra, dec, mag]) => {
        const { alt, az } = raDecToAltAz(ra, dec, loc.lat, loc.lon, simDate);
        const p = project(alt, az);
        return p ? { ...p, name, mag } : null;
      });

      // ── Draw constellation lines first (faint)
      ctx.strokeStyle = 'rgba(170,195,240,0.12)';
      ctx.lineWidth = 0.6;
      for (const line of CONSTELLATIONS) {
        ctx.beginPath();
        let started = false;
        for (const idx of line) {
          const p = positions[idx];
          if (!p || p.alt < 0) { started = false; continue; }
          if (!started) { ctx.moveTo(p.x, p.y); started = true; }
          else ctx.lineTo(p.x, p.y);
        }
        ctx.stroke();
      }

      // ── Draw stars
      for (const p of positions) {
        if (!p) continue;
        // Fade near horizon
        const horizonFade = Math.min(1, Math.max(0, p.alt / 8));
        // Size + brightness from magnitude (lower mag = brighter)
        const bright = Math.max(0, (2.5 - p.mag) / 3.5);
        const size = 0.5 + bright * 2.4;
        const alpha = (0.35 + bright * 0.65) * horizonFade;

        // Soft glow
        if (bright > 0.55) {
          const g = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, size * 4);
          g.addColorStop(0, `rgba(230,235,250,${alpha * 0.35})`);
          g.addColorStop(1, 'rgba(230,235,250,0)');
          ctx.fillStyle = g;
          ctx.beginPath();
          ctx.arc(p.x, p.y, size * 4, 0, Math.PI * 2);
          ctx.fill();
        }
        // Core dot
        ctx.fillStyle = `rgba(240,243,252,${alpha})`;
        ctx.beginPath();
        ctx.arc(p.x, p.y, size, 0, Math.PI * 2);
        ctx.fill();
      }

      // ── Label a few brightest stars (subtle)
      ctx.fillStyle = 'rgba(180,200,235,0.32)';
      ctx.font = '9px "Geist Mono", ui-monospace, monospace';
      ctx.textAlign = 'left';
      const brightestVisible = positions
        .filter(p => p && p.alt > 10)
        .sort((a, b) => a.mag - b.mag)
        .slice(0, 4);
      for (const p of brightestVisible) {
        ctx.fillText(p.name.toUpperCase(), p.x + 6, p.y + 3);
      }

      // ── Satellites
      if (now - lastSat > 45000 + Math.random() * 30000) {
        spawnSatellite();
        lastSat = now;
      }
      for (let i = satellites.length - 1; i >= 0; i--) {
        const s = satellites[i];
        const t = (now - s.t0) / s.duration;
        if (t > 1) { satellites.splice(i, 1); continue; }
        const fade = t < 0.1 ? t / 0.1 : t > 0.9 ? (1 - t) / 0.1 : 1;
        const x = s.x0 + (s.x1 - s.x0) * t;
        const y = s.y0 + (s.y1 - s.y0) * t;
        ctx.fillStyle = `rgba(200,220,255,${0.6 * fade})`;
        ctx.beginPath();
        ctx.arc(x, y, 1.2, 0, Math.PI * 2);
        ctx.fill();
      }

      // ── Meteors
      if (now - lastMeteor > 90000 + Math.random() * 90000) {
        spawnMeteor();
        lastMeteor = now;
      }
      for (let i = meteors.length - 1; i >= 0; i--) {
        const m = meteors[i];
        const t = (now - m.t0) / m.duration;
        if (t > 1) { meteors.splice(i, 1); continue; }
        const cx = m.x + m.dx * t;
        const cy = m.y + m.dy * t;
        const tailLen = 60;
        const tailX = cx - Math.cos(Math.atan2(m.dy, m.dx)) * tailLen * (1 - t);
        const tailY = cy - Math.sin(Math.atan2(m.dy, m.dx)) * tailLen * (1 - t);
        const g = ctx.createLinearGradient(cx, cy, tailX, tailY);
        const alpha = Math.min(1, Math.sin(t * Math.PI));
        g.addColorStop(0, `rgba(255,245,220,${0.9 * alpha})`);
        g.addColorStop(1, 'rgba(255,245,220,0)');
        ctx.strokeStyle = g;
        ctx.lineWidth = 1.4;
        ctx.beginPath();
        ctx.moveTo(cx, cy);
        ctx.lineTo(tailX, tailY);
        ctx.stroke();
      }

      rafRef.current = requestAnimationFrame(draw);
    };
    rafRef.current = requestAnimationFrame(draw);

    return () => {
      cancelAnimationFrame(rafRef.current);
      if (resizeRaf) cancelAnimationFrame(resizeRaf);
      window.removeEventListener('resize', onResize);
    };
  }, [loc.lat, loc.lon]);

  return <canvas ref={canvasRef} className="ambient-bg ambient-sky" aria-hidden="true" />;
}

// ────────────────────────────────────────────────────────────────
// Combined background — switches by theme
// ────────────────────────────────────────────────────────────────
function AmbientBackground({ theme, enabled = true }) {
  const loc = useGeolocation();
  if (!enabled) return null;
  return theme === 'light'
    ? <TopographyBackground loc={loc} />
    : <StarfieldBackground loc={loc} />;
}

// Export to window so app.jsx can use them
Object.assign(window, { AmbientBackground, useGeolocation, usePlaceName });
