// Terminal Atlas page — one page per shipping facility (carrier hub, cold storage,
// dry storage, port, etc.). Doubles as a Trojan horse for driver acquisition:
// drivers searching "UPS Commerce City driver entrance" land here.
//
// Pulls live data from Supabase: entity, attributes, status log, recent tips,
// recent wait-time reports. Submission widgets write back to the DB.

const { useState: useStateT, useEffect: useEffectT, useMemo: useMemoT } = React;

function TerminalPage({ slug, onNav }) {
  const [terminal, setTerminal] = useStateT(null);
  const [parent, setParent] = useStateT(null);
  const [city, setCity] = useStateT(null);
  const [attrs, setAttrs] = useStateT([]);
  const [statusLog, setStatusLog] = useStateT([]);
  const [tips, setTips] = useStateT([]);
  const [waits, setWaits] = useStateT([]);
  const [loading, setLoading] = useStateT(true);
  const [waitInput, setWaitInput] = useStateT("");
  const [waitSubmitted, setWaitSubmitted] = useStateT(false);
  const [tipInput, setTipInput] = useStateT("");
  const [tipRole, setTipRole] = useStateT("driver");
  const [tipSeverity, setTipSeverity] = useStateT("moderate");
  const [tipPublic, setTipPublic] = useStateT(false);
  const [tipEmail, setTipEmail] = useStateT("");
  const [tipSubmitted, setTipSubmitted] = useStateT(false);
  const [tipToken, setTipToken] = useStateT(null);
  // Photos attached to a tip — driver can drop up to 4 images. Held in
  // local state as { file, previewUrl, uploading, attachment } until
  // submit; uploaded to Supabase Storage on submit (or earlier as the
  // user drops them, see uploadOne). attachment is set when upload
  // completes — this is what ends up referenced from intel_submissions.
  const [tipPhotos, setTipPhotos] = useStateT([]);
  const [tipPhotoErr, setTipPhotoErr] = useStateT(null);
  // Driver-experience ratings (per facility, six dimensions, 1-5 stars).
  // Aggregates are fetched into `dxScores` (per-dimension) + `dxComposite`.
  // The per-row table itself is RLS-locked from anon — only the views surface.
  const [dxScores, setDxScores] = useStateT([]);
  const [dxComposite, setDxComposite] = useStateT(null);
  const [dxRatings, setDxRatings] = useStateT({});         // dim → rating in form
  const [dxNote, setDxNote] = useStateT("");
  const [dxEmail, setDxEmail] = useStateT("");
  const [dxSubmitted, setDxSubmitted] = useStateT(false);
  const [dxToken, setDxToken] = useStateT(null);
  const [dxFormOpen, setDxFormOpen] = useStateT(false);

  useEffectT(() => {
    if (!window.SI_DB) { setLoading(false); return; }

    (async () => {
      const ents = await window.SI_DB.raw.select(
        "entities",
        `select=*&slug=eq.${encodeURIComponent(slug)}`
      );
      if (!ents || !ents.length) { setLoading(false); return; }
      const t = ents[0];
      setTerminal(t);

      const [parentEnts, cityRows, attrRows, logRows, tipRows, waitRows] = await Promise.all([
        t.parent_id
          ? window.SI_DB.raw.select("entities", `select=slug,name&id=eq.${t.parent_id}`)
          : Promise.resolve([]),
        t.primary_city_id
          ? window.SI_DB.raw.select("cities", `select=*&id=eq.${t.primary_city_id}`)
          : Promise.resolve([]),
        window.SI_DB.raw.select(
          "entity_attributes",
          `select=key,value&entity_id=eq.${t.id}&order=key.asc`
        ),
        window.SI_DB.raw.select(
          "facility_status_log",
          `select=*&entity_id=eq.${t.id}&order=start_date.desc&limit=5`
        ),
        window.SI_DB.raw.select(
          "intel_submissions",
          `select=raw_text,submitter_role,parsed_intel_type,created_at&facility_id=eq.${t.id}&status=eq.verified&order=created_at.desc&limit=20`
        ),
        window.SI_DB.raw.select(
          "wait_time_reports",
          `select=minutes,created_at&facility_id=eq.${t.id}&order=created_at.desc&limit=12`
        ),
      ]);

      setParent(parentEnts && parentEnts[0]);
      setCity(cityRows && cityRows[0]);
      setAttrs(attrRows || []);
      setStatusLog(logRows || []);
      setTips(tipRows || []);
      setWaits(waitRows || []);
      setLoading(false);

      // Driver experience aggregates — fire-and-forget, don't block render.
      // Views may not exist yet (pre-migration); guard against errors silently.
      try {
        if (window.SI_DB.facilityDriverScore) {
          const [scores, composite] = await Promise.all([
            window.SI_DB.facilityDriverScore(t.id),
            window.SI_DB.facilityDriverComposite(t.id),
          ]);
          setDxScores(scores || []);
          setDxComposite(composite);
        }
      } catch (err) { /* views not yet deployed */ }
    })();
  }, [slug]);

  const attrMap = useMemoT(() => {
    const m = {};
    attrs.forEach((a) => { m[a.key] = a.value; });
    return m;
  }, [attrs]);

  // Current status: most recent open log row whose end_date is null or in future
  const currentStatus = useMemoT(() => {
    const now = new Date().toISOString().slice(0, 10);
    return statusLog.find((s) => !s.end_date || s.end_date >= now);
  }, [statusLog]);

  const avgWait = useMemoT(() => {
    if (!waits.length) return null;
    const total = waits.reduce((a, w) => a + w.minutes, 0);
    return Math.round(total / waits.length);
  }, [waits]);

  function timeAgo(iso) {
    const ms = Date.now() - new Date(iso).getTime();
    const m = Math.floor(ms / 60000);
    if (m < 60) return `${m}m ago`;
    const h = Math.floor(m / 60);
    if (h < 24) return `${h}h ago`;
    const d = Math.floor(h / 24);
    return `${d}d ago`;
  }

  async function submitWait(e) {
    e.preventDefault();
    const minutes = parseInt(waitInput, 10);
    if (!minutes || minutes < 0 || minutes > 600) return;
    await window.SI_DB.submitWaitTime(terminal.id, minutes);
    setWaitInput("");
    setWaitSubmitted(true);
    // Refetch
    const newWaits = await window.SI_DB.raw.select(
      "wait_time_reports",
      `select=minutes,created_at&facility_id=eq.${terminal.id}&order=created_at.desc&limit=12`
    );
    setWaits(newWaits || []);
  }

  function generateToken() {
    // 32 chars of alphanumeric — short enough for a URL, long enough to be unguessable
    const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
    let t = "";
    const arr = new Uint8Array(32);
    (window.crypto || window.msCrypto).getRandomValues(arr);
    for (let i = 0; i < 32; i++) t += chars[arr[i] % chars.length];
    return t;
  }

  // Six dimensions of driver experience. Order matters — most-actionable first.
  const DX_DIMS = [
    { id: "restroom_access",      label: "Restroom access",   hint: "Available 24/7? Locked? Clean?" },
    { id: "parking_overnight",    label: "Parking",            hint: "Overnight? Secure? Lit?" },
    { id: "detention_treatment",  label: "Detention treatment",hint: "Waiting area, respect, amenities" },
    { id: "dock_yard_clarity",    label: "Dock & yard clarity",hint: "Signage, gate-in, dispatch comms" },
    { id: "wait_honesty",         label: "Wait honesty",       hint: "Does the appointment match reality?" },
    { id: "security_treatment",   label: "Security",           hint: "Gate guards' professionalism" },
  ];

  async function submitDxRatings(e) {
    e.preventDefault();
    const entries = Object.entries(dxRatings).filter(([, v]) => v >= 1 && v <= 5);
    if (entries.length === 0) return;
    const token = generateToken();
    // Each dimension is its own row, but they share the same submitter token,
    // submitter email, and (optionally) sealed note. Token is what unlocks
    // self-service deletion of the whole submission.
    const rows = entries.map(([dim, rating], i) => ({
      facility_id: terminal.id,
      dimension: dim,
      rating: rating,
      // Attach the sealed note to the first row only, to avoid duplication
      notes_text: i === 0 && dxNote.trim() ? dxNote.trim() : null,
      submitter_role: "driver",
      submitter_token: token,
      submitter_email: dxEmail.trim() || null,
    }));
    // Batch insert — single REST call
    if (window.SI_DB.raw && window.SI_DB.raw.insert) {
      await window.SI_DB.raw.insert("facility_driver_ratings", rows);
    }
    setDxToken(token);
    setDxSubmitted(true);
    setDxFormOpen(false);
    setDxRatings({});
    setDxNote("");
    setDxEmail("");
    // Refetch aggregates
    try {
      const [scores, composite] = await Promise.all([
        window.SI_DB.facilityDriverScore(terminal.id),
        window.SI_DB.facilityDriverComposite(terminal.id),
      ]);
      setDxScores(scores || []);
      setDxComposite(composite);
    } catch (err) { /* ignore */ }
  }

  // Per-dimension averages keyed by id, for the breakdown badge
  const dxByDim = useMemoT(() => {
    const m = {};
    (dxScores || []).forEach((r) => { m[r.dimension] = r; });
    return m;
  }, [dxScores]);

  function StarRow({ value, onChange, ariaLabel }) {
    return (
      <div role="radiogroup" aria-label={ariaLabel} style={{ display: "inline-flex", gap: 4 }}>
        {[1, 2, 3, 4, 5].map((n) => (
          <button
            key={n}
            type="button"
            onClick={() => onChange(n)}
            aria-checked={value === n}
            role="radio"
            style={{
              border: 0,
              background: "transparent",
              cursor: "pointer",
              padding: "2px 4px",
              fontSize: 22,
              lineHeight: 1,
              color: value && n <= value ? "oklch(0.78 0.16 75)" : "var(--rule)",
            }}
          >★</button>
        ))}
      </div>
    );
  }

  function StarStatic({ value }) {
    if (value == null) return <span style={{ color: "var(--ink-soft)", fontSize: 13 }}>No ratings yet</span>;
    const filled = Math.round(value);
    return (
      <span style={{ display: "inline-flex", gap: 2 }}>
        {[1, 2, 3, 4, 5].map((n) => (
          <span key={n} style={{ fontSize: 16, lineHeight: 1, color: n <= filled ? "oklch(0.78 0.16 75)" : "var(--rule)" }}>★</span>
        ))}
      </span>
    );
  }

  // Photo handling — driver drops up to 4 image files. Each is uploaded
  // to Supabase Storage as soon as it's added so the submit click is
  // instant when the driver finishes the form. Files validated client-
  // side: image/* MIME, ≤8MB each. The path returned from upload is
  // what ends up in the intel row's attachments[] array.
  const MAX_PHOTOS = 4;
  const MAX_BYTES = 8 * 1024 * 1024;

  async function handlePhotoSelect(e) {
    setTipPhotoErr(null);
    const files = Array.from(e.target.files || []);
    e.target.value = ""; // allow re-selecting same file later
    const remaining = MAX_PHOTOS - tipPhotos.length;
    if (files.length > remaining) {
      setTipPhotoErr(`Up to ${MAX_PHOTOS} photos per tip — keeping the first ${remaining}.`);
      files.length = remaining;
    }
    // Pre-validate
    const goodFiles = [];
    for (const f of files) {
      if (!f.type || !f.type.startsWith("image/")) {
        setTipPhotoErr(`"${f.name}" isn't an image — skipped.`);
        continue;
      }
      if (f.size > MAX_BYTES) {
        setTipPhotoErr(`"${f.name}" is over 8MB — skipped.`);
        continue;
      }
      goodFiles.push(f);
    }
    if (!goodFiles.length) return;
    // Generate a stable token early so all photos for this submission
    // share the same namespace prefix in storage.
    const token = tipToken || generateToken();
    if (!tipToken) setTipToken(token);
    // Add placeholders + start uploads
    const placeholders = goodFiles.map(f => ({
      key: Math.random().toString(36).slice(2, 10),
      file: f,
      previewUrl: URL.createObjectURL(f),
      uploading: true,
      attachment: null,
      error: null,
    }));
    setTipPhotos(prev => [...prev, ...placeholders]);
    for (const ph of placeholders) {
      try {
        const att = await window.SI_DB.uploadTipPhoto(ph.file, token);
        setTipPhotos(prev => prev.map(p => p.key === ph.key
          ? { ...p, uploading: false, attachment: att, error: att ? null : "upload failed" }
          : p));
      } catch (err) {
        setTipPhotos(prev => prev.map(p => p.key === ph.key
          ? { ...p, uploading: false, error: String(err && err.message || err) }
          : p));
      }
    }
  }
  function removePhoto(key) {
    setTipPhotos(prev => {
      const removing = prev.find(p => p.key === key);
      if (removing && removing.previewUrl) URL.revokeObjectURL(removing.previewUrl);
      return prev.filter(p => p.key !== key);
    });
  }

  async function submitTip(e) {
    e.preventDefault();
    if (!tipInput.trim()) return;
    // Don't submit while any photo upload is still in flight — the
    // attachments[] array would be incomplete.
    if (tipPhotos.some(p => p.uploading)) return;
    const token = tipToken || generateToken();
    const attachments = tipPhotos
      .map(p => p.attachment)
      .filter(Boolean);
    await window.SI_DB.submitIntel({
      facility_id: terminal.id,
      carrier_id: terminal.parent_id,
      city_id: terminal.primary_city_id,
      submitter_role: tipRole,
      raw_text: tipInput.trim(),
      severity: tipSeverity,
      visibility: tipPublic ? "public" : "sealed",
      submitter_token: token,
      submitter_email: tipEmail.trim() || null,
      status: "pending",
      attachments: attachments.length ? attachments : null,
    });
    setTipInput("");
    setTipEmail("");
    setTipPublic(false);
    setTipToken(token);
    setTipSubmitted(true);
    // Clean up object URLs after submit
    tipPhotos.forEach(p => p.previewUrl && URL.revokeObjectURL(p.previewUrl));
    setTipPhotos([]);
  }

  if (loading) {
    return (
      <div className="terminal-page" style={{ padding: 80, textAlign: "center", color: "var(--ink-soft)" }}>
        Loading terminal…
      </div>
    );
  }

  if (!terminal) {
    return (
      <div className="terminal-page" style={{ padding: 80, textAlign: "center" }}>
        <h2 style={{ fontFamily: "var(--font-serif)", fontSize: 32, marginBottom: 12 }}>Terminal not found</h2>
        <p style={{ color: "var(--ink-soft)" }}>We don't have this facility in the atlas yet.</p>
        <button className="btn-primary" onClick={() => onNav("home")} style={{ marginTop: 24 }}>← Back home</button>
      </div>
    );
  }

  return (
    <div className="terminal-page">
      <style>{`
        .term-hero { padding: 56px 24px 28px; max-width: 1100px; margin: 0 auto; }
        .term-eyebrow { font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.18em;
          text-transform: uppercase; color: var(--ink-soft); margin-bottom: 12px; }
        .term-title { font-family: var(--font-serif); font-size: 48px; line-height: 1.05;
          letter-spacing: -0.02em; margin: 0 0 8px; }
        .term-sub { color: var(--ink-soft); font-size: 17px; }
        .term-status-banner { background: oklch(0.94 0.06 50); border-left: 3px solid oklch(0.65 0.16 50);
          padding: 14px 18px; margin: 20px 0 0; border-radius: 4px; font-size: 14px; }
        .term-status-banner.closed { background: oklch(0.94 0.06 25); border-left-color: oklch(0.55 0.18 25); }
        .term-grid { max-width: 1100px; margin: 32px auto; padding: 0 24px;
          display: grid; grid-template-columns: 2fr 1fr; gap: 32px; }
        @media (max-width: 800px) { .term-grid { grid-template-columns: 1fr; } }
        .term-card { background: var(--paper); border: 1px solid var(--rule); border-radius: 6px; padding: 24px; }
        .term-card h3 { font-family: var(--font-serif); font-size: 22px; margin: 0 0 16px; letter-spacing: -0.01em; }
        .term-fact { display: flex; gap: 14px; padding: 10px 0; border-bottom: 1px solid var(--rule); font-size: 14px; }
        .term-fact:last-child { border-bottom: none; }
        .term-fact .k { width: 130px; flex-shrink: 0; color: var(--ink-soft); font-family: var(--font-mono); font-size: 11px;
          letter-spacing: 0.1em; text-transform: uppercase; padding-top: 2px; }
        .term-fact .v { color: var(--ink); }
        .term-wait-big { font-family: var(--font-serif); font-size: 56px; line-height: 1; margin: 0; letter-spacing: -0.02em; }
        .term-wait-unit { font-size: 18px; color: var(--ink-soft); margin-left: 6px; font-family: var(--font-sans); }
        .term-wait-meta { color: var(--ink-soft); font-size: 13px; margin-top: 4px; }
        .term-tip { padding: 14px 0; border-bottom: 1px solid var(--rule); }
        .term-tip:last-child { border-bottom: none; }
        .term-tip-text { color: var(--ink); font-size: 14px; line-height: 1.5; }
        .term-tip-meta { color: var(--ink-soft); font-size: 12px; font-family: var(--font-mono);
          letter-spacing: 0.05em; margin-top: 4px; text-transform: uppercase; }
        .term-form input, .term-form textarea, .term-form select {
          width: 100%; border: 1px solid var(--rule); border-radius: 4px;
          padding: 10px 12px; font-size: 14px; font-family: inherit; box-sizing: border-box; }
        .term-form textarea { min-height: 70px; resize: vertical; }
        .term-form-row { margin-bottom: 10px; }
        .term-form-submitted { color: oklch(0.55 0.14 145); font-size: 14px; padding: 12px;
          background: oklch(0.96 0.04 145); border-radius: 4px; }
        .term-back { display: inline-flex; align-items: center; gap: 6px; color: var(--ink-soft);
          font-size: 13px; margin-bottom: 20px; cursor: pointer; }
        .term-back:hover { color: var(--ink); }
      `}</style>

      <div className="term-hero">
        <a className="term-back" onClick={() => onNav("home")}>← Atlas home</a>
        <div className="term-eyebrow">
          {parent ? parent.name : "Independent"} · Terminal {city ? `· ${city.name}, ${city.state}` : ""}
        </div>
        <h1 className="term-title">{terminal.name}</h1>
        <p className="term-sub">{terminal.address || "Address not yet on file"}</p>

        {currentStatus && currentStatus.status !== "open" && (
          <div className={`term-status-banner ${currentStatus.status === "closed" ? "closed" : ""}`}>
            🚧 <strong style={{ textTransform: "capitalize" }}>{currentStatus.status.replace("_", " ")}</strong>
            {currentStatus.end_date ? ` through ${new Date(currentStatus.end_date).toLocaleDateString("en-US", { month: "short", year: "numeric" })}` : ""}
            {currentStatus.notes ? ` — ${currentStatus.notes}` : ""}
          </div>
        )}
      </div>

      <div className="term-grid">
        <div>
          <div className="term-card">
            <h3>Facility</h3>
            <div className="term-fact"><span className="k">Address</span><span className="v">{terminal.address || "—"}</span></div>
            {(terminal.lat != null && terminal.lng != null) && (
              <div className="term-fact">
                <span className="k">Coordinates</span>
                <span className="v" style={{ fontFamily: "var(--font-mono)", fontSize: 13 }}>
                  {Number(terminal.lat).toFixed(5)}, {Number(terminal.lng).toFixed(5)}
                </span>
              </div>
            )}
            {attrMap.driver_entrance && <div className="term-fact"><span className="k">Driver entrance</span><span className="v">{attrMap.driver_entrance}</span></div>}
            {attrMap.hours_driver && <div className="term-fact"><span className="k">Hours</span><span className="v">{attrMap.hours_driver}</span></div>}
            {attrMap.phone && <div className="term-fact"><span className="k">Phone</span><span className="v">{attrMap.phone}</span></div>}
            {attrMap.services && <div className="term-fact"><span className="k">Services</span><span className="v">{attrMap.services}</span></div>}
            {attrMap.aircraft_capacity && <div className="term-fact"><span className="k">Air capacity</span><span className="v">{attrMap.aircraft_capacity}</span></div>}
            {parent && <div className="term-fact"><span className="k">Operated by</span><span className="v">{parent.name}</span></div>}
            {(terminal.address || (terminal.lat && terminal.lng)) && (() => {
              // Use the official Google Maps URL API (api=1) so links drop a
              // pin and reverse-geocode the address. The legacy /@lat,lng,zoom
              // syntax centers but doesn't pin, which made users think the
              // wrong location loaded.
              //   Satellite: search with address query + basemap=satellite
              //   StreetView: pano action at coordinates
              //   Directions: destination by address (better than coords for
              //              truck-legal routing) with coords as fallback
              const addrQ = terminal.address ? encodeURIComponent(terminal.address) : null;
              const llQ   = (terminal.lat && terminal.lng) ? `${terminal.lat},${terminal.lng}` : null;
              // All three buttons use the official Maps URL API (api=1) so
              // they drop pins / lock onto the correct address rather than
              // snapping to a nearest-pano default.
              //
              // Street view: uses search?layer=c which forces StreetView on
              // the searched address — viewpoint=<lat,lng> was snapping to
              // the wrong-side-of-the-block pano in some cases.
              //
              // No truck-routing button: Google Maps consumer is car-only;
              // the Trucker Path deep link (truckerpath://) didn't reliably
              // register. We tell users plainly. Real truck routing comes
              // post-launch via a HERE WeGo / Trimble integration.
              const satHref = `https://www.google.com/maps/search/?api=1&query=${addrQ || llQ}&basemap=satellite`;
              const svHref  = `https://www.google.com/maps/search/?api=1&query=${addrQ || llQ}&layer=c`;
              const dirHref = `https://www.google.com/maps/dir/?api=1&destination=${addrQ || llQ}&travelmode=driving`;
              return (
                <>
                  <div style={{ display: "flex", gap: 8, marginTop: 14, flexWrap: "wrap" }}>
                    <a className="term-map-btn" href={satHref} target="_blank" rel="noopener noreferrer">
                      🛰  Satellite view →
                    </a>
                    <a className="term-map-btn" href={svHref} target="_blank" rel="noopener noreferrer">
                      📷  Street view →
                    </a>
                    <a className="term-map-btn" href={dirHref} target="_blank" rel="noopener noreferrer"
                       title="Google Maps · passenger-car routing">
                      🗺  Open in Google Maps →
                    </a>
                  </div>
                  <p style={{ fontSize: 11, color: "var(--ink-soft)", marginTop: 8, fontStyle: "italic" }}>
                    Directions above are passenger-car routing. Drivers should use a truck-legal
                    nav app (Trucker Path, Sygic Truck, HERE WeGo Truck) for HOS-, weight-, and
                    bridge-aware routing.
                  </p>
                </>
              );
            })()}
          </div>

          <div className="term-card" style={{ marginTop: 20 }}>
            <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", flexWrap: "wrap", gap: 12 }}>
              <h3 style={{ margin: 0 }}>Driver experience</h3>
              <div style={{ fontSize: 12, color: "var(--ink-soft)", fontFamily: "var(--font-mono)", letterSpacing: "0.06em", textTransform: "uppercase" }}>
                {dxComposite && dxComposite.total_ratings > 0
                  ? `${dxComposite.total_ratings} rating${dxComposite.total_ratings === 1 ? "" : "s"}`
                  : "No ratings yet"}
              </div>
            </div>

            {dxComposite && dxComposite.composite_rating != null ? (
              <div style={{ display: "flex", alignItems: "center", gap: 16, padding: "16px 0", borderBottom: "1px solid var(--rule)", marginBottom: 14 }}>
                <div style={{ fontFamily: "var(--font-serif)", fontSize: 44, lineHeight: 1, letterSpacing: "-0.02em" }}>
                  {Number(dxComposite.composite_rating).toFixed(1)}
                  <span style={{ fontSize: 18, color: "var(--ink-soft)", fontFamily: "var(--font-sans)", marginLeft: 4 }}>/ 5</span>
                </div>
                <div>
                  <StarStatic value={Number(dxComposite.composite_rating)} />
                  <div style={{ fontSize: 12, color: "var(--ink-soft)", marginTop: 4, lineHeight: 1.4 }}>
                    Aggregated from drivers who rated this yard. Individual ratings stay sealed.
                  </div>
                </div>
              </div>
            ) : (
              <p style={{ color: "var(--ink-soft)", fontSize: 14, marginTop: 8 }}>
                Be the first to rate this facility on what matters to drivers.
              </p>
            )}

            {/* Per-dimension breakdown */}
            <div style={{ display: "grid", gridTemplateColumns: "1fr", gap: 8, marginBottom: 16 }}>
              {DX_DIMS.map((d) => {
                const row = dxByDim[d.id];
                const avg = row ? Number(row.avg_rating) : null;
                const cnt = row ? row.rating_count : 0;
                return (
                  <div key={d.id} style={{ display: "flex", alignItems: "center", gap: 10, fontSize: 13, padding: "6px 0", borderBottom: "1px solid var(--rule)" }}>
                    <div style={{ flex: "0 0 170px" }}>
                      <div style={{ color: "var(--ink)", fontWeight: 500 }}>{d.label}</div>
                      <div style={{ color: "var(--ink-soft)", fontSize: 11 }}>{d.hint}</div>
                    </div>
                    <div style={{ flex: 1, display: "flex", alignItems: "center", gap: 8 }}>
                      <StarStatic value={avg} />
                      {cnt > 0 && (
                        <span style={{ color: "var(--ink-soft)", fontSize: 12, fontFamily: "var(--font-mono)" }}>
                          {avg.toFixed(1)} · {cnt}
                        </span>
                      )}
                    </div>
                  </div>
                );
              })}
            </div>

            {dxSubmitted ? (
              <div className="term-form-submitted">
                <div style={{ fontWeight: 600, marginBottom: 6 }}>✓ Ratings logged.</div>
                <div style={{ fontSize: 13, color: "var(--ink-soft)", marginBottom: 8 }}>
                  Thanks. Aggregate scores update immediately; your individual ratings stay sealed. Save this URL if you want to delete what you just submitted later:
                </div>
                {dxToken && (
                  <code style={{ fontSize: 11, wordBreak: "break-all", display: "block", padding: 8, background: "var(--paper-2)", borderRadius: 4 }}>
                    {window.location.origin}/#/manage?action=delete-rating&token={dxToken}
                  </code>
                )}
              </div>
            ) : !dxFormOpen ? (
              <button className="btn-primary" onClick={() => setDxFormOpen(true)}>
                Rate this yard
              </button>
            ) : (
              <form onSubmit={submitDxRatings} className="term-form">
                <div style={{ fontSize: 12, color: "var(--ink-soft)", marginBottom: 12, lineHeight: 1.5 }}>
                  Skip any dimension you can't speak to. Your ratings are sealed — only the aggregate is shown publicly.
                </div>
                {DX_DIMS.map((d) => (
                  <div key={d.id} style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "8px 0", borderBottom: "1px solid var(--rule)", gap: 12 }}>
                    <div style={{ flex: 1, minWidth: 0 }}>
                      <div style={{ fontWeight: 500, fontSize: 13 }}>{d.label}</div>
                      <div style={{ fontSize: 11, color: "var(--ink-soft)" }}>{d.hint}</div>
                    </div>
                    <StarRow
                      value={dxRatings[d.id]}
                      onChange={(n) => setDxRatings({ ...dxRatings, [d.id]: n })}
                      ariaLabel={d.label}
                    />
                  </div>
                ))}
                <div className="term-form-row" style={{ marginTop: 12 }}>
                  <textarea
                    value={dxNote}
                    onChange={(e) => setDxNote(e.target.value)}
                    placeholder="Optional sealed note (never displayed publicly — for our editorial review only)"
                  />
                </div>
                <div className="term-form-row">
                  <input
                    type="email"
                    value={dxEmail}
                    onChange={(e) => setDxEmail(e.target.value)}
                    placeholder="Email (optional — only for managing/deleting your ratings)"
                  />
                </div>
                <div style={{ display: "flex", gap: 8 }}>
                  <button className="btn-primary" type="submit" disabled={Object.values(dxRatings).filter(Boolean).length === 0}>
                    Submit ratings
                  </button>
                  <button className="btn-ghost" type="button" onClick={() => setDxFormOpen(false)}>Cancel</button>
                </div>
              </form>
            )}
          </div>

          <div className="term-card" style={{ marginTop: 20 }}>
            <h3>Driver tips ({tips.length})</h3>
            {tips.length === 0 ? (
              <p style={{ color: "var(--ink-soft)", fontSize: 14 }}>No tips yet — be the first.</p>
            ) : (
              tips.map((t, i) => (
                <div key={i} className="term-tip">
                  <div className="term-tip-text">"{t.raw_text}"</div>
                  <div className="term-tip-meta">{t.submitter_role} · {timeAgo(t.created_at)}</div>
                </div>
              ))
            )}

            <div style={{ marginTop: 18, paddingTop: 18, borderTop: "1px solid var(--rule)" }}>
              {tipSubmitted ? (
                <div className="term-form-submitted">
                  <div style={{ fontWeight: 600, marginBottom: 6 }}>✓ Tip submitted.</div>
                  <div style={{ fontSize: 13, color: "var(--ink-soft)", marginBottom: 8 }}>
                    Editor will review before publishing. Save this URL to manage or delete your submission later:
                  </div>
                  {tipToken && (
                    <code style={{ fontSize: 11, wordBreak: "break-all", display: "block", padding: 8, background: "var(--paper-2)", borderRadius: 4 }}>
                      {window.location.origin}/#/manage?token={tipToken}
                    </code>
                  )}
                </div>
              ) : (
                <form onSubmit={submitTip} className="term-form">
                  <div className="term-form-row">
                    <select value={tipRole} onChange={(e) => setTipRole(e.target.value)}>
                      <option value="driver">I'm a driver here</option>
                      <option value="dispatcher">I'm a dispatcher</option>
                      <option value="manager">I work here (other)</option>
                      <option value="shipper">I'm a shipper</option>
                      <option value="anonymous">Anonymous</option>
                    </select>
                  </div>
                  <div className="term-form-row">
                    <textarea
                      value={tipInput}
                      onChange={(e) => setTipInput(e.target.value)}
                      placeholder="Share a tip — dock numbers, contact names, hours, what to avoid…"
                    />
                  </div>
                  <div className="term-form-row">
                    <select value={tipSeverity} onChange={(e) => setTipSeverity(e.target.value)}>
                      <option value="low">Severity: Low — heads-up</option>
                      <option value="moderate">Severity: Moderate — recurring issue</option>
                      <option value="high">Severity: High — affecting many drivers</option>
                      <option value="critical">Severity: Critical — safety / urgent</option>
                    </select>
                  </div>
                  <label className="term-form-row" style={{ display: "flex", alignItems: "flex-start", gap: 8, fontSize: 13, color: "var(--ink)", lineHeight: 1.4, cursor: "pointer" }}>
                    <input type="checkbox" checked={tipPublic} onChange={(e) => setTipPublic(e.target.checked)} style={{ marginTop: 2, width: "auto" }} />
                    <span>
                      <strong>Publish this on the terminal page</strong> (anonymous — your name never shown). We'll also formally contact the carrier and post their response alongside.
                    </span>
                  </label>
                  <div className="term-form-row" style={{ fontSize: 12, color: "var(--ink-soft)", lineHeight: 1.5, padding: "8px 12px", background: "var(--paper-2)", borderRadius: 4 }}>
                    <strong>What happens to your tip:</strong> If unchecked, it's <em>sealed</em> — we use it as anonymous data ("Reports of [type], severity [level]") that the carrier sees as counts only, never your words. If checked, it's published as journalism (with carrier response). Either way, you can delete it any time.
                  </div>
                  <div className="term-form-row">
                    <input
                      type="email"
                      value={tipEmail}
                      onChange={(e) => setTipEmail(e.target.value)}
                      placeholder="Email (optional — only for managing/deleting your tip)"
                      style={{ width: "100%" }}
                    />
                  </div>

                  {/* Photo attachments — up to 4 images per tip. Sealed photos
                      stay sealed (admin reviews via service role); public tips
                      surface them in the carrier-response journalism. */}
                  <div className="term-form-row">
                    <label style={{
                      display: "inline-flex", alignItems: "center", gap: 8,
                      padding: "8px 14px", border: "1px dashed var(--rule)",
                      borderRadius: 4, cursor: "pointer", fontSize: 13,
                      color: "var(--ink-soft)", background: "var(--paper)",
                    }}>
                      📎 Add photos {tipPhotos.length > 0 ? `(${tipPhotos.length}/${MAX_PHOTOS})` : `(optional, up to ${MAX_PHOTOS})`}
                      <input
                        type="file"
                        accept="image/*"
                        multiple
                        onChange={handlePhotoSelect}
                        disabled={tipPhotos.length >= MAX_PHOTOS}
                        style={{ display: "none" }}
                      />
                    </label>
                    {tipPhotoErr && (
                      <div style={{
                        marginTop: 6, fontSize: 12, color: "oklch(0.45 0.18 25)",
                      }}>{tipPhotoErr}</div>
                    )}
                    {tipPhotos.length > 0 && (
                      <div style={{
                        marginTop: 10, display: "grid",
                        gridTemplateColumns: "repeat(auto-fill, minmax(96px, 1fr))",
                        gap: 8,
                      }}>
                        {tipPhotos.map(p => (
                          <div key={p.key} style={{
                            position: "relative", aspectRatio: "1 / 1",
                            background: "var(--paper-2)", borderRadius: 4,
                            border: "1px solid var(--rule)", overflow: "hidden",
                          }}>
                            <img src={p.previewUrl} alt="" style={{
                              width: "100%", height: "100%", objectFit: "cover",
                              display: "block",
                            }} />
                            {(p.uploading || p.error) && (
                              <div style={{
                                position: "absolute", inset: 0,
                                background: p.error ? "rgba(180,30,30,0.55)" : "rgba(0,0,0,0.45)",
                                display: "flex", alignItems: "center",
                                justifyContent: "center",
                                color: "#fff", fontFamily: "var(--font-mono)",
                                fontSize: 10, letterSpacing: "0.12em",
                                textTransform: "uppercase",
                              }}>
                                {p.error ? "Failed" : "Uploading…"}
                              </div>
                            )}
                            <button
                              type="button"
                              onClick={() => removePhoto(p.key)}
                              aria-label="Remove photo"
                              style={{
                                position: "absolute", top: 4, right: 4,
                                width: 22, height: 22, borderRadius: "50%",
                                background: "rgba(0,0,0,0.6)", color: "#fff",
                                border: 0, cursor: "pointer", lineHeight: 1,
                                fontSize: 14, padding: 0,
                              }}
                            >×</button>
                          </div>
                        ))}
                      </div>
                    )}
                    <div style={{
                      marginTop: 6, fontSize: 11, color: "var(--ink-soft)",
                      lineHeight: 1.4,
                    }}>
                      Photos sealed by default — only the editor sees them in admin review. If you publish the tip, photos surface alongside the carrier response per journalism review.
                    </div>
                  </div>

                  <button className="btn-primary" type="submit"
                    disabled={tipPhotos.some(p => p.uploading)}>
                    {tipPhotos.some(p => p.uploading) ? "Uploading photos…" : "Submit tip"}
                  </button>
                </form>
              )}
            </div>
          </div>
        </div>

        <div>
          <div className="term-card">
            <h3>Live wait time</h3>
            {avgWait !== null ? (
              <>
                <p className="term-wait-big">{avgWait}<span className="term-wait-unit">min avg</span></p>
                <p className="term-wait-meta">Last {waits.length} reports · most recent {timeAgo(waits[0].created_at)}</p>
              </>
            ) : (
              <p style={{ color: "var(--ink-soft)" }}>No reports yet.</p>
            )}

            <div style={{ marginTop: 18, paddingTop: 18, borderTop: "1px solid var(--rule)" }}>
              {waitSubmitted ? (
                <div className="term-form-submitted">✓ Logged. Drivers behind you say thanks.</div>
              ) : (
                <form onSubmit={submitWait} className="term-form">
                  <div className="term-form-row" style={{ display: "flex", gap: 8 }}>
                    <input
                      type="number"
                      min="0"
                      max="600"
                      value={waitInput}
                      onChange={(e) => setWaitInput(e.target.value)}
                      placeholder="Currently waiting (min)"
                      style={{ flex: 1 }}
                    />
                    <button className="btn-primary" type="submit">Log</button>
                  </div>
                </form>
              )}
            </div>
          </div>

          {parent && (
            <div className="term-card" style={{ marginTop: 20 }}>
              <h3>This terminal feeds</h3>
              <p style={{ color: "var(--ink-soft)", fontSize: 14, lineHeight: 1.6 }}>
                Wait times, tips, and status changes here contribute to <strong>{parent.name}</strong>'s
                Infrastructure score{city ? ` in ${city.name}` : ""}.
              </p>
              {city && (
                <button
                  className="btn-ghost"
                  onClick={() => onNav("report")}
                  style={{ marginTop: 12 }}
                >View {city.name} report card →</button>
              )}
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

window.TerminalPage = TerminalPage;
