/* Activities — list + detail */

/* Mock fallback para "Volumen semanal · km" (4 semanas).
 * Coincide con el shape del endpoint /data/activities/summary?bucket=week&n=4
 * y se usa SOLO si: (a) modo demo; o (b) el backend está caído. */
const VOLUME_MOCK_PERIODS = [
  { period_start: "2026-04-13", period_end: "2026-04-19", label: "S-3", count: 3, total_distance_m: 32000, total_duration_s: 11400, by_type: { running: 3, cycling: 0 } },
  { period_start: "2026-04-20", period_end: "2026-04-26", label: "S-2", count: 5, total_distance_m: 38000, total_duration_s: 14000, by_type: { running: 4, cycling: 1 } },
  { period_start: "2026-04-27", period_end: "2026-05-03", label: "S-1", count: 5, total_distance_m: 42000, total_duration_s: 15300, by_type: { running: 4, cycling: 1 } },
  { period_start: "2026-05-04", period_end: "2026-05-10", label: "Esta", count: 6, total_distance_m: 49000, total_duration_s: 18000, by_type: { running: 4, cycling: 2 } },
];

const Activities = ({ density, dataState, setRoute, inDemo }) => {
  const [selected, setSelected] = React.useState(null);
  const [filter, setFilter] = React.useState("all");
  // En demo arrancamos con MOCK; en producción arrancamos vacío y mostramos
  // empty state si no hay nada sincronizado.
  const [activities, setActivities] = React.useState(() => inDemo ? MOCK.activities : []);
  const [activitiesLoaded, setActivitiesLoaded] = React.useState(inDemo);

  // Volumen semanal: cableado a /data/activities/summary. En producción
  // mostramos array vacío (sin badge falso) si no hay datos o el backend falla.
  const [volumePeriods, setVolumePeriods] = React.useState(() => inDemo ? VOLUME_MOCK_PERIODS : []);
  const [volumeLoading, setVolumeLoading] = React.useState(!inDemo);

  React.useEffect(() => {
    if (inDemo) {
      setActivities(MOCK.activities);
      setVolumePeriods(VOLUME_MOCK_PERIODS);
      setActivitiesLoaded(true);
      setVolumeLoading(false);
      return;
    }
    let alive = true;
    (async () => {
      try {
        const res = await API.activitiesList({ limit: 50 });
        const items = Array.isArray(res) ? res : (res?.rows || res?.items || res?.activities || []);
        if (alive) setActivities(items.map(window.normalizeActivityRow));
      } catch (e) {
        if (e.status === 401 || e.status === 403 || e.status === 422) return;
        // network error → quedamos con [] (empty state honesto, no MOCK)
      } finally {
        if (alive) setActivitiesLoaded(true);
      }

      // Cargar volumen semanal del backend.
      if (!alive) return;
      try {
        const res = await API.activitiesSummary({ bucket: "week", n: 4 });
        if (!alive) return;
        setVolumePeriods(Array.isArray(res?.periods) ? res.periods : []);
      } catch (e) {
        if (!alive) return;
        if (e.status === 401 || e.status === 403 || e.status === 422) return;
        // Error de red → vacío (no MOCK con badge)
        setVolumePeriods([]);
      } finally {
        if (alive) setVolumeLoading(false);
      }
    })();
    return () => { alive = false; };
  }, [inDemo]);

  if (selected) return <ActivityDetail id={selected} activities={activities} onBack={() => setSelected(null)} setRoute={setRoute} inDemo={inDemo} />;

  const filtered = filter === "all" ? activities : activities.filter(a => (a.type || "").toLowerCase() === filter);

  return (
    <div className="af-padded" style={{ padding: "var(--sp-6) var(--sp-8) 80px", display: "flex", flexDirection: "column", gap: 18 }}>
      <div style={{ display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" }}>
        {[
          { v: "all", l: "Todas" },
          { v: "carrera", l: "Carrera" },
          { v: "fuerza", l: "Fuerza" },
          { v: "bici", l: "Bici" },
        ].map(f => (
          <Chip key={f.v} active={filter === f.v} onClick={() => setFilter(f.v)}>{f.l}</Chip>
        ))}
        <div style={{ flex: 1 }} />
        <span style={{ fontSize: 11.5, color: "var(--text-3)", fontFamily: "var(--font-mono)" }}>
          {(() => {
            const thisWeekKm = volumePeriods && volumePeriods.length > 0
              ? Number(volumePeriods[volumePeriods.length - 1]?.total_distance_m || 0) / 1000
              : 0;
            const weekTxt = thisWeekKm > 0 ? ` · ${thisWeekKm.toFixed(1)} km esta semana` : "";
            return `${filtered.length} actividades${weekTxt}`;
          })()}
        </span>
      </div>

      {/* Volume bars — sólo cuando hay periodos reales o estamos en demo. */}
      {(inDemo || volumePeriods.length > 0 || volumeLoading) && (
        <VolumeBars
          periods={volumePeriods}
          loading={volumeLoading}
          fallback={false}
          inDemo={inDemo}
        />
      )}

      {!activitiesLoaded ? (
        <Card>
          <div style={{ padding: "30px 0", textAlign: "center", color: "var(--text-3)", fontFamily: "var(--font-mono)", fontSize: 12 }}>
            cargando actividades…
          </div>
        </Card>
      ) : filtered.length === 0 ? (
        <Card>
          <div style={{
            padding: "40px 20px", textAlign: "center",
            fontSize: 13, color: "var(--text-3)",
          }}>
            <div style={{ fontFamily: "var(--font-mono)", fontSize: 11, marginBottom: 6 }}>
              {activities.length === 0 ? "SIN ACTIVIDADES" : `SIN ACTIVIDADES DE TIPO "${filter.toUpperCase()}"`}
            </div>
            <div style={{ color: "var(--text-2)" }}>
              {activities.length === 0
                ? "Conectá un proveedor (Garmin, etc.) y esperá al primer sync."
                : "Probá con otro filtro o sincronizá más historial."}
            </div>
          </div>
        </Card>
      ) : (
        <Card padded={false}>
          <table style={{ width: "100%", borderCollapse: "collapse", fontSize: 12.5 }}>
            <thead>
              <tr style={{ borderBottom: "1px solid var(--line)" }}>
                {["Fecha", "Tipo", "Detalle", "Duración", "Distancia", "FC", "Pace", "Carga", "TE", ""].map((h) => (
                  <th key={h} style={{ padding: "12px 14px", textAlign: "left", fontSize: 10, fontFamily: "var(--font-mono)", color: "var(--text-3)", letterSpacing: 0.08, textTransform: "uppercase", fontWeight: 500 }}>
                    {h}
                  </th>
                ))}
              </tr>
            </thead>
            <tbody>
              {filtered.map((a, i) => (
                <tr key={a.id}
                  onClick={() => setSelected(a.id)}
                  style={{ cursor: "pointer", borderBottom: i === filtered.length - 1 ? "none" : "1px solid var(--line-soft)" }}
                  onMouseEnter={(e) => e.currentTarget.style.background = "var(--bg-2)"}
                  onMouseLeave={(e) => e.currentTarget.style.background = "transparent"}
                >
                  <td style={{ padding: "12px 14px", fontFamily: "var(--font-mono)", color: "var(--text-3)" }}>
                    {(a.date || "").replace("2026-", "")} <span style={{ color: "var(--text-4)" }}>{a.time}</span>
                  </td>
                  <td style={{ padding: "12px 14px" }}>
                    <Badge tone={a.type === "Carrera" ? "info" : a.type === "Fuerza" ? "accent" : "warn"}>{a.type}</Badge>
                  </td>
                  <td style={{ padding: "12px 14px", color: "var(--text-2)" }}>{a.subtype}</td>
                  <td style={{ padding: "12px 14px", fontFamily: "var(--font-mono)" }}>{a.dur}</td>
                  <td style={{ padding: "12px 14px", fontFamily: "var(--font-mono)" }}>{a.dist > 0 ? `${Number(a.dist).toFixed(2)} km` : "—"}</td>
                  <td style={{ padding: "12px 14px", fontFamily: "var(--font-mono)" }}>{a.hr}</td>
                  <td style={{ padding: "12px 14px", fontFamily: "var(--font-mono)" }}>{a.pace}</td>
                  <td style={{ padding: "12px 14px" }}>
                    <span className="num" style={{ fontWeight: 500 }}>{a.load}</span>
                  </td>
                  <td style={{ padding: "12px 14px" }}>
                    <span className="num" style={{ color: a.te >= 4 ? "var(--bad)" : a.te >= 3 ? "var(--warn)" : "var(--good)" }}>{Number(a.te || 0).toFixed(1)}</span>
                  </td>
                  <td style={{ padding: "12px 14px", textAlign: "right" }}>
                    <Icon name="chevR" size={12} color="var(--text-3)" />
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </Card>
      )}
    </div>
  );
};

/* Toma los puntos planos del backend (`/data/activities/{id}/streams`)
 * — un array de `{t_offset_s, hr, pace_ms, cadence_spm, power_w, altitude_m,
 *   lat, lon}` — y devuelve series listas para `StreamChart` y un track GPS
 * normalizado para el mini-mapa. Si no hay datos, devuelve `null`.
 */
function _processStreams(streams) {
  if (!streams) return null;
  const points = Array.isArray(streams.points) ? streams.points : null;
  if (points && points.length > 1) {
    const hr = points.map((p) => p.hr).filter((v) => v != null);
    const elevation = points.map((p) => p.altitude_m).filter((v) => v != null);
    const paceSecPerKm = points
      .map((p) => (p.pace_ms && p.pace_ms > 0 ? 1000 / p.pace_ms : null))
      .filter((v) => v != null && isFinite(v) && v < 1500);
    const cadence = points.map((p) => p.cadence_spm).filter((v) => v != null);
    const gps = points
      .filter((p) => p.lat != null && p.lon != null)
      .map((p) => [p.lat, p.lon]);
    return {
      hr: hr.length > 1 ? hr : null,
      pace: paceSecPerKm.length > 1 ? paceSecPerKm : null,
      elevation: elevation.length > 1 ? elevation : null,
      cadence: cadence.length > 1 ? cadence : null,
      gps: gps.length > 1 ? gps : null,
    };
  }
  // Compat hacia atrás: backend antiguo o mock que ya entrega `{hr, pace, elevation, cadence}`.
  const st = streams.streams || streams;
  if (st && (Array.isArray(st.hr) || Array.isArray(st.pace) || Array.isArray(st.elevation))) {
    return {
      hr: Array.isArray(st.hr) && st.hr.length > 1 ? st.hr : null,
      pace: Array.isArray(st.pace) && st.pace.length > 1 ? st.pace : null,
      elevation: Array.isArray(st.elevation) && st.elevation.length > 1 ? st.elevation : null,
      cadence: Array.isArray(st.cadence) && st.cadence.length > 1 ? st.cadence : null,
      gps: null,
    };
  }
  return null;
}

/* Convierte `extras.splits` (FIT laps) a filas listas para la tabla de splits.
 * Cada split: `{n, dur_s, dist_m, avg_hr, avg_pace_ms, avg_cadence, avg_power_w}`.
 */
function _normalizeSplits(extras) {
  const raw = extras && extras.splits;
  if (!Array.isArray(raw) || raw.length === 0) return [];
  return raw
    .map((sp) => {
      const distKm = sp.dist_m != null ? Number(sp.dist_m) / 1000 : null;
      const durS = sp.dur_s != null ? Number(sp.dur_s) : null;
      const paceMs = sp.avg_pace_ms != null ? Number(sp.avg_pace_ms) : null;
      // Pace m:ss/km — preferir avg_pace_ms (m/s) si existe, sino dur/dist.
      let secPerKm = null;
      if (paceMs && paceMs > 0) secPerKm = 1000 / paceMs;
      else if (durS && distKm) secPerKm = durS / distKm;
      return {
        n: sp.n,
        distKm,
        durS,
        paceStr: secPerKm && isFinite(secPerKm)
          ? `${Math.floor(secPerKm / 60)}:${String(Math.round(secPerKm % 60)).padStart(2, "0")}`
          : "—",
        secPerKm: secPerKm && isFinite(secPerKm) ? secPerKm : null,
        avgHr: sp.avg_hr != null ? Math.round(Number(sp.avg_hr)) : null,
        cadence: sp.avg_cadence != null ? Math.round(Number(sp.avg_cadence)) : null,
        power: sp.avg_power_w != null ? Math.round(Number(sp.avg_power_w)) : null,
      };
    })
    .filter((sp) => sp.distKm != null || sp.durS != null);
}

/* GpsMap — mapa real con tiles de OpenStreetMap vía Leaflet.
 *
 * Por qué OSM/Leaflet:
 *   - Sin API key, sin costo ($0), sin tracking.
 *   - Self-hosted-friendly (sólo necesitamos los tiles públicos de OSM).
 *   - Atribución automática en la esquina inferior derecha (requisito OSM).
 *   - Leaflet es ~40 KB minified, MIT, sin deps adicionales.
 *
 * El componente:
 *   1. Crea un map al montar y dibuja el track como polyline.
 *   2. fitBounds() centra/zoom-ajusta al track.
 *   3. Limpia el map al desmontar para no leakear instancias.
 *   4. Si no hay GPS, renderiza el placeholder "Sin GPS" sin tocar Leaflet.
 *   5. Mantiene el badge "GPS · N pts" + "elevación +Xm" como overlays.
 */
const GpsMap = ({ track, elevation }) => {
  const mapRef = React.useRef(null);
  const containerRef = React.useRef(null);

  React.useEffect(() => {
    if (!track || track.length < 2) return undefined;
    if (typeof window.L === "undefined") return undefined;
    if (!containerRef.current) return undefined;

    // Si quedó una instancia previa (re-render con otro track), la matamos.
    if (mapRef.current) {
      mapRef.current.remove();
      mapRef.current = null;
    }

    const L = window.L;
    const map = L.map(containerRef.current, {
      zoomControl: true,
      attributionControl: true,
      scrollWheelZoom: false,  // evita atrapar el scroll de la página
    });
    mapRef.current = map;

    // Tile provider: OSM standard. Si quisieras un dark-theme tile sin costo,
    // CartoDB tiene "dark_all" gratuito con atribución; lo dejamos como TODO.
    L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
      maxZoom: 19,
      attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
    }).addTo(map);

    const latlngs = track.map(([lat, lon]) => [lat, lon]);
    const polyline = L.polyline(latlngs, {
      color: "#e63946",
      weight: 4,
      opacity: 0.9,
      lineJoin: "round",
    }).addTo(map);

    // Markers de inicio (verde) y fin (azul) — circle markers para no
    // bajarnos los iconos PNG por defecto de Leaflet.
    L.circleMarker(latlngs[0], {
      radius: 6, color: "#2e7d32", fillColor: "#2e7d32", fillOpacity: 1, weight: 2,
    }).bindTooltip("Inicio", { direction: "top", offset: [0, -6] }).addTo(map);
    L.circleMarker(latlngs[latlngs.length - 1], {
      radius: 5, color: "#1976d2", fillColor: "#1976d2", fillOpacity: 1, weight: 2,
    }).bindTooltip("Fin", { direction: "top", offset: [0, -6] }).addTo(map);

    map.fitBounds(polyline.getBounds(), { padding: [22, 22] });

    // Recalcular tamaño cuando el contenedor termina de pintarse (Card hace
    // animaciones de mount). Sin esto a veces Leaflet calcula un viewport
    // de 0×0 y los tiles salen en la esquina.
    const resizeTimer = setTimeout(() => map.invalidateSize(), 100);

    return () => {
      clearTimeout(resizeTimer);
      if (mapRef.current) {
        mapRef.current.remove();
        mapRef.current = null;
      }
    };
  }, [track]);

  if (!track || track.length < 2) {
    return (
      <div style={{
        position: "relative", width: "100%", height: "100%",
        display: "flex", alignItems: "center", justifyContent: "center",
        background: "var(--bg-2)", color: "var(--text-3)",
        fontSize: 11, fontFamily: "var(--font-mono)", textAlign: "center", padding: 16,
      }}>
        Sin GPS<br />
        <span style={{ fontSize: 9, color: "var(--text-4)", marginTop: 4 }}>
          (esta actividad no tiene track registrado)
        </span>
      </div>
    );
  }

  return (
    <div style={{ position: "relative", width: "100%", height: "100%", background: "var(--bg-2)" }}>
      <div ref={containerRef} style={{ position: "absolute", inset: 0, borderRadius: "inherit" }} />
      <div style={{
        position: "absolute", top: 12, left: 12, zIndex: 500,
        padding: "4px 9px", background: "var(--bg)",
        borderRadius: 4, border: "1px solid var(--line)",
        fontSize: 10, fontFamily: "var(--font-mono)", color: "var(--text-3)",
        pointerEvents: "none",
      }}>
        GPS · {track.length} pts
      </div>
      {elevation != null && (
        <div style={{
          position: "absolute", bottom: 12, right: 12, zIndex: 500,
          padding: "4px 9px", background: "var(--bg)",
          borderRadius: 4, border: "1px solid var(--line)",
          fontSize: 10, fontFamily: "var(--font-mono)", color: "var(--text-3)",
          pointerEvents: "none",
        }}>
          elevación +{elevation}m
        </div>
      )}
    </div>
  );
};

const ActivityDetail = ({ id, activities, onBack, setRoute, inDemo }) => {
  // En demo permitimos caer al MOCK si el list no trae nada; en producción
  // arrancamos con la fila que ya teníamos en la lista (o null mientras
  // /data/activities/{id} responde) y NUNCA caemos a MOCK.activities[0].
  const list = activities && activities.length > 0
    ? activities
    : (inDemo ? MOCK.activities : []);
  const [a, setA] = React.useState(() => {
    const fromList = list.find(x => x.id === id);
    if (fromList) return fromList;
    return inDemo ? MOCK.activities[0] : null;
  });
  const [s, setS] = React.useState(() => inDemo
    ? { hr: MOCK.stream.hr, pace: MOCK.stream.pace, elevation: MOCK.stream.elevation, cadence: MOCK.stream.cadence, gps: null }
    : null);
  const [streamsLoading, setStreamsLoading] = React.useState(!inDemo);

  React.useEffect(() => {
    if (inDemo) return;
    let alive = true;
    setStreamsLoading(true);
    (async () => {
      try {
        const detail = await API.activityDetail(id);
        if (alive && detail) setA(window.normalizeActivityRow(detail));
      } catch (e) {
        if (e.status === 401 || e.status === 403 || e.status === 422) return;
      }
      try {
        const streams = await API.activityStreams(id);
        if (alive) {
          const processed = _processStreams(streams);
          setS(processed);
        }
      } catch (e) {
        if (alive && !(e.status === 401 || e.status === 403 || e.status === 422)) {
          setS(null);
        }
      } finally {
        if (alive) setStreamsLoading(false);
      }
    })();
    return () => { alive = false; };
  }, [id, inDemo]);

  // useMemo SIEMPRE se llama (rules of hooks). Si `a` es null devuelve [].
  const splits = React.useMemo(() => a ? _normalizeSplits(a.extras) : [], [a]);

  // `a` puede ser null por una fracción de segundo (entre que se entra al
  // detalle y que /data/activities/{id} responde). Mientras tanto, render
  // de loading. Sin esto, los a.X de abajo truenan.
  if (!a) {
    return (
      <div className="af-padded" style={{ padding: "var(--sp-6) var(--sp-8) 80px" }}>
        <button onClick={onBack} style={{ display: "inline-flex", gap: 6, alignItems: "center", background: "none", border: "none", color: "var(--text-3)", fontSize: 12, padding: 0, marginBottom: 14 }}>
          ← Actividades
        </button>
        <div style={{ padding: "40px 0", textAlign: "center", color: "var(--text-3)", fontFamily: "var(--font-mono)", fontSize: 13 }}>
          cargando actividad…
        </div>
      </div>
    );
  }

  const fastestSplit = splits.length > 0
    ? Math.min(...splits.filter((sp) => sp.secPerKm != null).map((sp) => sp.secPerKm))
    : null;

  const hasGps = !!(s && s.gps && s.gps.length > 1);
  const hasStreams = !!(s && (s.hr || s.pace || s.elevation));
  const hasSplits = splits.length > 0;

  return (
    <div className="af-padded af-table-scroll" style={{ padding: "var(--sp-6) var(--sp-8) 80px", display: "flex", flexDirection: "column", gap: 14 }}>
      <button onClick={onBack} style={{ display: "inline-flex", gap: 6, alignItems: "center", background: "none", border: "none", color: "var(--text-3)", fontSize: 12, padding: 0, alignSelf: "flex-start" }}>
        <Icon name="chevR" size={11} color="var(--text-3)" /><span style={{ transform: "scaleX(-1)", display: "inline-block" }}>{""}</span>
        <span style={{ marginLeft: -10 }}>← Actividades</span>
      </button>

      <div style={{ display: "flex", alignItems: "flex-end", gap: 14 }}>
        <div>
          <div className="eyebrow" style={{ marginBottom: 6 }}>{a.date.replace("2026-", "")} · {a.time}{a.source ? ` · ${a.source}` : ""}</div>
          <h2 className="display" style={{ margin: 0, fontSize: 28, fontWeight: 400, letterSpacing: "-0.02em" }}>
            {a.subtype || a.type}
          </h2>
          <div style={{ marginTop: 4, fontSize: 13, color: "var(--text-2)" }}>
            {a.type}
            {a.dist > 0 ? ` · ${Number(a.dist).toFixed(2)} km` : ""}
            {a.dur && a.dur !== "—" ? ` en ${a.dur}` : ""}
            {a.pace && a.pace !== "—" ? ` a ${a.pace}/km` : ""}
          </div>
        </div>
        <div style={{ flex: 1 }} />
        <Btn variant="primary" icon={<Icon name="chat" size={13} />} onClick={() => setRoute("agents")}>
          Pregúntale al coach sobre esta actividad
        </Btn>
      </div>

      {/* Big stats — sólo lo que tiene valor real (no inventamos). */}
      <Card>
        <div className="af-grid-6" style={{ display: "grid", gridTemplateColumns: "repeat(6, 1fr)", gap: 18 }}>
          {[
            a.dist > 0 ? { l: "Distancia", v: Number(a.dist).toFixed(2), u: "km" } : null,
            a.dur && a.dur !== "—" ? { l: "Duración", v: a.dur, u: "" } : null,
            a.pace && a.pace !== "—" ? { l: "Pace medio", v: a.pace, u: "/km" } : null,
            a.hr ? { l: "FC media", v: String(a.hr), u: "bpm" } : null,
            a.calories ? { l: "Calorías", v: String(a.calories), u: "kcal" } : null,
            a.elevation ? { l: "Desnivel", v: `+${a.elevation}`, u: "m" } : null,
          ].filter(Boolean).map((x, i) => (
            <div key={i}>
              <div className="eyebrow" style={{ fontSize: 9.5, marginBottom: 4 }}>{x.l}</div>
              <div style={{ display: "flex", alignItems: "baseline", gap: 3 }}>
                <span className="num" style={{ fontSize: 22, fontWeight: 500, letterSpacing: "-0.02em" }}>{x.v}</span>
                <span style={{ fontSize: 11, color: "var(--text-3)" }}>{x.u}</span>
              </div>
            </div>
          ))}
        </div>
      </Card>

      {/* Map + streams */}
      <div className="af-stack-mobile" style={{ display: "grid", gridTemplateColumns: "1fr 1.4fr", gap: 14 }}>
        <Card padded={false} style={{ overflow: "hidden", aspectRatio: "1.1" }}>
          {streamsLoading
            ? <div style={{ width: "100%", height: "100%", display: "flex", alignItems: "center", justifyContent: "center", color: "var(--text-3)", fontSize: 11, fontFamily: "var(--font-mono)" }}>cargando GPS…</div>
            : <GpsMap track={hasGps ? s.gps : null} elevation={a.elevation || null} />}
        </Card>

        <Card>
          <div style={{ display: "flex", alignItems: "center", marginBottom: 10 }}>
            <div className="eyebrow">STREAMS · POR SEGUNDO</div>
            <div style={{ flex: 1 }} />
            <div style={{ display: "flex", gap: 12, fontSize: 11 }}>
              {hasStreams && s.hr && <span><Dot tone="bad" /> FC</span>}
              {hasStreams && s.pace && <span><Dot tone="info" /> Pace</span>}
              {hasStreams && s.elevation && <span><Dot tone="text-3" size={6} style={{ background: "var(--text-3)" }} /> Elevación</span>}
            </div>
          </div>
          {streamsLoading ? (
            <div style={{ height: 260, display: "flex", alignItems: "center", justifyContent: "center", color: "var(--text-3)", fontSize: 11, fontFamily: "var(--font-mono)" }}>
              cargando streams…
            </div>
          ) : hasStreams ? (
            <StreamChart
              xMax={a.dist > 0 ? a.dist : 1}
              series={[
                s.elevation ? { data: s.elevation, color: "var(--text-4)", fill: true, axis: "right", label: "Elev (m)", format: (v) => `${Math.round(v)}` } : null,
                s.hr ? { data: s.hr, color: "var(--bad)", axis: "left", label: "FC (bpm)", format: (v) => `${Math.round(v)}` } : null,
                s.pace ? {
                  // Pace se grafica invertido (más rápido = más alto). Para
                  // el label mostramos m:ss reales (no negativos).
                  data: s.pace.map((p) => -p),
                  color: "var(--info)",
                  axis: "left",
                  label: "Pace",
                  format: (v) => {
                    const sec = -v;
                    if (!isFinite(sec) || sec <= 0) return "—";
                    return `${Math.floor(sec / 60)}:${String(Math.round(sec % 60)).padStart(2, "0")}`;
                  },
                } : null,
              ].filter(Boolean)}
              height={260}
            />
          ) : (
            <div style={{
              height: 260, display: "flex", alignItems: "center", justifyContent: "center",
              color: "var(--text-3)", fontSize: 12, fontFamily: "var(--font-mono)",
              border: "1px dashed var(--line)", borderRadius: "var(--r-2)",
            }}>
              Sin streams por segundo para esta actividad.
            </div>
          )}
        </Card>
      </div>

      {/* Splits + métricas del dispositivo (sólo lo que tiene valor real) */}
      <div className="af-stack-mobile" style={{ display: "grid", gridTemplateColumns: "1.4fr 1fr", gap: 14 }}>
        <Card>
          <div className="eyebrow" style={{ marginBottom: 12 }}>SPLITS · 1 KM</div>
          {hasSplits ? (
            <table style={{ width: "100%", borderCollapse: "collapse", fontSize: 12, fontFamily: "var(--font-mono)" }}>
              <thead>
                <tr style={{ borderBottom: "1px solid var(--line-soft)" }}>
                  {["KM", "PACE", "FC", "CADENCIA", ""].map(h => <th key={h} style={{ padding: "6px 4px", textAlign: "left", fontSize: 9.5, color: "var(--text-3)", fontWeight: 500 }}>{h}</th>)}
                </tr>
              </thead>
              <tbody>
                {splits.map((sp, i) => {
                  // Barra de pace relativo al split más rápido (visual de qué tan duro fue).
                  const ratio = sp.secPerKm && fastestSplit
                    ? Math.min(1, fastestSplit / sp.secPerKm)
                    : null;
                  return (
                    <tr key={i} style={{ borderBottom: "1px dashed var(--line-soft)" }}>
                      <td style={{ padding: "6px 4px", color: "var(--text-3)" }}>{sp.n}</td>
                      <td style={{ padding: "6px 4px", fontWeight: 500 }}>{sp.paceStr}</td>
                      <td style={{ padding: "6px 4px" }}>{sp.avgHr ?? "—"}</td>
                      <td style={{ padding: "6px 4px" }}>{sp.cadence ?? "—"}</td>
                      <td style={{ padding: "6px 4px", width: 100 }}>
                        {ratio != null && (
                          <div style={{ height: 5, background: "var(--line-soft)", borderRadius: 2, overflow: "hidden" }}>
                            <div style={{ height: "100%", width: `${ratio * 100}%`, background: "var(--bad)", opacity: 0.6 }} />
                          </div>
                        )}
                      </td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          ) : (
            <div style={{
              padding: "30px 10px", textAlign: "center",
              fontSize: 12, color: "var(--text-3)", fontFamily: "var(--font-mono)",
              border: "1px dashed var(--line)", borderRadius: "var(--r-2)",
            }}>
              Sin splits para esta actividad.<br />
              <span style={{ color: "var(--text-4)", fontSize: 10 }}>
                (los splits llegan al sincronizar el FIT — sólo se enriquecen las 5 más recientes)
              </span>
            </div>
          )}
        </Card>

        <Card>
          <div className="eyebrow" style={{ marginBottom: 12 }}>MÉTRICAS DEL DISPOSITIVO</div>
          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10, fontSize: 12 }}>
            <Stat label="Training Effect" value={a.te != null ? Number(a.te).toFixed(1) : "—"} unit="aer" />
            <Stat label="TE anaeróbico" value={a.teAnaerobic != null ? Number(a.teAnaerobic).toFixed(1) : "—"} unit="" />
            <Stat label="Recovery time" value={a.recovery || "—"} unit="" />
            <Stat label="Carga" value={a.load != null ? a.load : "—"} unit="" />
          </div>
          <div style={{ marginTop: 14, paddingTop: 14, borderTop: "1px solid var(--line-soft)", fontSize: 11, color: "var(--text-3)" }}>
            Pídele al coach un análisis específico de esta sesión arriba ↑
          </div>
        </Card>
      </div>
    </div>
  );
};

/* VolumeBars — bar chart de volumen semanal por bucket
 *
 * Espera la shape devuelta por GET /data/activities/summary:
 *   { periods: [{ period_start, period_end, label, count, total_distance_m,
 *                 total_duration_s, by_type: {running, cycling, ...} }] }
 *
 * - Convierte total_distance_m → km (÷1000).
 * - Una sola barra (distancia total) — el desglose por tipo se podría agregar
 *   si más adelante queremos volver al diseño de 3 barras (run/bike/strength).
 *   Por ahora simplificamos para que el contrato del backend sea más limpio.
 * - Loading: muestra placeholder skeleton.
 * - fallback === true → badge discreto "datos demo (sin backend)".
 */
const VolumeBars = ({ periods, loading, fallback, inDemo }) => {
  const items = Array.isArray(periods) ? periods : [];
  const kmValues = items.map(p => Number(p?.total_distance_m || 0) / 1000);
  const maxKm = Math.max(1, ...kmValues); // evitar /0
  const totalCount = items.reduce((s, p) => s + (Number(p?.count) || 0), 0);

  return (
    <Card>
      <div style={{ display: "flex", alignItems: "center", marginBottom: 14, gap: 10, flexWrap: "wrap" }}>
        <div>
          <div className="eyebrow" style={{ marginBottom: 4 }}>VOLUMEN SEMANAL · KM</div>
          <div style={{ fontSize: 13, color: "var(--text-2)" }}>
            {items.length > 0 ? `${items.length} semanas · ${totalCount} actividades` : "Sin datos en el rango"}
          </div>
        </div>
        <div style={{ flex: 1 }} />
        {loading && (
          <span style={{ fontSize: 10.5, color: "var(--text-3)", fontFamily: "var(--font-mono)" }}>cargando…</span>
        )}
        {fallback && !inDemo && !loading && (
          <span title="No se pudo conectar al backend — mostrando datos de muestra"
            style={{
              fontSize: 10, padding: "3px 7px", borderRadius: 3,
              background: "var(--warn-soft)", color: "var(--warn)",
              fontFamily: "var(--font-mono)", letterSpacing: "0.06em",
              border: "1px solid color-mix(in srgb, var(--warn) 30%, transparent)",
            }}>
            datos demo (sin backend)
          </span>
        )}
      </div>

      {items.length === 0 && !loading ? (
        <div style={{
          padding: "30px 10px", textAlign: "center",
          fontSize: 12, color: "var(--text-3)", fontFamily: "var(--font-mono)",
          border: "1px dashed var(--line)", borderRadius: "var(--r-2)",
        }}>
          Aún no hay actividades para resumir.
        </div>
      ) : (
        <div className="af-grid-4" style={{
          display: "grid",
          gridTemplateColumns: `repeat(${Math.max(1, items.length)}, 1fr)`,
          gap: 18,
          opacity: loading ? 0.5 : 1,
          transition: "opacity 0.2s",
        }}>
          {items.map((p, i) => {
            const km = kmValues[i];
            const pct = (km / maxKm) * 100;
            const byType = p.by_type || {};
            const typeSummary = Object.entries(byType)
              .filter(([, v]) => Number(v) > 0)
              .map(([k, v]) => `${v}× ${k}`)
              .join(" · ");
            return (
              <div key={i}>
                <div style={{ fontSize: 10, fontFamily: "var(--font-mono)", color: "var(--text-3)", marginBottom: 6 }}>
                  {p.label || p.period_start || `S${i + 1}`}
                </div>
                <div style={{ display: "flex", alignItems: "flex-end", gap: 4, height: 80 }}
                  title={`${km.toFixed(1)} km${typeSummary ? ` · ${typeSummary}` : ""}`}>
                  <div style={{
                    flex: 1,
                    height: `${pct}%`,
                    minHeight: km > 0 ? 2 : 0,
                    background: "var(--info)",
                    borderRadius: 2,
                  }} />
                </div>
                <div style={{ marginTop: 4, fontSize: 10, fontFamily: "var(--font-mono)", color: "var(--text-3)", textAlign: "center" }}>
                  {km.toFixed(1)} km
                </div>
                {p.count != null && (
                  <div style={{ marginTop: 2, fontSize: 9.5, color: "var(--text-4)", fontFamily: "var(--font-mono)", textAlign: "center" }}>
                    {p.count} act.
                  </div>
                )}
              </div>
            );
          })}
        </div>
      )}
    </Card>
  );
};

window.Activities = Activities;
