// London & Middlesex Food Map — main app
// React + Leaflet, hash-based routing, geolocation.
// All listing data comes from places.csv; categories from categories.csv.

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

const CENTER = window.DEFAULT_CENTER;

const catById = (id, cats) => cats.find(c => c.id === id);

// ── Haversine distance (km) ──
function distanceKm(a, b) {
  if (!a || !b) return null;
  if (a.lat == null || b.lat == null || isNaN(a.lat) || isNaN(b.lat)) return null;
  if (a.lng == null || b.lng == null || isNaN(a.lng) || isNaN(b.lng)) return null;
  const R = 6371;
  const toRad = d => d * Math.PI / 180;
  const dLat = toRad(b.lat - a.lat);
  const dLng = toRad(b.lng - a.lng);
  const lat1 = toRad(a.lat);
  const lat2 = toRad(b.lat);
  const x = Math.sin(dLat/2)**2 + Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLng/2)**2;
  return 2 * R * Math.asin(Math.sqrt(x));
}
function fmtKm(km) {
  if (km == null) return "";
  if (km < 1) return Math.round(km * 1000) + " m";
  return km.toFixed(1) + " km";
}

// Build a Google Maps URL that opens the actual business listing (not just a
// pin at coordinates). Resolution order:
//   1. googleCid    — deep-links to the exact business (most reliable)
//   2. googlePlaceId — exact match via Places API ID (ChIJ…)
//   3. name + address search — usually right, but can land on near-duplicates
//
// Getting a CID for the maintainer sheet:
//   Open the place in Google Maps → click Share → "Copy link" → the URL
//   contains `!1s0xAAAAAAAA:0xBBBBBBBB` — the second hex is the CID.
//   Convert with BigInt("0xBBBBBBBB").toString().
function gmapsBusinessUrl(place) {
  if (place.googleCid)     return `https://www.google.com/maps?cid=${place.googleCid}`;
  if (place.googlePlaceId) return `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(place.name)}&query_place_id=${place.googlePlaceId}`;
  return `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(place.name + ", " + place.address)}`;
}
function gmapsDirectionsUrl(place) {
  const base = "https://www.google.com/maps/dir/?api=1";
  if (place.googlePlaceId) {
    return `${base}&destination=${encodeURIComponent(place.name)}&destination_place_id=${place.googlePlaceId}`;
  }
  // Directions API doesn't accept CID, so fall back to coordinates — accurate
  // for routing even when the search result is ambiguous.
  return `${base}&destination=${place.lat}${encodeURIComponent(",")}${place.lng}`;
}

