// app.jsx — Charter Day Trail root: geo utilities, screen routing, D-pad controller.

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

// ============================================================
// GEO UTILITIES
// ============================================================
function haversine(aLat, aLng, bLat, bLng) {
  const R = 6371000;
  const dLat = (bLat - aLat) * Math.PI / 180;
  const dLng = (bLng - aLng) * Math.PI / 180;
  const a = Math.sin(dLat / 2) ** 2
    + Math.cos(aLat * Math.PI / 180) * Math.cos(bLat * Math.PI / 180) * Math.sin(dLng / 2) ** 2;
  return 2 * R * Math.asin(Math.sqrt(a));
}

function bearingTo(aLat, aLng, bLat, bLng) {
  const y = Math.sin((bLng - aLng) * Math.PI / 180) * Math.cos(bLat * Math.PI / 180);
  const x = Math.cos(aLat * Math.PI / 180) * Math.sin(bLat * Math.PI / 180)
    - Math.sin(aLat * Math.PI / 180) * Math.cos(bLat * Math.PI / 180) * Math.cos((bLng - aLng) * Math.PI / 180);
  return (Math.atan2(y, x) * 180 / Math.PI + 360) % 360;
}

function fmtDist(m) {
  return m < 950 ? Math.round(m / 5) * 5 + " m" : (m / 1609.34).toFixed(1) + " mi";
}

function fmtEta(m) {
  return Math.max(1, Math.round(m / 1.35 / 60)) + " min";
}

function compassPoint(b) {
  return ["N","NE","E","SE","S","SW","W","NW"][Math.round(b / 45) % 8];
}

function maneuverInfo(m, isFirst) {
  switch (m) {
    case "left":        return { verb: "Turn left onto",  angle: -78 };
    case "right":       return { verb: "Turn right onto", angle:  78 };
    case "slight-left": return { verb: "Bear left onto",  angle: -36 };
    case "slight-right":return { verb: "Bear right onto", angle:  36 };
    case "arrive":      return { verb: "Arrive at",        angle:   0, arrive: true };
    default:            return { verb: isFirst ? "Head along" : "Continue on", angle: 0 };
  }
}

function computeNav(stop, userPos) {
  const SIM = { lat: 37.78050, lng: -122.41450 };
  const u = userPos || SIM;
  const c = window.COORDS && window.COORDS[stop.id];
  if (!c) return null;
  const dist = haversine(u.lat, u.lng, c[0], c[1]);
  const brg  = bearingTo(u.lat, u.lng, c[0], c[1]);
  const route = (window.ROUTES && window.ROUTES[stop.id]) || [];
  const steps = route.map((st, i) => {
    const mi = maneuverInfo(st.m, i === 0);
    return {
      text:      mi.verb + " " + st.street,
      dist:      st.dist || "",
      transform: `rotate(${mi.angle}deg)`,
      arrive:    !!mi.arrive,
      isTurn:    !mi.arrive
    };
  });
  const first = route.length ? maneuverInfo(route[0].m, true) : { angle: 0 };
  const laneAngle = Math.max(-42, Math.min(42, first.angle));
  return {
    distLabel:         fmtDist(dist),
    etaLabel:          fmtEta(dist),
    compass:           compassPoint(brg),
    instruction:       steps.length ? steps[0].text : "Continue to destination",
    nextDist:          route.length ? (route[0].dist || "") : "",
    currentStreet:     route.length ? route[0].street : "",
    steps,
    maneuverTransform: `rotate(${first.angle}deg)`,
    laneStyle: {
      transform: `translateX(-50%) rotate(${laneAngle}deg)`,
      transformOrigin: "bottom center",
    }
  };
}

function nearestStopId(userPos) {
  if (!window.STOPS || !window.COORDS) return null;
  const SIM = { lat: 37.78050, lng: -122.41450 };
  const u = userPos || SIM;
  let bestId = null, bestDist = Infinity;
  for (const stop of window.STOPS) {
    const c = window.COORDS[stop.id];
    if (!c) continue;
    const d = haversine(u.lat, u.lng, c[0], c[1]);
    if (d < bestDist) { bestDist = d; bestId = stop.id; }
  }
  return bestId;
}

