/** A real map (Leaflet + OSM tiles) with a real driving route (OSRM), revealed by the
 *  folded-paper cascade: six white bands rotate away from their top edge, one after the other.
 *  Falls back to a straight dashed line + haversine estimate if the router can't be reached.
 *  Tiles: OSM-FR "hot" — the only light basemap measured with genuine dark linework (min L≈82);
 *  CARTO light_all and voyager sit in a near-white band and vanish under desaturation, and
 *  tile.openstreetmap.org blocks embedded origins.
 *
 *  The map is created once on mount and never unmounted: the collapsed state hides it behind
 *  the bands and clips the wrapper, but Leaflet always has a real box to measure, so tiles are
 *  already warm when the slab opens. */
const BANDS = 6;

function MapPanel({ open, width, height = 176, dest, origin, pins, onRoute }) {
  const host = React.useRef(null);
  const map = React.useRef(null);
  const marks = React.useRef({});
  const [ready, setReady] = React.useState(false);

  // create once, eagerly — not gated on `open`
  React.useEffect(() => {
    if (!host.current || map.current || typeof L === "undefined") return;
    const m = L.map(host.current, {
      zoomControl: false, attributionControl: false, fadeAnimation: false,
      scrollWheelZoom: false, doubleClickZoom: true, dragging: true, keyboard: false
    }).setView([dest.lat, dest.lon], 13);
    L.tileLayer("https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png", {
      maxZoom: 19, subdomains: "ab", crossOrigin: true,
      keepBuffer: 4, updateWhenIdle: false, updateWhenZooming: false
    }).addTo(m);
    map.current = m;
    m.whenReady(() => setReady(true));
    requestAnimationFrame(() => m.invalidateSize({ animate: false }));
  }, []);

  // re-measure across the width transition, then re-apply the route fit once the box has settled
  React.useEffect(() => {
    const m = map.current;
    if (!m) return;
    let raf, stop = false;
    const t0 = performance.now();
    const tick = () => {
      if (stop) return;
      m.invalidateSize({ animate: false, pan: false });
      if (performance.now() - t0 < 520) { raf = requestAnimationFrame(tick); return; }
      applyFit(m);
    };
    raf = requestAnimationFrame(tick);
    return () => { stop = true; cancelAnimationFrame(raf); };
  }, [open, width, ready]);

  // One authority for framing. Leaflet only re-projects layer geometry on viewreset/moveend, so a
  // fitBounds that resolves to the same center/zoom leaves paths stuck at "M0 0" (which is exactly
  // what happens when the route is added while the slab is collapsed). Re-set the coordinates
  // explicitly afterwards to force _project() to run.
  const applyFit = React.useCallback((m) => {
    const s = m.getSize();
    if (!s || s.x < 2 || s.y < 2) return;
    const b = marks.current.bounds;
    if (b && b.isValid()) m.fitBounds(b, { padding: [16, 16], maxZoom: 15, animate: false });
    const pts = marks.current.pts;
    if (pts) {
      if (marks.current.halo) marks.current.halo.setLatLngs(pts);
      if (marks.current.line) marks.current.line.setLatLngs(pts);
    }
    if (marks.current.dest && marks.current.dest.update) marks.current.dest.update();
    if (marks.current.origin && marks.current.origin.update) marks.current.origin.update();
  }, []);

  // Leaflet's own resize event is the other moment the box can change under us
  React.useEffect(() => {
    const m = map.current;
    if (!m || !ready) return;
    const onResize = () => applyFit(m);
    m.on("resize", onResize);
    return () => m.off("resize", onResize);
  }, [ready, applyFit]);

  React.useEffect(() => {
    const m = map.current;
    if (!m || !ready) return;
    let cancelled = false;

    const pin = (lat, lon, filled, label) => L.marker([lat, lon], {
      icon: L.divIcon({
        className: "",
        iconSize: [12, 12], iconAnchor: [6, 6],
        html: '<div style="width:12px;height:12px;border-radius:999px;background:' + (filled ? "#000" : "#fff") +
          ';border:2px solid ' + (filled ? "#fff" : "#000") + ';box-shadow:0 0 0 1px #000"></div>'
      }),
      title: label, keyboard: false
    });

    Object.values(marks.current).forEach(l => l && l.addTo && m.removeLayer(l));
    marks.current = { bounds: null, pts: null };
    marks.current.dest = pin(dest.lat, dest.lon, true, dest.label).addTo(m);

    // managing addresses: pin them all and frame the set instead of a route
    if (pins && pins.length) {
      marks.current.extra = L.layerGroup(
        pins.filter(p => p.line !== dest.line).map(p => pin(p.lat, p.lon, false, p.label))
      ).addTo(m);
      marks.current.bounds = L.latLngBounds(pins.map(p => [p.lat, p.lon]));
      applyFit(m);
      if (onRoute) onRoute(null);
      return;
    }

    if (!origin) {
      marks.current.bounds = null; marks.current.pts = null;
      m.setView([dest.lat, dest.lon], 13, { animate: true, duration: 0.5 });
      if (onRoute) onRoute(null);
      return;
    }

    marks.current.origin = pin(origin.lat, origin.lon, false, origin.name).addTo(m);

    const frame = (line, pts) => { marks.current.bounds = line.getBounds(); marks.current.pts = pts; applyFit(m); };

    const straight = () => {
      const R = 6371, dLat = (dest.lat - origin.lat) * Math.PI / 180, dLon = (dest.lon - origin.lon) * Math.PI / 180;
      const a = Math.sin(dLat / 2) ** 2 + Math.cos(origin.lat * Math.PI / 180) * Math.cos(dest.lat * Math.PI / 180) * Math.sin(dLon / 2) ** 2;
      const km = 2 * R * Math.asin(Math.sqrt(a)) * 1.35;
      const pts = [[origin.lat, origin.lon], [dest.lat, dest.lon]];
      marks.current.line = L.polyline(pts, {
        color: "#000", weight: 2.5, dashArray: "6 6", opacity: 0.9
      }).addTo(m);
      frame(marks.current.line, pts);
      if (onRoute) onRoute({ km: km, min: Math.max(6, Math.round((km / 22) * 60)), exact: false });
    };

    // don't hang on a slow router — fall back to the estimate after 3.5s
    const ctl = typeof AbortController !== "undefined" ? new AbortController() : null;
    const bail = setTimeout(() => { if (ctl) ctl.abort(); }, 3500);

    fetch("https://router.project-osrm.org/route/v1/driving/" +
      origin.lon + "," + origin.lat + ";" + dest.lon + "," + dest.lat +
      "?overview=full&geometries=geojson", ctl ? { signal: ctl.signal } : undefined)
      .then(r => r.json())
      .then(j => {
        clearTimeout(bail);
        if (cancelled) return;
        const route = j && j.routes && j.routes[0];
        if (!route) return straight();
        const pts = route.geometry.coordinates.map(c => [c[1], c[0]]);
        marks.current.halo = L.polyline(pts, { color: "#fff", weight: 6, opacity: 0.7 }).addTo(m);
        marks.current.line = L.polyline(pts, { color: "#000", weight: 3, dashArray: "7 7", opacity: 0.95 }).addTo(m);
        frame(marks.current.line, pts);
        if (onRoute) onRoute({ km: route.distance / 1000, min: Math.max(5, Math.round(route.duration / 60)), exact: true });
      })
      .catch(() => { clearTimeout(bail); if (!cancelled) straight(); });

    return () => { cancelled = true; clearTimeout(bail); };
  }, [ready, applyFit, dest.lat, dest.lon, pins && pins.length, pins && pins.map(p => p.lat + "," + p.lon).join("|"), origin && origin.lat, origin && origin.lon]);

  const bandH = height / BANDS;

  return (
    <div style={{
      position: "relative", width, height: open ? height : 0,
      perspective: "820px", perspectiveOrigin: "50% 0%",
      background: open ? "var(--white)" : "transparent", overflow: "hidden",
      borderBottomLeftRadius: "var(--radius-md)",
      transition: "height 320ms var(--ease-out) " + (open ? "0ms" : "180ms")
    }}>
      <div ref={host} style={{
        position: "absolute", left: 0, top: 0, width: "100%", height, zIndex: 0, isolation: "isolate",
        filter: "grayscale(1) contrast(1.15)",
        opacity: open ? 1 : 0, transition: "opacity 200ms linear " + (open ? "150ms" : "0ms")
      }} />
      {Array.from({ length: BANDS }).map((_, i) => (
        <div key={i} style={{
          position: "absolute", left: 0, right: 0, top: i * bandH, height: bandH + 0.5, zIndex: 10,
          background: "var(--white)",
          borderBottom: "1px solid rgb(0 0 0 / 0.12)",
          transformOrigin: "top center",
          transform: open ? "rotateX(-94deg)" : "rotateX(0deg)",
          transition: "transform 300ms var(--ease-out) " + (open ? 70 + i * 52 : (BANDS - 1 - i) * 38) + "ms",
          backfaceVisibility: "hidden", pointerEvents: "none", willChange: "transform"
        }} />
      ))}
      {dest.city && (
        <div style={{
          position: "absolute", left: 8, bottom: 7, zIndex: 20,
          fontFamily: "var(--font-mono)", fontWeight: 700, fontSize: 8.5, letterSpacing: "0.14em",
          textTransform: "uppercase", color: "var(--black)", background: "rgb(255 255 255 / 0.86)", padding: "3px 5px",
          opacity: open ? 1 : 0, transition: "opacity var(--dur-base) var(--ease-out) 380ms"
        }}>{dest.city}</div>
      )}
    </div>
  );
}
Object.assign(window, { MapPanel });