// ── hash routing ──
function parseHash() {
  const raw = (location.hash || "#/").replace(/^#\/?/, "");
  const parts = raw.split("/").filter(Boolean);
  return parts;
}
function useRoute() {
  const [route, setRoute] = useState(parseHash());
  useEffect(() => {
    const onChange = () => setRoute(parseHash());
    window.addEventListener("hashchange", onChange);
    return () => window.removeEventListener("hashchange", onChange);
  }, []);
  return route;
}
function nav(path) { location.hash = "#" + path; }

// ── responsive ──
function useIsDesktop() {
  const [is, setIs] = useState(() => window.matchMedia("(min-width: 900px)").matches);
  useEffect(() => {
    const mql = window.matchMedia("(min-width: 900px)");
    const h = e => setIs(e.matches);
    mql.addEventListener("change", h);
    return () => mql.removeEventListener("change", h);
  }, []);
  return is;
}

// ── geolocation hook ──
function useGeo() {
  const [loc, setLoc] = useState(() => {
    const cached = sessionStorage.getItem("user-loc");
    return cached ? JSON.parse(cached) : null;
  });
  const [status, setStatus] = useState("idle"); // idle | requesting | granted | denied | error
  const request = useCallback(() => {
    if (!navigator.geolocation) { setStatus("error"); return; }
    setStatus("requesting");
    navigator.geolocation.getCurrentPosition(
      pos => {
        const p = { lat: pos.coords.latitude, lng: pos.coords.longitude };
        setLoc(p);
        sessionStorage.setItem("user-loc", JSON.stringify(p));
        setStatus("granted");
      },
      () => {
        // fallback to London ON centre so the demo still feels real
        setLoc(CENTER);
        sessionStorage.setItem("user-loc", JSON.stringify(CENTER));
        setStatus("denied");
      },
      { timeout: 8000, maximumAge: 60_000 }
    );
  }, []);
  return { loc, status, request };
}

// ── toast ──
const ToastCtx = React.createContext(() => {});
function useToast() { return React.useContext(ToastCtx); }
function ToastProvider({ children }) {
  const [msg, setMsg] = useState(null);
  const t = useRef(null);
  const show = useCallback((m) => {
    setMsg(m);
    clearTimeout(t.current);
    t.current = setTimeout(() => setMsg(null), 2400);
  }, []);
  return (
    <ToastCtx.Provider value={show}>
      {children}
      <div className={"toast" + (msg ? " is-show" : "")}>{msg}</div>
    </ToastCtx.Provider>
  );
}

// ── Pin svg ──
function pinSvg(color, size = 28) {
  const w = size, h = Math.round(size * 1.33);
  return `<svg width="${w}" height="${h}" viewBox="0 0 24 32" xmlns="http://www.w3.org/2000/svg">
    <circle cx="12" cy="11" r="10" fill="${color}"/>
    <path d="M5 18 Q12 32 19 18" fill="${color}"/>
    <circle cx="12" cy="11" r="4" fill="white" opacity="0.9"/>
  </svg>`;
}
function buildIcon(color, opts = {}) {
  const size = opts.size ?? 28;
  const dim = opts.dim;
  const html = `<div class="pin${dim ? " pin-dimmed" : ""}${opts.active ? " is-active" : ""}">${pinSvg(color, size)}</div>`;
  return L.divIcon({
    html, className: "", iconSize: [size, Math.round(size*1.33)], iconAnchor: [size/2, Math.round(size*1.33)]
  });
}

// ── Filter chip row ──
function FilterBar({ active, onChange, includeAll = true, className = "", cats = [] }) {
  return (
    <div className={"filterbar " + className}>
      {includeAll && (
        <button className={"chip" + (active === "all" ? " is-active" : "")} onClick={() => onChange("all")}>All</button>
      )}
      {cats.map(c => (
        <button
          key={c.id}
          className={"chip" + (active === c.id ? " is-active" : "")}
          style={active === c.id ? { background: c.color, borderColor: c.color } : {}}
          onClick={() => onChange(c.id)}
        >{c.label}</button>
      ))}
    </div>
  );
}

// ── Place card ──
function PlaceCard({ place, userLoc, cats = [], active, onClick }) {
  const cat = catById(place.category, cats);
  const d = userLoc ? distanceKm(userLoc, place) : null;
  const pillStyle = cat ? { background: cat.soft, color: cat.color, borderColor: cat.border } : {};
  return (
    <button
      className={"card card-cat" + (active ? " is-active" : "")}
      style={{ borderLeftColor: cat?.color }}
      onClick={onClick}
    >
      <div className="card-row">
        <span className="card-title">{place.name}</span>
        <span className="pill" style={pillStyle}>{cat?.label}</span>
      </div>
      <span className="card-meta">{place.address}</span>
      <div className="card-row">
        <span className="card-faint">{place.hours}</span>
        {d != null && <span className="card-distance">{fmtKm(d)}</span>}
      </div>
    </button>
  );
}

// ── Map host (Leaflet) ──
function MapHost({ filter, userLoc, places, cats = [], activeId, onPinClick, focusId, fitAll, sheetState }) {
  const elRef = useRef(null);
  const mapRef = useRef(null);
  const layerRef = useRef(null);
  const userRef = useRef(null);

  // init
  useEffect(() => {
    if (mapRef.current) return;
    const map = L.map(elRef.current, {
      center: [CENTER.lat, CENTER.lng],
      zoom: 12,
      zoomControl: false,
      attributionControl: true,
      preferCanvas: true
    });
    L.tileLayer("https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png", {
      attribution: '© OpenStreetMap · CARTO',
      maxZoom: 19, subdomains: "abcd"
    }).addTo(map);
    layerRef.current = L.layerGroup().addTo(map);
    mapRef.current = map;
    setTimeout(() => map.invalidateSize(), 50);
  }, []);

  // resize on container changes
  useEffect(() => {
    const obs = new ResizeObserver(() => mapRef.current && mapRef.current.invalidateSize());
    if (elRef.current) obs.observe(elRef.current);
    return () => obs.disconnect();
  }, []);

  // update pins
  useEffect(() => {
    const layer = layerRef.current;
    if (!layer) return;
    layer.clearLayers();
    places.forEach(p => {
      const cat = catById(p.category, cats);
      const dim = filter !== "all" && filter !== p.category;
      const isActive = p.id === activeId;
      const icon = buildIcon(cat.color, { dim, active: isActive, size: isActive ? 34 : 28 });
      const m = L.marker([p.lat, p.lng], { icon, riseOnHover: true });
      m.on("click", () => onPinClick && onPinClick(p));
      m.addTo(layer);
    });
  }, [places, filter, activeId, onPinClick]);

  // user dot
  useEffect(() => {
    if (!mapRef.current) return;
    if (userRef.current) {
      mapRef.current.removeLayer(userRef.current);
      userRef.current = null;
    }
    if (userLoc) {
      const icon = L.divIcon({
        html: '<div class="user-dot"></div>',
        className: "", iconSize: [16, 16], iconAnchor: [8, 8]
      });
      userRef.current = L.marker([userLoc.lat, userLoc.lng], { icon, interactive: false }).addTo(mapRef.current);
    }
  }, [userLoc]);

  // focus
  useEffect(() => {
    if (!mapRef.current || !focusId) return;
    const p = places.find(x => x.id === focusId);
    if (p) mapRef.current.flyTo([p.lat, p.lng], 14, { duration: 0.6 });
  }, [focusId, places]);

  // fit all — keep the focus point in the visible strip above the sheet
  useEffect(() => {
    if (!mapRef.current || !fitAll) return;
    const visible = places.filter(p => filter === "all" || filter === p.category);
    if (!visible.length) return;

    const mapH = elRef.current?.offsetHeight ?? window.innerHeight;
    const sheetPx = sheetState === "detail" ? Math.round(mapH * 0.60)
                  : sheetState === "open"   ? Math.round(mapH * 0.50)
                  : 96;

    // Target screen-Y for the focus point: 20% from top when sheet is large
    // (above the ~28% bottom boundary), 38% when just peeking (more centred).
    const targetY = sheetState === "peek"
      ? Math.round(mapH * 0.38)
      : Math.round(mapH * 0.20);
    const offsetPx = Math.max(0, Math.round(mapH / 2 - targetY));

    // Shift a latlng southward so it renders at targetY instead of map centre
    const raise = (latlng, zoom) => {
      if (offsetPx === 0) return latlng;
      const px = mapRef.current.project(latlng, zoom);
      return mapRef.current.unproject(px.add([0, offsetPx]), zoom);
    };

    // Single pin: expand bbox so fitBounds zooms to street level with padding
    if (visible.length === 1 && !userLoc) {
      const p = visible[0];
      const d = 0.003;
      mapRef.current.fitBounds(
        [[p.lat - d, p.lng - d], [p.lat + d, p.lng + d]],
        { paddingTopLeft: [20, 56], paddingBottomRight: [20, sheetPx + 24], maxZoom: 15 }
      );
      return;
    }

    // With user location: centre on user at neighbourhood zoom
    if (userLoc) {
      mapRef.current.setView(raise([userLoc.lat, userLoc.lng], 13), 13, { animate: true });
      return;
    }

    // Multiple pins, no user: fitBounds so zoom adapts to pin spread
    // (a fixed zoom fails when pins are far apart, e.g. a farm in Thorndale vs London)
    const b = L.latLngBounds(visible.map(p => [p.lat, p.lng]));
    mapRef.current.fitBounds(b, {
      paddingTopLeft:    [20, 56],
      paddingBottomRight:[20, sheetPx + 24],
      maxZoom: 14
    });
  }, [fitAll, filter, places, userLoc, sheetState]);

  return <div ref={elRef} id="map" />;
}

// ── HOME (mobile + left panel on desktop) ──
function Home({ geo, places = [], cats = [] }) {
  const counts = useMemo(() => {
    const m = {};
    cats.forEach(c => m[c.id] = places.filter(p => p.category === c.id).length);
    return m;
  }, [cats, places]);

  const onNearMe = () => {
    geo.request();
    nav("/map");
  };

  return (
    <div className="home-wrap">
      <div className="home-hero">
        <div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between" }}>
          <div>
            <div className="label-eyebrow eyebrow">London &amp; Middlesex</div>
            <h1>Find food<br />near you.</h1>
          </div>
          <a href="https://mlfpc.ca/" target="_blank" rel="noopener" style={{ flexShrink: 0, marginTop: 4 }}>
            <img src="mlfpc-logo-green.png" alt="MLFPC" style={{ height: 72, width: "auto", objectFit: "contain", display: "block" }} />
          </a>
        </div>
      </div>

      <div className="search">
        <svg width="16" height="16" viewBox="0 0 20 20" fill="none"><circle cx="8" cy="8" r="6" stroke="#c8beb4" strokeWidth="2"/><path d="M13 13L18 18" stroke="#c8beb4" strokeWidth="2" strokeLinecap="round"/></svg>
        <input placeholder="Search by name or address…" onFocus={() => nav("/map")} readOnly />
      </div>

      <button className="btn btn-primary" onClick={onNearMe} style={{ width: "100%" }}>
        <span style={{ width: 10, height: 10, borderRadius: "50%", background: "rgba(255,255,255,0.85)" }} />
        Use my location
      </button>

      <div className="home-divider">or browse by category</div>

      <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
        {cats.map(c => (
          <button key={c.id} className="cat-tile" style={{ borderLeftColor: c.color }} onClick={() => nav("/map/cat=" + c.id)}>
            <span className="cat-swatch" style={{ background: c.soft, color: c.color }}>
              <span dangerouslySetInnerHTML={{ __html: pinSvg(c.color, 18) }} />
            </span>
            <span className="cat-label">{c.label}</span>
            <span className="cat-count">{counts[c.id]} {counts[c.id] === 1 ? "place" : "places"}</span>
          </button>
        ))}
      </div>

      <div className="home-footer">
        Data updated weekly by community maintainers.<br />
        Last update: April 28, 2026.
      </div>
    </div>
  );
}

// ── BROWSE (desktop sidebar only — mobile routes to pre-filtered MapScreen) ──
function Browse({ catId, geo, places = [], cats = [] }) {
  const cat = catById(catId, cats);
  const userLoc = geo.loc;

  if (!cat) {
    return (
      <>
        <div className="subhead">
          <button className="backbtn" onClick={() => nav("/")}>←</button>
          <div className="subhead-title">Not found</div>
        </div>
        <div className="empty">Unknown category.</div>
      </>
    );
  }

  const matches = useMemo(() => {
    const list = places.filter(p => p.category === catId);
    if (!userLoc) return list;
    return list.map(p => ({ ...p, _d: distanceKm(userLoc, p) }))
               .sort((a, b) => (a._d ?? 999) - (b._d ?? 999));
  }, [catId, userLoc, places]);

  return (
    <>
      <div className="subhead">
        <button className="backbtn" onClick={() => nav("/")}>←</button>
        <div style={{ flex: 1 }}>
          <div className="subhead-title">{cat.label}</div>
          <div className="subhead-sub">{matches.length} {matches.length === 1 ? "result" : "results"}{userLoc ? " · sorted by distance" : ""}</div>
        </div>
      </div>
      <div className="panel-scroll">
        <div className="list">
          {matches.map(p => (
            <PlaceCard key={p.id} place={p} userLoc={userLoc} cats={cats} onClick={() => nav("/location/" + p.id)} />
          ))}
        </div>
      </div>
    </>
  );
}

// ── MAP screen (mobile: full-bleed; desktop: left panel + map) ──
function MapScreen({ initialFilter, geo, isDesktop, places = [], cats = [] }) {
  const [filter, setFilter] = useState(initialFilter || "all");
  const [sheetOpen, setSheetOpen] = useState(!!initialFilter && initialFilter !== "all");
  const [selectedPlace, setSelectedPlace] = useState(null);
  const userLoc = geo.loc;
  const toast = useToast();

  const sheetState = selectedPlace ? "detail" : sheetOpen ? "open" : "peek";

  // history.pushState so swipe-left dismisses the detail sheet before leaving the page
  const pushedForPlace  = useRef(false);
  const clearingManual  = useRef(false);
  useEffect(() => {
    const onPop = () => {
      if (clearingManual.current) { clearingManual.current = false; return; }
      if (pushedForPlace.current) { pushedForPlace.current = false; setSelectedPlace(null); }
    };
    window.addEventListener("popstate", onPop);
    return () => window.removeEventListener("popstate", onPop);
  }, []);

  // Swipe-to-open/close on the sheet handle
  const dragStartY = useRef(null);
  const didSwipe   = useRef(false);
  const sheetHandleEvents = {
    onTouchStart: e => { dragStartY.current = e.touches[0].clientY; didSwipe.current = false; },
    onTouchEnd:   e => {
      if (dragStartY.current === null) return;
      const delta = e.changedTouches[0].clientY - dragStartY.current;
      dragStartY.current = null;
      if (delta < -40) { setSheetOpen(true);  didSwipe.current = true; }
      else if (delta > 40) { setSheetOpen(false); didSwipe.current = true; }
    },
    onClick: () => { if (didSwipe.current) { didSwipe.current = false; return; } setSheetOpen(o => !o); }
  };

  const visible = useMemo(() => {
    const list = filter === "all" ? places : places.filter(p => p.category === filter);
    if (!userLoc) return list;
    return list.map(p => ({ ...p, _d: distanceKm(userLoc, p) }))
               .sort((a, b) => (a._d ?? 999) - (b._d ?? 999));
  }, [filter, userLoc, places]);

  const onNearMe = () => {
    if (!userLoc) {
      geo.request();
      toast("Locating you…");
    } else {
      toast("Centred on your location");
    }
  };

  const selectPlace = (p) => {
    setSelectedPlace(p);
    setSheetOpen(false);
    history.pushState({ foodmap: "place" }, "");
    pushedForPlace.current = true;
  };
  const clearPlace = () => {
    setSelectedPlace(null);
    if (pushedForPlace.current) {
      pushedForPlace.current = false;
      clearingManual.current = true;
      history.back();
    }
  };

  return (
    <>
      <MapHost
        filter={filter}
        userLoc={userLoc}
        places={places}
        cats={cats}
        activeId={selectedPlace?.id}
        focusId={selectedPlace?.id}
        onPinClick={selectPlace}
        fitAll={!selectedPlace}
        sheetState={sheetState}
      />

      {!isDesktop && (
        <>
          <div className="map-overlay map-search-row">
            <button className="map-iconbtn" onClick={() => nav("/")}>←</button>
            <div className="map-search">
              <svg width="14" height="14" viewBox="0 0 20 20" fill="none"><circle cx="8" cy="8" r="6" stroke="#c8beb4" strokeWidth="2"/><path d="M13 13L18 18" stroke="#c8beb4" strokeWidth="2" strokeLinecap="round"/></svg>
              <input placeholder="Search…" />
            </div>
          </div>

          <button className="nearme-pill" onClick={onNearMe}>
            <span style={{ width: 8, height: 8, borderRadius: "50%", background: "var(--g700)", boxShadow: "0 0 0 3px var(--g200)" }} />
            {userLoc ? "Near me" : "Use my location"}
            {userLoc && <span style={{ color: "var(--light)" }}>·</span>}
            {userLoc && <span style={{ color: "var(--light)", fontSize: 12 }}>{visible.length} nearby</span>}
          </button>

          <div className="map-filters">
            <FilterBar active={filter} onChange={v => { setFilter(v); clearPlace(); }} cats={cats} />
          </div>

          {selectedPlace ? (
            <div className="sheet is-detail">
              <div className="sheet-handle" onClick={clearPlace}><span /></div>
              <div className="sheet-head">
                <button className="backbtn" onClick={clearPlace} style={{ fontSize: 14 }}>←</button>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div className="sheet-head-title" style={{ fontSize: 15, fontWeight: 700 }}>{selectedPlace.name}</div>
                  <div style={{ fontSize: 12, color: "var(--light)" }}>{selectedPlace.address}</div>
                </div>
                {(() => { const c = catById(selectedPlace.category, cats); return c ? <span className="pill" style={{ background: c.soft, color: c.color, borderColor: c.border }}>{c.label}</span> : null; })()}
              </div>
              <div className="sheet-body">
                {userLoc && distanceKm(userLoc, selectedPlace) != null && (
                  <div className="detail-row" style={{ paddingBottom: 4 }}>
                    <span className="row-icon">📍</span>
                    <span style={{ color: "var(--g700)", fontWeight: 600 }}>{fmtKm(distanceKm(userLoc, selectedPlace))} away</span>
                  </div>
                )}
                <div className="detail-row">
                  <span className="row-icon">🕒</span>
                  <span>{selectedPlace.hoursFull || selectedPlace.hours}</span>
                </div>
                {selectedPlace.eligibility && (
                  <div className="detail-row">
                    <span className="row-icon">✓</span>
                    <span style={{ color: "var(--g700)" }}>{selectedPlace.eligibility}</span>
                  </div>
                )}
                <div className="detail-actions" style={{ marginTop: 12 }}>
                  <a className="btn btn-primary" href={gmapsDirectionsUrl(selectedPlace)} target="_blank" rel="noopener" style={{ textDecoration: "none", flex: 1 }}>Directions</a>
                  <button className="btn" style={{ flex: 1 }} onClick={() => nav("/location/" + selectedPlace.id)}>Full details →</button>
                </div>
              </div>
            </div>
          ) : (
            <div className={"sheet" + (sheetOpen ? " is-open" : "")} style={{ "--sheet-peek": "96px" }}>
              <div className="sheet-handle" {...sheetHandleEvents}><span /></div>
              <div className="sheet-head">
                <span className="sheet-head-title">
                  {visible.length} {visible.length === 1 ? "result" : "results"} {filter !== "all" && "in " + catById(filter, cats).label}
                </span>
                <button className="sheet-head-action" onClick={() => setSheetOpen(o => !o)}>
                  {sheetOpen ? "↓ Hide" : "↑ Show list"}
                </button>
              </div>
              <div className="sheet-body">
                <div className="list">
                  {visible.map(p => (
                    <PlaceCard key={p.id} place={p} userLoc={userLoc} cats={cats} onClick={() => selectPlace(p)} />
                  ))}
                  {!visible.length && <div className="empty">No places match this filter.</div>}
                </div>
              </div>
            </div>
          )}
        </>
      )}
    </>
  );
}

// Desktop sidebar version of the map screen (left panel)
function MapSidebar({ filter, setFilter, geo, places = [], cats = [] }) {
  const userLoc = geo.loc;
  const toast = useToast();
  const visible = useMemo(() => {
    const list = filter === "all" ? places : places.filter(p => p.category === filter);
    if (!userLoc) return list;
    return list.map(p => ({ ...p, _d: distanceKm(userLoc, p) }))
               .sort((a, b) => (a._d ?? 999) - (b._d ?? 999));
  }, [filter, userLoc, places]);

  return (
    <>
      <div className="subhead">
        <button className="backbtn" onClick={() => nav("/")}>←</button>
        <div style={{ flex: 1 }}>
          <div className="subhead-title">{filter === "all" ? "All resources" : catById(filter, cats).label}</div>
          <div className="subhead-sub">{visible.length} results{userLoc ? " · sorted by distance" : ""}</div>
        </div>
        <button className="chip" onClick={() => {
          if (!userLoc) { geo.request(); toast("Locating you…"); }
          else toast("Centred on your location");
        }}>
          <span style={{ width: 6, height: 6, borderRadius: "50%", background: "var(--g700)", display: "inline-block", marginRight: 4 }} />
          Near me
        </button>
      </div>
      <FilterBar active={filter} onChange={setFilter} cats={cats} />
      <div className="panel-scroll">
        <div className="list">
          {visible.map(p => (
            <PlaceCard key={p.id} place={p} userLoc={userLoc} cats={cats} onClick={() => nav("/location/" + p.id)} />
          ))}
          {!visible.length && <div className="empty">No places match this filter.</div>}
        </div>
      </div>
    </>
  );
}

// ── DETAIL ──
function Detail({ id, geo, places = [], cats = [] }) {
  const place = places.find(p => p.id === id);
  const [correction, setCorrection] = useState("");
  const cat = catById(place?.category, cats);
  const toast = useToast();

  if (!place) {
    return (
      <>
        <div className="subhead">
          <button className="backbtn" onClick={() => history.length > 1 ? history.back() : nav("/")}>←</button>
          <div className="subhead-title">Not found</div>
        </div>
        <div className="empty">This location is no longer listed.</div>
      </>
    );
  }
  const d = geo.loc ? distanceKm(geo.loc, place) : null;
  const businessUrl = gmapsBusinessUrl(place);
  const directionsUrl = gmapsDirectionsUrl(place);

  const submitCorrection = () => {
    if (!correction.trim()) {
      toast("Add a note before submitting");
      return;
    }
    const cfg = window.FORM_CONFIG;
    if (cfg && cfg.enabled && cfg.formId && cfg.formId !== "REPLACE_ME") {
      const fd = new FormData();
      fd.append(cfg.fields.placeId,    place.id);
      fd.append(cfg.fields.placeName,  place.name);
      fd.append(cfg.fields.correction, correction);
      fetch(`https://docs.google.com/forms/d/e/${cfg.formId}/formResponse`, {
        method: "POST", body: fd, mode: "no-cors"
      }).catch(() => { /* no-cors is fire-and-forget */ });
    }
    setCorrection("");
    toast("Correction sent — thank you!");
  };

  return (
    <>
      <div className="subhead">
        <button className="backbtn" onClick={() => history.length > 1 ? history.back() : nav("/")}>←</button>
        <div className="subhead-title">Location</div>
      </div>
      <div className="panel-scroll">
        <div className="detail-wrap">
          <div className="detail-card">
            <div className="card-row">
              <span className="detail-title">{place.name}</span>
              {cat && <span className="pill" style={{ background: cat.soft, color: cat.color, borderColor: cat.border }}>{cat.label}</span>}
            </div>

            <div className="detail-row">
              <span className="row-icon">📍</span>
              <span>{place.address}{d != null && <> · <strong style={{ color: "var(--g700)" }}>{fmtKm(d)}</strong> away</>}</span>
            </div>
            <div className="detail-row">
              <span className="row-icon">🕒</span>
              <span>{place.hoursFull}</span>
            </div>
            <div className="detail-row">
              <span className="row-icon">✓</span>
              <span style={{ color: "var(--g700)" }}>{place.eligibility}</span>
            </div>
            {place.phone && (
              <div className="detail-row">
                <span className="row-icon">☎</span>
                <a href={"tel:" + place.phone.replace(/[^\d+]/g, "")} style={{ color: "var(--ink)", textDecoration: "underline" }}>{place.phone}</a>
              </div>
            )}
            {place.notes && (
              <div className="detail-row" style={{ color: "var(--mid)", fontStyle: "italic" }}>
                <span className="row-icon">ℹ</span>
                <span>{place.notes}</span>
              </div>
            )}

            <div className="detail-row" style={{ color: "var(--light)", fontSize: 12, marginTop: 2 }}>
              <span className="row-icon">•</span>
              <span>Last verified {place.verified}</span>
            </div>

            <div className="detail-actions">
              <a className="btn btn-primary" href={businessUrl} target="_blank" rel="noopener" style={{ textDecoration: "none" }}>
                Open in Google Maps ↗
              </a>
              <a className="btn" href={directionsUrl} target="_blank" rel="noopener" style={{ textDecoration: "none" }}>
                Directions
              </a>
              {place.phone && (
                <a className="btn" href={"tel:" + place.phone.replace(/[^\d+]/g, "")} style={{ textDecoration: "none" }}>
                  Call
                </a>
              )}
            </div>
          </div>

          <div className="detail-correction">
            <h3>Something wrong with this listing?</h3>
            <p>Hours change, programs move. Tell the maintainers what we missed and they'll review.</p>
            <textarea
              placeholder="What needs correcting? (e.g. hours, address, eligibility…)"
              value={correction}
              onChange={e => setCorrection(e.target.value)}
            />
            <button className="btn btn-amber btn-pill" onClick={submitCorrection}>Send correction →</button>
          </div>
        </div>
      </div>
    </>
  );
}

// ── App shell + routing ──
function App({ places = [], cats = [] }) {
  const route = useRoute();
  const isDesktop = useIsDesktop();
  const geo = useGeo();

  // Parse route. Shape: [] | ["browse", catId] | ["map"] | ["location", id]
  const screen = route[0] || "home";
  const param  = route[1];

  // For map screen, we also support ?cat=xxx via a custom query-style segment
  let mapFilter = "all";
  if (screen === "map" && route[1] && route[1].startsWith("cat=")) {
    mapFilter = route[1].slice(4);
  }

  // Desktop: keep the map mounted at all times; sidebar swaps content.
  // Mobile: render one screen at a time.
  const [desktopFilter, setDesktopFilter] = useState(mapFilter);
  useEffect(() => { if (mapFilter !== "all") setDesktopFilter(mapFilter); }, [mapFilter]);

  // What sidebar to show on desktop
  let desktopSidebar;
  if (screen === "home") {
    desktopSidebar = <Home geo={geo} places={places} cats={cats} />;
  } else if (screen === "browse") {
    desktopSidebar = <Browse catId={param} geo={geo} places={places} cats={cats} />;
  } else if (screen === "location") {
    desktopSidebar = <Detail id={param} geo={geo} places={places} cats={cats} />;
  } else if (screen === "map") {
    desktopSidebar = <MapSidebar filter={desktopFilter} setFilter={setDesktopFilter} geo={geo} places={places} cats={cats} />;
  } else {
    desktopSidebar = <Home geo={geo} places={places} cats={cats} />;
  }

  // For desktop map host, use the right filter for the current sidebar
  const desktopMapFilter = (screen === "browse" && param) ? param :
                           (screen === "location" && places.find(p => p.id === param)?.category) ||
                           (screen === "map" ? desktopFilter : "all");
  const focusId = screen === "location" ? param : null;

  return (
    <ToastProvider>
      <div className="app">
        {/* Desktop topnav */}
        <header className="topnav">
          <span className="topnav-title">London &amp; Middlesex Food Map</span>
          <span className="pill" style={{ background: "var(--canvas)", borderColor: "var(--border)", color: "var(--mid)" }}>
            Phase 1 prototype
          </span>
          <div className="topnav-spacer" />
          <a className="topnav-link" href="#/">Home</a>
          <a className="topnav-link" href="#/map">Map</a>
          <button className="btn btn-pill" onClick={() => alert("In Phase 2 this opens the maintainer Google Form.")}>+ Suggest a resource</button>
        </header>

        {/* MOBILE */}
        {!isDesktop && (
          screen === "map" ? (
            <main className="stage">
              <MapScreen initialFilter={mapFilter} geo={geo} isDesktop={false} places={places} cats={cats} />
            </main>
          ) : screen === "browse" ? (
            <main className="stage">
              <MapScreen initialFilter={param} geo={geo} isDesktop={false} places={places} cats={cats} />
            </main>
          ) : screen === "location" ? (
            <main className="panel">
              <Detail id={param} geo={geo} places={places} cats={cats} />
            </main>
          ) : (
            <main className="panel">
              <div className="panel-scroll">
                <Home geo={geo} places={places} cats={cats} />
              </div>
            </main>
          )
        )}

        {/* DESKTOP */}
        {isDesktop && (
          <>
            <aside className="panel">
              {desktopSidebar}
            </aside>
            <section className="stage">
              <MapHost
                filter={desktopMapFilter === "all" ? "all" : desktopMapFilter}
                userLoc={geo.loc}
                places={places}
                cats={cats}
                activeId={focusId}
                onPinClick={p => nav("/location/" + p.id)}
                focusId={focusId}
                fitAll={screen === "home" || screen === "map" || screen === "browse"}
              />
            </section>
          </>
        )}

        <div className="demo-banner">Demo · hardcoded data</div>
      </div>
    </ToastProvider>
  );
}

// ── Bootstrap: load both CSVs first, then mount React ────────────────────────
const root = ReactDOM.createRoot(document.getElementById("root"));

function showError(msg) {
  document.getElementById("root").innerHTML =
    `<div style="padding:32px;font-family:'DM Sans',sans-serif;color:#56493e;max-width:480px;margin:48px auto;">
       <div style="font-size:14px;font-weight:600;color:#a85518;margin-bottom:8px;">Couldn't load data</div>
       <div style="font-size:13px;">${msg}</div>
       <div style="font-size:12px;color:#9c8f83;margin-top:16px;">If you opened this file directly (file://), run a local server instead: <code>python -m http.server</code> in the <code>app/</code> folder.</div>
     </div>`;
}

(async () => {
  try {
    const [places, cats] = await Promise.all([
      window.loadPlaces(),
      window.loadCategories()
    ]);
    window.PLACES = places; // debug exposure
    window.CATS   = cats;
    root.render(<App places={places} cats={cats} />);
  } catch (err) {
    console.error(err);
    showError(err.message || String(err));
  }
})();