// Expose for use in screen components
window.computeNav    = computeNav;
window.nearestStopId = nearestStopId;
window.fmtDist       = fmtDist;
window.fmtEta        = fmtEta;

// ============================================================
// D-PAD CONTROLLER
// ============================================================
function useDpad(active, deps) {
  useEffect(() => {
    if (!active) return;
    const root = document.getElementById("app");
    if (!root) return;

    let items = [];
    let idx = 0;

    const query = () =>
      Array.from(root.querySelectorAll(".focusable")).filter((el) => el.offsetParent !== null);

    const paint = () => items.forEach((el, i) => el.classList.toggle("is-focused", i === idx));
    const focusCurrent = (scroll) => {
      const el = items[idx];
      if (!el) return;
      try { el.focus({ preventScroll: true }); } catch (e) {}
      if (scroll) el.scrollIntoView({ block: "nearest" });
    };

    items = query();
    if (!items.length) return;
    idx = Math.max(0, items.findIndex((el) => el.hasAttribute("data-autofocus")));
    paint(); focusCurrent(true);

    const refresh = () => {
      const prev = items[idx] || null;
      const next = query();
      if (!next.length) return;
      const sameSet = next.length === items.length && next.every((el, i) => el === items[i]);
      items = next;
      if (sameSet) { idx = Math.min(idx, items.length - 1); paint(); return; }
      const af = items.findIndex((el) => el.hasAttribute("data-autofocus"));
      const survived = prev ? items.indexOf(prev) : -1;
      idx = af >= 0 ? af : survived >= 0 ? survived : 0;
      paint(); focusCurrent(true);
    };
    let pending = false;
    const mo = new MutationObserver(() => {
      if (pending) return;
      pending = true;
      requestAnimationFrame(() => { pending = false; refresh(); });
    });
    mo.observe(root, { childList: true, subtree: true });

    const goBack = () => { const b = root.querySelector("[data-back]"); if (b) b.click(); };
    const centerOf = (el) => { const r = el.getBoundingClientRect(); return { x: r.left + r.width / 2, y: r.top + r.height / 2 }; };

    const spatial = (dir) => {
      const cur = items[idx]; if (!cur) return false;
      const a = centerOf(cur);
      let best = -1, bestScore = Infinity;
      for (let i = 0; i < items.length; i++) {
        if (i === idx) continue;
        const b = centerOf(items[i]);
        const dx = b.x - a.x, dy = b.y - a.y;
        let along, cross;
        if      (dir === "down")  { if (dy <= 4)  continue; along = dy;  cross = Math.abs(dx); }
        else if (dir === "up")    { if (dy >= -4) continue; along = -dy; cross = Math.abs(dx); }
        else if (dir === "right") { if (dx <= 4)  continue; along = dx;  cross = Math.abs(dy); }
        else                      { if (dx >= -4) continue; along = -dx; cross = Math.abs(dy); }
        const score = along + cross * 2.2;
        if (score < bestScore) { bestScore = score; best = i; }
      }
      if (best >= 0) { idx = best; paint(); focusCurrent(true); return true; }
      return false;
    };

    const hasLeftNeighbor = () => {
      const cur = items[idx]; if (!cur) return false;
      const a = centerOf(cur);
      const tol = Math.max(20, cur.getBoundingClientRect().height / 2);
      return items.some((el, i) => {
        if (i === idx) return false;
        const b = centerOf(el);
        return b.x - a.x < -4 && Math.abs(b.y - a.y) < tol;
      });
    };

    const onKey = (e) => {
      const cur = items[idx];
      const scroller = cur && cur.hasAttribute("data-scroll") ? cur : null;
      const backOnLeft = !!root.querySelector("[data-back-on-left]");
      switch (e.key) {
        case "ArrowDown":
        case "ArrowUp": {
          const down = e.key === "ArrowDown";
          if (scroller && down  && scroller.scrollTop + scroller.clientHeight < scroller.scrollHeight - 2) { scroller.scrollTop += 90; e.preventDefault(); return; }
          if (scroller && !down && scroller.scrollTop > 2) { scroller.scrollTop -= 90; e.preventDefault(); return; }
          spatial(down ? "down" : "up"); e.preventDefault(); break;
        }
        case "ArrowLeft": {
          const edge = !backOnLeft && root.querySelector("[data-back-on-left-edge]");
          if (backOnLeft || (edge && !hasLeftNeighbor())) goBack();
          else spatial("left");
          e.preventDefault(); break;
        }
        case "ArrowRight":
          if (root.querySelector("[data-right-next]")) {
            idx = (idx + 1) % items.length; paint(); focusCurrent(true);
          } else if (!spatial("right") && root.querySelector("[data-right-to-back]")) {
            const bi = items.findIndex((el) => el.hasAttribute("data-back"));
            if (bi >= 0 && bi !== idx) { idx = bi; paint(); focusCurrent(true); }
          }
          e.preventDefault(); break;
        case "Enter":
        case " ":
          if (cur) cur.click(); e.preventDefault(); break;
        case "Escape":
        case "Backspace":
          goBack(); e.preventDefault(); break;
        default: break;
      }
    };

    window.addEventListener("keydown", onKey);
    return () => { window.removeEventListener("keydown", onKey); mo.disconnect(); };
  }, deps); // eslint-disable-line react-hooks/exhaustive-deps
}

