// running/src/sharedUI.jsx — shared UI layer (slice 1 of the module split).
//
// Extracted verbatim from running/index.html: design tokens (C), race
// branding, number formatting (fmt), and every cross-view component
// (Card / MetricCard / Sparkline / ChartTooltip / ...). Loaded as a
// text/babel script BEFORE the main app block in dev; concatenated in
// the same order into app.gen.js by scripts/build-running.mjs for
// deploy. Exposes everything on window.SharedUI — the main block
// destructures from there, so identifiers stay bare in app code.
//
// useData lives in the main block (DataContext); components here call
// it at RENDER time via the window.__appUseData bridge the main block
// installs — never at module-eval time.

const useData = () => (window.__appUseData ? window.__appUseData() : null);

    const C = {
      bg0: "#060810", bg1: "#080b12", bg2: "#0c0f1a", bg3: "#111827",
      border: "#17202e", borderHover: "#2d3f55",
      cyan: "#7dd3fc", cyanDim: "#38bdf8", cyanMuted: "#0ea5e9",
      green: "#34d399", greenDim: "#10b981",
      amber: "#f59e0b", amberDim: "#d97706",
      purple: "#a78bfa", purpleDim: "#7c3aed",
      red: "#f87171", pink: "#f472b6",
      text: "#e2e8f0", textMuted: "#64748b", textDim: "#94a3b8",
    };

    // ── Race Branding ─────────────────────────────────────────────────────────
    const RACE_BRANDS = {
      "boston marathon":     { abbr:"BOS", colors:["#0033A0","#FFD100"], icon:"https://www.baa.org/favicon.ico" },
      "new york marathon":  { abbr:"NYC", colors:["#003DA5","#FF6900"], icon:"https://www.nyrr.org/favicon.ico" },
      "nyc marathon":       { abbr:"NYC", colors:["#003DA5","#FF6900"], icon:"https://www.nyrr.org/favicon.ico" },
      "tcs new york city marathon": { abbr:"NYC", colors:["#003DA5","#FF6900"], icon:"https://www.nyrr.org/favicon.ico" },
      "chicago marathon":   { abbr:"CHI", colors:["#CE1126","#041E42"], icon:"https://www.chicagomarathon.com/favicon.ico" },
      "bank of america chicago marathon": { abbr:"CHI", colors:["#CE1126","#041E42"] },
      "london marathon":    { abbr:"LDN", colors:["#E31837","#002D72"], icon:"https://www.tcslondonmarathon.com/favicon.ico" },
      "tcs london marathon":{ abbr:"LDN", colors:["#E31837","#002D72"] },
      "berlin marathon":    { abbr:"BER", colors:["#009FE3","#E30613"] },
      "bmw berlin marathon":{ abbr:"BER", colors:["#009FE3","#E30613"] },
      "tokyo marathon":     { abbr:"TKY", colors:["#ED1C24","#231F20"] },
      "marine corps marathon": { abbr:"MCM", colors:["#CC0000","#003366"] },
      "dublin marathon":    { abbr:"DUB", colors:["#169B62","#FF883E"] },
      "irish life dublin marathon": { abbr:"DUB", colors:["#169B62","#FF883E"] },
      "cork marathon":      { abbr:"CRK", colors:["#C8102E","#FFFFFF"] },
      "paris marathon":     { abbr:"PAR", colors:["#002395","#ED2939"] },
      "philadelphia marathon": { abbr:"PHL", colors:["#003DA5","#FFD100"] },
      "la marathon":        { abbr:"LA",  colors:["#FDB913","#003DA5"] },
      "los angeles marathon":{ abbr:"LA", colors:["#FDB913","#003DA5"] },
      "houston marathon":   { abbr:"HOU", colors:["#002D62","#D31145"] },
      "seattle marathon":   { abbr:"SEA", colors:["#003F2D","#69BE28"] },
      "san francisco marathon": { abbr:"SF", colors:["#FF6600","#003DA5"] },
      "walt disney world marathon": { abbr:"DIS", colors:["#003DA5","#FFD100"] },
      "disney marathon":    { abbr:"DIS", colors:["#003DA5","#FFD100"] },
      "half marathon":      { abbr:"HM",  colors:["#7dd3fc","#0ea5e9"] },
      "parkrun":            { abbr:"PR",  colors:["#E31837","#111111"] },
    };

    function getRaceBrand(raceName) {
      if (!raceName) return null;
      const lower = raceName.toLowerCase().trim();
      // Exact match first
      if (RACE_BRANDS[lower]) return RACE_BRANDS[lower];
      // Partial match
      for (const [key, brand] of Object.entries(RACE_BRANDS)) {
        if (lower.includes(key) || key.includes(lower)) return brand;
      }
      return null;
    }

    function RaceLogo({ name, size = 44 }) {
      const brand = getRaceBrand(name);
      const c1 = brand ? brand.colors[0] : C.cyan;
      const c2 = brand ? brand.colors[1] : C.cyanDim;
      const abbr = brand ? brand.abbr : (name || "?").split(/\s+/).map(w => w[0]).join("").toUpperCase().slice(0, 3);

      return React.createElement("div", {
        style: {
          width: size, height: size, borderRadius: size * 0.22, flexShrink: 0,
          background: `linear-gradient(135deg, ${c1}, ${c2})`,
          display: "flex", alignItems: "center", justifyContent: "center",
          fontFamily: "'Space Grotesk', sans-serif", fontWeight: 900,
          fontSize: size * 0.28, color: "#fff", letterSpacing: "-0.02em",
          textShadow: "0 1px 3px rgba(0,0,0,0.4)",
          boxShadow: `0 2px 8px ${c1}30, inset 0 1px 0 rgba(255,255,255,0.15)`,
          position: "relative", overflow: "hidden",
        }
      },
        // Subtle shine overlay
        React.createElement("div", { style: {
          position: "absolute", top: 0, left: 0, right: 0, height: "50%",
          background: "linear-gradient(180deg, rgba(255,255,255,0.12) 0%, rgba(255,255,255,0) 100%)",
          borderRadius: `${size*0.22}px ${size*0.22}px 0 0`,
        }}),
        abbr
      );
    }

    // ── Utility ───────────────────────────────────────────────────────────────
    const fmt = {
      pace: (spm) => { // seconds per mile → "M:SS /mi"
        if (!spm) return "--";
        // Round total seconds first, then split — otherwise spm=599.7
        // becomes 9:60 (Math.floor(9.99)=9, Math.round(59.7)=60).
        const total = Math.round(spm);
        const m = Math.floor(total / 60), s = total % 60;
        return `${m}:${s.toString().padStart(2,"0")} /mi`;
      },
      dist: (m) => m ? `${(m / 1609.34).toFixed(2)} mi` : "--",
      distShort: (m) => m ? `${(m / 1609.34).toFixed(1)}` : "0",
      duration: (s) => {
        if (!s) return "--";
        s = Math.round(s);
        const h = Math.floor(s/3600), m = Math.floor((s%3600)/60), sec = s%60;
        return h > 0 ? `${h}:${m.toString().padStart(2,"0")}:${sec.toString().padStart(2,"0")}` : `${m}:${sec.toString().padStart(2,"0")}`;
      },
      date: (ts) => {
        if (!ts) return "--";
        const d = ts.toDate ? ts.toDate() : new Date(ts);
        return d.toLocaleDateString("en-US", { month:"short", day:"numeric", year:"numeric" });
      },
      dateShort: (ts) => {
        if (!ts) return "--";
        const d = ts.toDate ? ts.toDate() : new Date(ts);
        return d.toLocaleDateString("en-US", { month:"short", day:"numeric" });
      },
      hr: (bpm) => bpm ? `${Math.round(bpm)} bpm` : "--",
      elev: (m) => m ? `${Math.round(m * 3.28084)} ft` : "--",
    };

    // ── Shared UI components ──────────────────────────────────────────────────
    const Card = ({ children, style = {}, onClick, id }) => (
      <div id={id} onClick={onClick} style={{
        background: C.bg1, border: `1px solid ${C.border}`, borderRadius: 12,
        boxShadow: "0 1px 3px rgba(0,0,0,0.35)",
        padding: 20, ...style,
        cursor: onClick ? "pointer" : "default",
        transition: "border-color 0.15s",
      }}
      onMouseEnter={e => onClick && (e.currentTarget.style.borderColor = C.borderHover)}
      onMouseLeave={e => onClick && (e.currentTarget.style.borderColor = C.border)}
      >{children}</div>
    );

    const Stat = ({ label, value, unit, color = C.cyan, size = 28 }) => (
      <div style={{ display:"flex", flexDirection:"column", gap: 4 }}>
        <div style={{ fontSize: 11, color: C.textMuted, textTransform:"uppercase", letterSpacing:"0.08em" }}>{label}</div>
        <div style={{ fontSize: size, fontWeight: 700, color, fontFamily:"'Space Grotesk', sans-serif", lineHeight: 1 }}>
          {value}<span style={{ fontSize: size * 0.45, color: C.textMuted, marginLeft: 4 }}>{unit}</span>
        </div>
      </div>
    );

    const Badge = ({ label, color = C.cyan }) => (
      <span style={{
        background: color + "18", color, border: `1px solid ${color}40`,
        borderRadius: 6, padding: "2px 8px", fontSize: 11, fontWeight: 600
      }}>{label}</span>
    );

    // "← Status" breadcrumb shown atop every non-Status page so each
    // drill-down reads as a child of the cohesive Status view. Uses the
    // navigate fn on DataContext (set by App); renders nothing if absent.
    const StatusBreadcrumb = ({ here }) => {
      const data = useData();
      const navigate = data && data.navigate;
      if (!navigate) return null;
      return (
        <button onClick={() => navigate("Status")} style={{
          display: "inline-flex", alignItems: "center", gap: 6, background: "transparent",
          border: `1px solid ${C.border}`, color: C.textMuted, borderRadius: 7,
          padding: "4px 10px", fontSize: 11, cursor: "pointer", fontFamily: "'IBM Plex Mono', monospace",
          marginBottom: 14,
        }}
        onMouseEnter={e => { e.currentTarget.style.borderColor = C.borderHover; e.currentTarget.style.color = C.textDim; }}
        onMouseLeave={e => { e.currentTarget.style.borderColor = C.border; e.currentTarget.style.color = C.textMuted; }}>
          <span>←</span> Status{here ? <span style={{ opacity: 0.55 }}> / {here}</span> : null}
        </button>
      );
    };

    const Btn = ({ children, onClick, variant = "primary", small = false, disabled = false }) => {
      const styles = {
        primary: { background: C.cyan, color: C.bg0, border: "none" },
        secondary: { background: "transparent", color: C.cyan, border: `1px solid ${C.cyan}40` },
        ghost: { background: "transparent", color: C.textDim, border: `1px solid ${C.border}` },
        danger: { background: "transparent", color: C.red, border: `1px solid ${C.red}40` },
      };
      return (
        <button onClick={disabled ? undefined : onClick} style={{
          ...styles[variant],
          padding: small ? "4px 12px" : "8px 16px",
          borderRadius: 8, cursor: disabled ? "not-allowed" : "pointer",
          fontSize: small ? 12 : 13, fontWeight: 600, fontFamily:"'IBM Plex Mono', monospace",
          opacity: disabled ? 0.5 : 1, transition: "opacity 0.15s",
        }}>{children}</button>
      );
    };

    const TabBar = ({ tabs, active, onChange }) => (
      <div style={{ display:"flex", gap: 4, background: C.bg2, padding: 4, borderRadius: 10, flexWrap:"wrap" }}>
        {tabs.map(t => (
          <button key={t} onClick={() => onChange(t)} style={{
            padding: "6px 14px", borderRadius: 7, border:"none", cursor:"pointer",
            background: active === t ? C.bg1 : "transparent",
            color: active === t ? C.cyan : C.textMuted,
            fontSize: 12, fontWeight: 600, fontFamily:"'IBM Plex Mono', monospace",
            borderBottom: active === t ? `2px solid ${C.cyan}` : "2px solid transparent",
            transition: "all 0.15s",
          }}>{t}</button>
        ))}
      </div>
    );

    const EmptyState = ({ icon, title, sub }) => (
      <div style={{ textAlign:"center", padding: "60px 20px", color: C.textMuted }}>
        <div style={{ fontSize: 40, marginBottom: 12 }}>{icon}</div>
        <div style={{ fontSize: 16, color: C.textDim, marginBottom: 8, fontFamily:"'Space Grotesk', sans-serif" }}>{title}</div>
        <div style={{ fontSize: 13 }}>{sub}</div>
      </div>
    );

    // ── Shared metric primitives — used by the Status view and (over time)
    //    every drill-down so a number looks the same wherever it shows up. ──
    // Grades carry +/- modifiers (A-, B+ …) — color on the letter so a
    // B- chip reads cyan like any B, not unknown-gray.
    const gradeColor = (g) => { const l = typeof g === "string" && g.length ? g[0].toUpperCase() : null; return l === "A" ? C.green : l === "B" ? C.cyan : l === "C" ? C.amber : l === "D" ? C.red : C.textMuted; };

    // Single source of "what counts as good/ok" per metric. Forms:
    //   { good, ok }            higher is better (≥good = green, ≥ok = amber)
    //   { good, ok, lower: 1 }  lower is better  (≤good = green, ≤ok = amber)
    //   { band: [lo, hi] }      in-band = green, just outside = amber, else red
    const THRESHOLDS = {
      readiness:    { good: 70, ok: 40 },
      sleepScore:   { good: 80, ok: 60 },
      cadenceSpm:   { good: 170, ok: 160 },
      gctMs:        { good: 240, ok: 290, lower: 1 },
      vertOscCm:    { good: 8, ok: 10, lower: 1 },
      onTargetPct:  { good: 80, ok: 50 },
      acwr:         { band: [0.8, 1.5] },
    };
    function metricColor(value, key) {
      if (value == null || !Number.isFinite(value)) return C.textMuted;
      const t = THRESHOLDS[key];
      if (!t) return C.text;
      if (t.band) {
        const [lo, hi] = t.band;
        if (value >= lo && value <= hi) return C.green;
        if (value >= lo * 0.85 && value <= hi * 1.15) return C.amber;
        return C.red;
      }
      if (t.lower) return value <= t.good ? C.green : value <= t.ok ? C.amber : C.red;
      return value >= t.good ? C.green : value >= t.ok ? C.amber : C.red;
    }

    // A clickable summary tile: title (uppercase) + optional drill-down
    // arrow, then arbitrary children. onClick navigates via DataContext.
    const MetricCard = ({ title, color, to, children, style = {} }) => {
      const data = useData();
      const nav = data && data.navigate;
      return (
        <Card onClick={to && nav ? () => nav(to) : undefined} style={{ padding: 16, display: "flex", flexDirection: "column", gap: 10, ...style }}>
          <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
            <div style={{ fontSize: 11, fontWeight: 700, color: color || C.textDim, textTransform: "uppercase", letterSpacing: "0.08em" }}>{title}</div>
            {to && <span style={{ fontSize: 11, color: C.textMuted }}>→</span>}
          </div>
          {children}
        </Card>
      );
    };
    const MetricRow = ({ label, value, color }) => (
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", fontSize: 12 }}>
        <span style={{ color: C.textMuted }}>{label}</span>
        <span style={{ color: color || C.text, fontWeight: 600, fontFamily: "'IBM Plex Mono', monospace" }}>{value}</span>
      </div>
    );
    const Sparkline = ({ vals, color = C.green, width = 120, height = 24 }) => {
      const xs = (vals || []).filter(x => x != null && Number.isFinite(x));
      if (xs.length < 2) return null;
      const max = Math.max(...xs), min = Math.min(...xs), span = max - min || 1;
      const pts = xs.map((x, i) => `${(i / (xs.length - 1)) * width},${height - ((x - min) / span) * height}`).join(" ");
      return <svg width={width} height={height} style={{ display: "block" }}><polyline points={pts} fill="none" stroke={color} strokeWidth="1.5" /></svg>;
    };

    // First-run journey — shown on the Status view until the athlete has a
    // training plan. Each step is connect-Strava → sync runs → (optional)
    // Garmin dynamics → generate a plan; done steps collapse to a ✓.
    const OnboardingChecklist = ({ activities = [] }) => {
      const data = useData() || {};
      const { activePlan, navigate } = data;
      const nav = navigate || (() => {});
      const runCount = (activities || []).filter(a => a && (a.type === "Run" || a.type === "VirtualRun")).length;
      const dynCount = (activities || []).filter(a => a && a.dynamics && (a.dynamics.groundContactTime || a.dynamics.cadenceSpm || a.dynamics.verticalOscillation || a.dynamics.power)).length;
      const steps = [
        { done: true, label: "Strava connected", detail: "activity feed + HR zones", cta: null },
        { done: runCount >= 8, label: runCount >= 8 ? "Runs synced" : "Sync your runs", detail: `${runCount} run${runCount === 1 ? "" : "s"} in the log${runCount < 8 ? " — sync to fill out trends & paces" : ""}`, cta: runCount >= 8 ? null : { label: "Open Settings → Strava → Sync", to: "Settings" } },
        { done: dynCount >= 1, label: dynCount >= 1 ? "Garmin running dynamics" : "Add Garmin dynamics (optional)", detail: dynCount >= 1 ? `${dynCount} run${dynCount === 1 ? "" : "s"} with cadence / GCT / power` : "cadence, ground-contact, power — run the Garmin bookmarklet", cta: dynCount >= 1 ? null : { label: "Settings → Garmin", to: "Settings" } },
        { done: !!activePlan, label: activePlan ? `Plan: ${activePlan.raceName || activePlan.raceDistance || "active"}` : "Generate a training plan", detail: activePlan ? "your periodized race build" : "AI-built, tailored to your race goal & fitness — the Status hero fills in once it exists", cta: activePlan ? null : { label: "Training → Plan", to: "Training" } },
      ];
      const doneN = steps.filter(s => s.done).length;
      return (
        <Card style={{ padding: 18, display: "flex", flexDirection: "column", gap: 12 }}>
          <div style={{ display: "flex", alignItems: "baseline", justifyContent: "space-between", flexWrap: "wrap", gap: 8 }}>
            <div style={{ fontSize: 13, fontWeight: 700, color: C.text }}>Get set up</div>
            <span style={{ fontSize: 11, color: C.textMuted, fontFamily: "'IBM Plex Mono', monospace" }}>{doneN}/{steps.length} done</span>
          </div>
          <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
            {steps.map((s, i) => (
              <div key={i} style={{ display: "flex", alignItems: "flex-start", gap: 10, padding: "8px 10px", background: C.bg2, borderRadius: 8, opacity: s.done ? 0.7 : 1 }}>
                <span style={{ flexShrink: 0, width: 18, height: 18, borderRadius: "50%", border: `1.5px solid ${s.done ? C.green : C.border}`, background: s.done ? C.green : "transparent", color: C.bg0, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 11, fontWeight: 900 }}>{s.done ? "✓" : ""}</span>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ fontSize: 12, fontWeight: 600, color: s.done ? C.textMuted : C.text, textDecoration: s.done ? "line-through" : "none" }}>{s.label}</div>
                  <div style={{ fontSize: 11, color: C.textMuted, lineHeight: 1.4 }}>{s.detail}</div>
                </div>
                {s.cta && <button onClick={() => nav(s.cta.to)} style={{ flexShrink: 0, background: C.cyan + "14", border: `1px solid ${C.cyan}33`, color: C.cyan, borderRadius: 7, padding: "5px 10px", fontSize: 11, fontWeight: 600, cursor: "pointer", fontFamily: "'IBM Plex Mono', monospace", whiteSpace: "nowrap" }}>{s.cta.label} →</button>}
              </div>
            ))}
          </div>
        </Card>
      );
    };

    // ── Chart tooltip ─────────────────────────────────────────────────────────
    const ChartTooltip = ({ active, payload, label, formatter, labelFormatter }) => {
      if (!active || !payload?.length) return null;
      const displayLabel = labelFormatter ? labelFormatter(label) : label;
      return (
        <div style={{ background: C.bg2, border:`1px solid ${C.border}`, borderRadius: 8, padding:"10px 14px", fontSize: 12 }}>
          <div style={{ color: C.textMuted, marginBottom: 6 }}>{displayLabel}</div>
          {payload.map((p, i) => (
            <div key={i} style={{ color: p.color || C.cyan, marginBottom: 2 }}>
              {p.name}: {formatter ? formatter(p.value, p.name) : p.value}
            </div>
          ))}
        </div>
      );
    };


    // ── Nutrition reset control ───────────────────────────────────────────────
    // One athlete-declared anchor, two effects: glycogen stores are taken as
    // FULL at the start of the chosen day, and the RED-S nutrition window
    // restarts there (pre-anchor food data disregarded; the EA / protein /
    // low-intake penalties pause until logging has resumed for 3+ days).
    // Exists for the "ate normally but didn't log it" case, where both models
    // otherwise read the gap as starvation. Persisted to
    // settings/nutrition.glycogenFullAsOf so the web, the server snapshot and
    // the coach read the same anchor. Rendered on every surface that shows the
    // affected numbers — the fix belongs where the wrong number is.
    const NutritionResetControl = ({ isOwner }) => {
      const data = useData();
      const setAnchor = data && data.setGlycogenFullAsOf;
      const active = (data && data.nutritionSettings && data.nutritionSettings.glycogenFullAsOf) || null;
      const today = new Date().toISOString().split("T")[0];
      const [date, setDate] = React.useState(active || today);
      const [saved, setSaved] = React.useState(false);
      React.useEffect(() => { if (active) setDate(active); }, [active]);
      if (!isOwner || !setAnchor) return null;
      const apply = (v) => { setAnchor(v); setSaved(true); setTimeout(() => setSaved(false), 2500); };
      const btn = (color) => ({ background:"none", border:`1px solid ${color}40`, borderRadius:6,
        padding:"5px 12px", cursor:"pointer", fontSize:11, color, fontFamily:"'IBM Plex Mono',monospace" });
      return (
        <div style={{ padding:"10px 12px", background:C.bg2, borderRadius:8, marginBottom:12,
          border:`1px solid ${active ? C.cyan + "40" : C.border}` }}>
          <div style={{ fontSize:11, fontWeight:700, color:C.text, marginBottom:3 }}>Nutrition Reset</div>
          <div style={{ fontSize:10, color:C.textMuted, lineHeight:1.5, marginBottom:8 }}>
            Marks glycogen stores full at the start of the chosen day and restarts RED-S scoring from it.
            Use after a stretch you ate normally but didn't log — anything logged since still counts.
          </div>
          <div style={{ display:"flex", gap:8, alignItems:"center", flexWrap:"wrap" }}>
            <input type="date" value={date} max={today} onChange={e => setDate(e.target.value)}
              style={{ background:C.bg3, border:`1px solid ${C.border}`, borderRadius:6, padding:"4px 8px",
                fontSize:11, color:C.textDim, fontFamily:"'IBM Plex Mono',monospace" }} />
            <button onClick={() => date && apply(date)} style={btn(C.cyan)}>Reset from this date</button>
            {active && <button onClick={() => apply(null)} style={btn(C.textMuted)}>Clear</button>}
            {saved && <span style={{ fontSize:10, color:C.green, fontWeight:700 }}>✓ saved</span>}
          </div>
          {active && (
            <div style={{ fontSize:10, color:C.cyan, fontWeight:600, marginTop:6 }}>
              Active — stores full at start of {active}, RED-S window restarts there
            </div>
          )}
        </div>
      );
    };


window.SharedUI = { C, RACE_BRANDS, getRaceBrand, RaceLogo, fmt, Card, Stat, Badge, StatusBreadcrumb, Btn, TabBar, EmptyState, gradeColor, THRESHOLDS, metricColor, MetricCard, MetricRow, Sparkline, OnboardingChecklist, ChartTooltip, NutritionResetControl };