// ============================================================
// APP
// ============================================================
function App() {
  const [screen, setScreen] = useState("home");
  const [activeStop, setActiveStop] = useState(null);
  const [activeVideo, setActiveVideo] = useState(null);
  const [videoDrawer, setVideoDrawer] = useState(null);
  const [userPos, setUserPos] = useState(null);
  const [arrivedStop, setArrivedStop] = useState(null);
  const announcedRef = useRef(new Set());

  const [claimed, setClaimed] = useState(() => {
    try { return new Set(JSON.parse(localStorage.getItem("un_trail_patches") || "[]")); }
    catch { return new Set(); }
  });

  const claimPatch = useCallback((id) => {
    setClaimed((prev) => {
      const next = new Set(prev);
      next.add(id);
      try { localStorage.setItem("un_trail_patches", JSON.stringify([...next])); } catch {}
      return next;
    });
  }, []);

  // Heading in a ref — updates do NOT trigger re-renders (avoids restarting chevron animation)
  const headingRef = useRef(null);

  // Start GPS + compass sensors
  useEffect(() => {
    let watchId = null;
    if (navigator.geolocation) {
      try {
        watchId = navigator.geolocation.watchPosition(
          (p) => {
            const pos = { lat: p.coords.latitude, lng: p.coords.longitude, acc: p.coords.accuracy };
            setUserPos(pos);
            for (const stop of (window.STOPS || [])) {
              if (announcedRef.current.has(stop.id)) continue;
              const c = window.COORDS && window.COORDS[stop.id];
              if (!c) continue;
              if (haversine(pos.lat, pos.lng, c[0], c[1]) <= 60) {
                announcedRef.current.add(stop.id);
                setArrivedStop(stop);
                break;
              }
            }
          },
          () => {},
          { enableHighAccuracy: true, maximumAge: 2000, timeout: 12000 }
        );
      } catch (e) {}
    }
    // Dev simulation: ?sim=<stop-id> fakes GPS arrival at that stop
    const simId = new URLSearchParams(location.search).get("sim");
    if (simId) {
      const c = window.COORDS && window.COORDS[simId];
      if (c) {
        setTimeout(() => {
          const pos = { lat: c[0], lng: c[1], acc: 5 };
          setUserPos(pos);
          if (!announcedRef.current.has(simId)) {
            const stop = (window.STOPS || []).find((s) => s.id === simId);
            if (stop) { announcedRef.current.add(simId); setArrivedStop(stop); }
          }
        }, 1200);
      }
    }

    const onOrient = (e) => {
      let h = null;
      if (typeof e.webkitCompassHeading === "number") h = e.webkitCompassHeading;
      else if (e.absolute && typeof e.alpha === "number") h = (360 - e.alpha) % 360;
      else if (typeof e.alpha === "number") h = (360 - e.alpha) % 360;
      if (h != null && !isNaN(h)) headingRef.current = h;
    };
    window.addEventListener("deviceorientationabsolute", onOrient, true);
    window.addEventListener("deviceorientation", onOrient, true);
    return () => {
      if (watchId != null && navigator.geolocation) navigator.geolocation.clearWatch(watchId);
      window.removeEventListener("deviceorientationabsolute", onOrient, true);
      window.removeEventListener("deviceorientation", onOrient, true);
    };
  }, []);

  const goHome     = useCallback(() => setScreen("home"), []);
  const goList     = useCallback(() => setScreen("list"), []);
  const goStop     = useCallback((stop) => { setActiveStop(stop); setScreen("detail"); }, []);
  const goNavigate = useCallback((stop) => { setActiveStop(stop); setScreen("navigate"); }, []);
  const goBack     = useCallback(() => {
    if (screen === "watch")    { setScreen("detail");  return; }
    if (screen === "navigate") { setScreen("detail");  return; }
    if (screen === "detail")   { setScreen("list");    return; }
    if (screen === "list")     { setScreen("home");    return; }
  }, [screen]);

  const watchVideo = useCallback((video, stopName) => {
    if (!video) return;
    if (window.innerWidth < 600) {
      setVideoDrawer({ video, stopName });
    } else {
      setActiveVideo({ video, stopName });
      setScreen("watch");
    }
  }, []);

  useDpad(true, [screen, activeStop && activeStop.id, arrivedStop && arrivedStop.id]);

  // Is the user physically within 60 m of the active stop (requires real GPS)?
  const isNearby = (() => {
    if (!activeStop || !userPos) return false;
    const c = window.COORDS && window.COORDS[activeStop.id];
    if (!c) return false;
    return haversine(userPos.lat, userPos.lng, c[0], c[1]) <= 60;
  })();

  let view = null;
  switch (screen) {
    case "home":
      view = <window.HomeScreen onStart={goList} />;
      break;
    case "list":
      view = <window.ListScreen onStop={goStop} onBack={goHome} userPos={userPos} claimed={claimed} />;
      break;
    case "detail":
      view = activeStop && (
        <window.DetailScreen
          stop={activeStop}
          userPos={userPos}
          onNavigate={goNavigate}
          onWatch={watchVideo}
          onBack={goList}
          claimed={claimed}
          onClaim={claimPatch}
          isNearby={isNearby}
        />
      );
      break;
    case "navigate":
      view = activeStop && (
        <window.NavigateScreen
          stop={activeStop}
          userPos={userPos}
          onBack={() => setScreen("detail")}
        />
      );
      break;
    case "watch":
      view = activeVideo && (
        <window.WatchScreen
          video={activeVideo.video}
          stopName={activeVideo.stopName}
          onBack={() => setScreen("detail")}
        />
      );
      break;
    default:
      view = null;
  }

  return (
    <>
      {view}
      {videoDrawer && (
        <window.VideoDrawer
          video={videoDrawer.video}
          stopName={videoDrawer.stopName}
          onClose={() => setVideoDrawer(null)}
        />
      )}
      {arrivedStop && (
        <window.ArrivalOverlay
          stop={arrivedStop}
          isClaimed={claimed.has(arrivedStop.id)}
          onCollect={() => { claimPatch(arrivedStop.id); setArrivedStop(null); }}
          onDismiss={() => setArrivedStop(null)}
        />
      )}
    </>
  );
}

ReactDOM.createRoot(document.getElementById("app")).render(<App />);
