// running/src/garminHistory.jsx — slice 3 of the module split.
//
// The History tab inside Training: long-term progress from the exported
// Garmin history doc (Firestore users/{uid}/history/timeseries, seeded
// by static garmin-history.json). Extracted verbatim from
// running/index.html. First data-bearing slice: `db` and `functions`
// resolve from the page-level firebase <script> (top-level consts live
// in the global lexical environment, reachable from slice scopes in
// both dev and the concatenated build); useData goes through the
// window.__appUseData bridge installed by the main block.

const { C, Card } = window.SharedUI;
const useData = () => (window.__appUseData ? window.__appUseData() : null);
const { useState, useEffect } = React;
const {
  AreaChart, Area, LineChart, Line, ComposedChart, Bar,
  XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer,
  ReferenceLine,
} = Recharts;

    function GarminHistory({ userId }) {
      const { activePlan: activePlanSummary } = useData();
      const [data, setData] = useState(null);
      const [loading, setLoading] = useState(true);
      const [range, setRange] = useState("all");
      const [insights, setInsights] = useState(null);
      const [analyzingInsights, setAnalyzingInsights] = useState(false);
      const [rebuilding, setRebuilding] = useState(false);
      const [rebuildMsg, setRebuildMsg] = useState(null);

      // Live history lives in Firestore under users/{uid}/history/timeseries
      // (appended on every new activity sync). Static garmin-history.json
      // is the initial seed / deep-history backfill. We take whichever is
      // larger on totalRuns — the live doc wins once it has caught up.
      useEffect(() => {
        let liveUnsub = null;
        let cancelled = false;
        const staticPromise = fetch("/running/data/garmin-history.json")
          .then(r => r.json()).catch(() => null);
        if (userId) {
          liveUnsub = db.collection("users").doc(userId).collection("history")
            .doc("timeseries").onSnapshot(async snap => {
              if (cancelled) return;
              const live = snap.exists ? snap.data() : null;
              const staticD = await staticPromise;
              const liveRuns = live?.totalRuns || 0;
              const staticRuns = staticD?.totalRuns || 0;
              setData(liveRuns >= staticRuns ? live : staticD);
              setLoading(false);
            }, () => {
              staticPromise.then(d => { if (!cancelled) { setData(d); setLoading(false); } });
            });
        } else {
          staticPromise.then(d => { if (!cancelled) { setData(d); setLoading(false); } });
        }
        return () => { cancelled = true; if (liveUnsub) liveUnsub(); };
      }, [userId]);

      if (loading) return <Card><div style={{textAlign:"center",padding:40,color:C.textMuted}}>Loading historical data...</div></Card>;
      if (!data) return <Card><div style={{textAlign:"center",padding:40,color:C.textMuted}}>No historical data found.</div></Card>;

      const filterByRange = (series, dateKey = "date") => {
        if (range === "all") return series;
        const now = new Date();
        const months = range === "1y" ? 12 : range === "6m" ? 6 : 3;
        const cutoff = new Date(now.getFullYear(), now.getMonth() - months, now.getDate()).toISOString().slice(0, 10);
        return series.filter(s => (s[dateKey] || s.week || s.month) >= cutoff);
      };

      const weekly = filterByRange(data.weekly, "week");
      const monthly = filterByRange(data.monthly, "month");
      const endurance = filterByRange(data.metrics.endurance);
      const hill = filterByRange(data.metrics.hill);
      const readiness = filterByRange(data.metrics.readiness);

      // Multi-year fitness arc — the FULL run history through the same
      // Banister TRIMP → CTL (42-day) model the live charts use, so every
      // build and peak since the log began is on one curve. Uses the
      // app-baseline HR constants (176/49: this view has no athlete HR
      // config) — the value is the shape and the peaks, not absolute
      // scale. Weekly downsample keeps ~4y of dailies renderable.
      // Plain computation (no hook): this sits below the early returns.
      const fitnessArc = (() => {
        const TL = window.TrainingLoad;
        if (!TL || !Array.isArray(data.runs) || data.runs.length < 30) return [];
        const acts = data.runs.map(r => ({ type: "Run", moving_time: (r.durMin || 0) * 60, average_heartrate: r.avgHR || null, start_date: r.date }));
        const series = TL.dailyLoadSeries(acts, { maxHR: 176, restHR: 49, days: 3650 });
        const ff = TL.fitnessFatigue(series);
        if (!ff) return [];
        return ff.points
          .filter((p, i) => i % 7 === 0 || i === ff.points.length - 1)
          .map(p => ({ date: p.date, fitness: p.ctl }));
      })();
      const fitnessArcShown = filterByRange(fitnessArc);

      // Setback analysis — involuntary gaps (≥10 days with no running)
      // and the load signature that PRECEDED each: peak EWMA ACWR in the
      // prior 3 weeks and the 4-week CTL ramp. The athlete's history
      // includes overuse injuries and RED-S/fatigue spells (gaps may be
      // either — nutrition era matters), so the point is a PERSONAL load
      // ceiling: the lowest pre-setback signature seen, vs today's.
      const setbackAnalysis = (() => {
        const TL = window.TrainingLoad;
        if (!TL || !Array.isArray(data.runs) || data.runs.length < 50) return null;
        const acts = data.runs.map(r => ({ type: "Run", moving_time: (r.durMin || 0) * 60, average_heartrate: r.avgHR || null, start_date: r.date }));
        const series = TL.dailyLoadSeries(acts, { maxHR: 176, restHR: 49, days: 3650 });
        if (series.length < 120) return null;
        // Daily EWMA acute/chronic + CTL, in one pass.
        const lA = 2 / 8, lC = 2 / 29, kCtl = 1 - Math.exp(-1 / 42);
        let a = series[0].load, c = series[0].load, ctl = series.slice(0, 14).reduce((s, p) => s + p.load, 0) / 14;
        const day = series.map(p => {
          a = p.load * lA + a * (1 - lA); c = p.load * lC + c * (1 - lC);
          ctl = ctl + (p.load - ctl) * kCtl;
          return { date: p.date, load: p.load, acwr: c > 5 ? a / c : null, ctl };
        });
        const gaps = [];
        let start = null;
        day.forEach((p, i) => {
          if (p.load === 0) { if (start == null) start = i; }
          else { if (start != null && i - start >= 10) gaps.push({ start, days: i - start }); start = null; }
        });
        if (start != null && day.length - start >= 10) gaps.push({ start, days: day.length - start });
        const rows = gaps.map(g => {
          const pre = day.slice(Math.max(0, g.start - 21), g.start);
          const maxAcwr = pre.reduce((m, p) => p.acwr != null && p.acwr > m ? p.acwr : m, 0);
          const ctlAt = day[Math.max(0, g.start - 1)].ctl;
          const ctl4wAgo = day[Math.max(0, g.start - 29)].ctl;
          return { date: day[g.start].date, days: g.days, maxAcwr: Math.round(maxAcwr * 100) / 100, ctlRamp: Math.round((ctlAt - ctl4wAgo) * 10) / 10 };
        }).filter(r => r.maxAcwr > 0);
        if (!rows.length) return null;
        const spikes = rows.map(r => r.maxAcwr).sort((x, y) => x - y);
        const ceiling = spikes[0]; // lowest ratio that ever preceded a setback
        const cur = day[day.length - 1];
        const pre21 = day.slice(-21);
        const curMaxAcwr = Math.round(pre21.reduce((m, p) => p.acwr != null && p.acwr > m ? p.acwr : m, 0) * 100) / 100;
        const curRamp = Math.round((cur.ctl - day[Math.max(0, day.length - 29)].ctl) * 10) / 10;
        return { rows: rows.slice(-8).reverse(), ceiling: Math.round(ceiling * 100) / 100, curMaxAcwr, curRamp };
      })();
      const fitnessArcPeak = fitnessArc.reduce((m, p) => p.fitness > (m?.fitness || 0) ? p : m, null);

      const fmtDur = (sec) => { if (!sec) return "--"; const h=Math.floor(sec/3600),m=Math.floor((sec%3600)/60),s=Math.round(sec%60); return h>0?`${h}:${String(m).padStart(2,"0")}:${String(s).padStart(2,"0")}`:`${m}:${String(s).padStart(2,"0")}`; };

      const totalMiles = data.runs.reduce((s, r) => s + r.distMi, 0);
      const totalHours = data.runs.reduce((s, r) => s + r.durMin, 0) / 60;
      const avgWeeklyMiles = weekly.length > 0 ? weekly.reduce((s, w) => s + w.miles, 0) / weekly.length : 0;
      const peakWeek = weekly.reduce((max, w) => w.miles > (max?.miles || 0) ? w : max, null);

      const analyzeHistory = async () => {
          if (!data) return;
          setAnalyzingInsights(true);
          try {
            const recent12 = data.weekly.slice(-12);
            const recent4 = data.weekly.slice(-4);
            const prior4 = data.weekly.slice(-8, -4);
            const summary = {
              totalRuns: data.totalRuns,
              dateRange: data.dateRange,
              recentWeeks: recent12.map(w => ({ week: w.week, miles: w.miles, runs: w.runs, longRun: w.longRun, avgHR: w.avgHR, gct: w.gct, avgCad: w.avgCad, vo: w.vo, power: w.power, z1pct: w.z1pct, z2pct: w.z2pct, z345pct: w.z345pct })),
              monthlyTrend: data.monthly.slice(-6),
              avgWeeklyMiles4wk: recent4.reduce((s,w) => s+w.miles,0)/4,
              avgWeeklyMilesPrior4wk: prior4.length > 0 ? prior4.reduce((s,w) => s+w.miles,0)/4 : 0,
              peakWeekMiles: Math.max(...data.weekly.map(w => w.miles)),
              latestMetrics: {
                endurance: data.metrics.endurance.length > 0 ? data.metrics.endurance[data.metrics.endurance.length-1] : null,
                hill: data.metrics.hill.length > 0 ? data.metrics.hill[data.metrics.hill.length-1] : null,
              },
              latestMarathonPred: data.racePredictions.length > 0 ? data.racePredictions[data.racePredictions.length-1] : null,
              formTrend: {
                gctRecent: recent4.filter(w=>w.gct).map(w=>w.gct),
                cadRecent: recent4.filter(w=>w.avgCad).map(w=>w.avgCad),
                voRecent: recent4.filter(w=>w.vo).map(w=>w.vo),
                powerRecent: recent4.filter(w=>w.power).map(w=>w.power),
              },
            };
            const fn = functions.httpsCallable("analyzeTrainingHistory");
            const result = await fn({ summary, race: {
              name: activePlanSummary?.raceName || activePlanSummary?.raceDistance || "Marathon",
              date: activePlanSummary?.raceDate || null,
              goalTime: activePlanSummary?.goalTime || null,
            } });
            setInsights(result.data);
          } catch (e) { console.error("History analysis failed:", e); }
          setAnalyzingInsights(false);
        };

        // Replay every stored running activity into history/timeseries. The
        // live append is per-activity and fire-and-forget, and the Garmin
        // export ZIP never touches this doc, so these charts can lag reality.
        // The onSnapshot listener picks up the rewrite automatically.
        const rebuildHistory = async () => {
          setRebuilding(true);
          setRebuildMsg(null);
          try {
            const res = await functions.httpsCallable("rebuildRunHistory", { timeout: 300000 })({});
            const d = res?.data || {};
            setRebuildMsg(`Rebuilt from ${d.processed || 0} runs (through ${d.dateRange?.to || "—"}).`);
          } catch (e) {
            setRebuildMsg(`Rebuild failed: ${e?.message || e}`);
          }
          setRebuilding(false);
        };

        return (
        <div style={{display:"flex",flexDirection:"column",gap:16}}>
          <div style={{display:"flex",gap:6,justifyContent:"space-between",alignItems:"center",flexWrap:"wrap"}}>
            <div style={{display:"flex",gap:6,alignItems:"center",flexWrap:"wrap"}}>
            <button onClick={analyzeHistory} disabled={analyzingInsights}
              style={{padding:"8px 16px",borderRadius:8,border:"none",fontSize:12,fontWeight:700,
                background:C.cyan,color:"#000",cursor:analyzingInsights?"wait":"pointer",opacity:analyzingInsights?0.6:1}}>
              {analyzingInsights ? "Analyzing..." : insights ? "Refresh Insights" : "Coach Analysis"}
            </button>
            <button onClick={rebuildHistory} disabled={rebuilding} title="Replay every stored running activity into the history charts — use after a sync gap or a Garmin export import."
              style={{padding:"8px 14px",borderRadius:8,border:`1px solid ${C.border}`,fontSize:12,fontWeight:600,
                background:"transparent",color:rebuilding?C.textMuted:C.text,cursor:rebuilding?"wait":"pointer"}}>
              {rebuilding ? "Rebuilding…" : "Rebuild from activities"}
            </button>
            {rebuildMsg && <span style={{fontSize:11,color:C.textMuted}}>{rebuildMsg}</span>}
            </div>
            <div style={{display:"flex",gap:6}}>
            {[["all","All Time"],["1y","1 Year"],["6m","6 Months"],["3m","3 Months"]].map(([val,label]) => (
              <button key={val} onClick={() => setRange(val)}
                style={{padding:"4px 12px",borderRadius:6,border:"none",fontSize:11,fontWeight:range===val?700:400,
                  background:range===val?C.cyan+"20":"transparent",color:range===val?C.cyan:C.textMuted,cursor:"pointer"}}>
                {label}
              </button>
            ))}
            </div>
          </div>

          {/* AI Coach Insights */}
          {insights && (
            <Card style={{borderColor:C.cyan+"40"}}>
              <div style={{fontSize:14,fontWeight:700,color:C.text,marginBottom:12,display:"flex",alignItems:"center",gap:8}}>
                <span style={{fontSize:18}}>{"🏃"}</span> Coach Analysis
              </div>
              {insights.overallAssessment && (
                <div style={{fontSize:12,color:C.text,lineHeight:1.7,marginBottom:14,padding:"10px 14px",background:C.bg2,borderRadius:8}}>
                  {insights.overallAssessment}
                </div>
              )}
              <div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:14,marginBottom:14}}>
                {insights.volumeTrend && (
                  <div style={{padding:"10px 12px",background:C.bg2,borderRadius:8}}>
                    <div style={{fontSize:10,fontWeight:700,color:C.cyan,textTransform:"uppercase",letterSpacing:"0.05em",marginBottom:4}}>Volume Trend</div>
                    <div style={{fontSize:11,color:C.textDim,lineHeight:1.6}}>{insights.volumeTrend}</div>
                  </div>
                )}
                {insights.formAnalysis && (
                  <div style={{padding:"10px 12px",background:C.bg2,borderRadius:8}}>
                    <div style={{fontSize:10,fontWeight:700,color:C.purple,textTransform:"uppercase",letterSpacing:"0.05em",marginBottom:4}}>Running Form</div>
                    <div style={{fontSize:11,color:C.textDim,lineHeight:1.6}}>{insights.formAnalysis}</div>
                  </div>
                )}
                {insights.polarizationCompliance && (
                  <div style={{padding:"10px 12px",background:C.bg2,borderRadius:8}}>
                    <div style={{fontSize:10,fontWeight:700,color:C.green,textTransform:"uppercase",letterSpacing:"0.05em",marginBottom:4}}>80/20 Compliance</div>
                    <div style={{fontSize:11,color:C.textDim,lineHeight:1.6}}>{insights.polarizationCompliance}</div>
                  </div>
                )}
                {insights.marathonReadiness && (
                  <div style={{padding:"10px 12px",background:C.bg2,borderRadius:8}}>
                    <div style={{fontSize:10,fontWeight:700,color:C.amber,textTransform:"uppercase",letterSpacing:"0.05em",marginBottom:4}}>Marathon Readiness</div>
                    <div style={{fontSize:11,color:C.textDim,lineHeight:1.6}}>{insights.marathonReadiness}</div>
                  </div>
                )}
              </div>
              {insights.strengths?.length > 0 && (
                <div style={{marginBottom:10}}>
                  <div style={{fontSize:10,fontWeight:700,color:C.green,textTransform:"uppercase",marginBottom:4}}>Strengths</div>
                  {insights.strengths.map((s,i) => (
                    <div key={i} style={{display:"flex",gap:6,fontSize:11,color:C.green,marginBottom:3}}>
                      <span>+</span><span>{s}</span>
                    </div>
                  ))}
                </div>
              )}
              {insights.concerns?.length > 0 && (
                <div style={{marginBottom:10}}>
                  <div style={{fontSize:10,fontWeight:700,color:C.amber,textTransform:"uppercase",marginBottom:4}}>Concerns</div>
                  {insights.concerns.map((c,i) => (
                    <div key={i} style={{display:"flex",gap:6,fontSize:11,color:C.amber,marginBottom:3}}>
                      <span>!</span><span>{c}</span>
                    </div>
                  ))}
                </div>
              )}
              {insights.recommendations?.length > 0 && (
                <div style={{marginBottom:10}}>
                  <div style={{fontSize:10,fontWeight:700,color:C.cyan,textTransform:"uppercase",marginBottom:4}}>Recommendations</div>
                  {insights.recommendations.map((r,i) => (
                    <div key={i} style={{display:"flex",gap:6,fontSize:11,color:C.cyan,marginBottom:3}}>
                      <span style={{color:C.cyan}}>→</span><span>{r}</span>
                    </div>
                  ))}
                </div>
              )}
              {insights.keyMetric && (
                <div style={{padding:"8px 12px",background:C.amber+"10",border:`1px solid ${C.amber}30`,borderRadius:8,fontSize:11,color:C.amber}}>
                  <strong>Key Focus:</strong> {insights.keyMetric}
                </div>
              )}
            </Card>
          )}

          <div style={{display:"grid",gridTemplateColumns:"repeat(5, 1fr)",gap:10}}>
            {[
              {label:"Total Runs",value:data.totalRuns,color:C.cyan},
              {label:"Total Miles",value:Math.round(totalMiles).toLocaleString(),color:C.green},
              {label:"Total Hours",value:Math.round(totalHours).toLocaleString(),color:C.purple},
              {label:"Avg Weekly",value:`${avgWeeklyMiles.toFixed(1)} mi`,color:C.amber},
              {label:"Peak Week",value:peakWeek?`${peakWeek.miles} mi`:"--",sub:peakWeek?.week,color:C.red},
            ].map((s,i) => (
              <Card key={i} style={{textAlign:"center",padding:"12px 8px"}}>
                <div style={{fontSize:10,color:C.textMuted,marginBottom:4}}>{s.label}</div>
                <div style={{fontSize:18,fontWeight:700,color:s.color,fontFamily:"'Space Grotesk',sans-serif"}}>{s.value}</div>
                {s.sub && <div style={{fontSize:9,color:C.textMuted,marginTop:2}}>{s.sub}</div>}
              </Card>
            ))}
          </div>

          <Card>
            <div style={{fontSize:13,fontWeight:700,color:C.text,marginBottom:12}}>Weekly Mileage</div>
            <ResponsiveContainer width="100%" height={200}>
              <ComposedChart data={weekly}>
                <XAxis dataKey="week" tick={{fontSize:9,fill:C.textMuted}} tickFormatter={w => w.slice(5)} interval={Math.max(1,Math.floor(weekly.length/12))} />
                <YAxis tick={{fontSize:9,fill:C.textMuted}} />
                <Tooltip contentStyle={{background:C.bg2,border:`1px solid ${C.border}`,borderRadius:8,fontSize:11}} />
                <Bar dataKey="miles" fill={C.cyan+"80"} radius={[2,2,0,0]} name="Miles" />
                <Line dataKey="longRun" stroke={C.amber} strokeWidth={2} dot={false} name="Long Run" />
              </ComposedChart>
            </ResponsiveContainer>
          </Card>

          {/* Marathon Predicted Time removed: this read Garmin's own race
              predictor (different algorithm + frequently stale in the
              history doc — last update was Dec 2025 even though predictions
              kept moving in Garmin Connect). Status → Race Time History is
              the canonical projection trajectory, sourced from your Garmin
              VO2max readings week-by-week. */}

          {/* Multi-year fitness arc — every build and peak on one curve. */}
          {fitnessArcShown.length >= 8 && (
            <Card>
              <div style={{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:12,flexWrap:"wrap",gap:8}}>
                <div style={{fontSize:13,fontWeight:700,color:C.text}}>Fitness Arc <span style={{fontSize:10,color:C.textMuted,fontWeight:400,marginLeft:6}}>· CTL (42-day load) across the full log</span></div>
                {fitnessArcPeak && (
                  <div style={{fontSize:11,color:C.cyan,fontFamily:"'IBM Plex Mono',monospace"}}>peak {Math.round(fitnessArcPeak.fitness)} · {fitnessArcPeak.date}</div>
                )}
              </div>
              <ResponsiveContainer width="100%" height={200}>
                <AreaChart data={fitnessArcShown} margin={{top:8,right:16,left:-8,bottom:0}}>
                  <CartesianGrid strokeDasharray="3 3" stroke={C.border} vertical={false} />
                  <XAxis dataKey="date" tick={{fontSize:9,fill:C.textMuted}} tickFormatter={d => d.slice(0,7)} minTickGap={40} />
                  <YAxis tick={{fontSize:9,fill:C.textMuted}} />
                  <Tooltip contentStyle={{background:C.bg2,border:`1px solid ${C.border}`,borderRadius:8,fontSize:11,color:C.text}} formatter={(v) => [Math.round(v), "Fitness (CTL)"]} />
                  <Area type="monotone" dataKey="fitness" stroke={C.cyan} strokeWidth={2} fill={C.cyan} fillOpacity={0.10} dot={false} />
                </AreaChart>
              </ResponsiveContainer>
              <div style={{marginTop:6,fontSize:10,color:C.textMuted,fontStyle:"italic",lineHeight:1.5}}>
                Same Banister TRIMP→CTL model as the live Fitness &amp; Fatigue chart, run over the whole history. Compare where past race-day peaks sat vs today's fitness.
              </div>
            </Card>
          )}

          {/* Setbacks & personal load ceiling */}
          {setbackAnalysis && (
            <Card>
              <div style={{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:10,flexWrap:"wrap",gap:8}}>
                <div style={{fontSize:13,fontWeight:700,color:C.text}}>Setbacks &amp; Your Load Ceiling <span style={{fontSize:10,color:C.textMuted,fontWeight:400,marginLeft:6}}>· gaps ≥10 days and what preceded them</span></div>
                <div style={{fontSize:11,fontFamily:"'IBM Plex Mono',monospace",color:setbackAnalysis.curMaxAcwr >= setbackAnalysis.ceiling ? C.red : C.green}}>
                  now: ACWR {setbackAnalysis.curMaxAcwr} vs your ceiling {setbackAnalysis.ceiling}
                </div>
              </div>
              <div style={{display:"grid",gridTemplateColumns:"auto auto auto auto",gap:"4px 16px",fontSize:12,fontFamily:"'IBM Plex Mono',monospace"}}>
                <div style={{color:C.textMuted,fontSize:10}}>GAP STARTED</div>
                <div style={{color:C.textMuted,fontSize:10,textAlign:"right"}}>LENGTH</div>
                <div style={{color:C.textMuted,fontSize:10,textAlign:"right"}}>PEAK ACWR BEFORE</div>
                <div style={{color:C.textMuted,fontSize:10,textAlign:"right"}}>4-WK CTL RAMP</div>
                {setbackAnalysis.rows.map((r, i) => (
                  <React.Fragment key={i}>
                    <div style={{color:C.text}}>{r.date}</div>
                    <div style={{color:C.textDim,textAlign:"right"}}>{r.days}d</div>
                    <div style={{color:r.maxAcwr > 1.3 ? C.amber : C.textDim,textAlign:"right"}}>{r.maxAcwr}</div>
                    <div style={{color:r.ctlRamp > 0 ? C.amber : C.textDim,textAlign:"right"}}>{r.ctlRamp > 0 ? "+" : ""}{r.ctlRamp}</div>
                  </React.Fragment>
                ))}
              </div>
              <div style={{marginTop:8,fontSize:10,color:C.textMuted,fontStyle:"italic",lineHeight:1.5}}>
                "Ceiling" = the LOWEST pre-setback load spike in your history — staying under it has kept you running. Gaps mix causes (overuse injury, RED-S/fatigue spells in the low-nutrition era, plus any planned breaks), so read alongside what you know about each block; recent well-fuelled blocks may tolerate more. Current 3-week peak ACWR and 4-week CTL ramp shown top-right.
              </div>
            </Card>
          )}

          {(endurance.length > 0 || hill.length > 0) && (() => {
            // Endurance Score is native ~thousands (shown ÷100 → ~50-75); Hill
            // Score is native ~tens. Sharing one ÷100 axis crushed Hill onto
            // the baseline so it read as a dead flat-zero line. Give Hill its
            // OWN right-hand axis at its native scale, and only draw it when
            // there's real data — otherwise tell the athlete how to capture it
            // (it comes from the userscript scraping Garmin's Hill Score page,
            // NOT from the activity sync or the export ZIP).
            const hillHasData = hill.some(d => d.value > 0);
            return (
            <Card>
              <div style={{fontSize:13,fontWeight:700,color:C.text,marginBottom:12}}>Endurance & Hill Score</div>
              <ResponsiveContainer width="100%" height={180}>
                <LineChart>
                  <XAxis dataKey="date" tick={{fontSize:9,fill:C.textMuted}} tickFormatter={d => d.slice(5)} type="category" allowDuplicatedCategory={false} interval={Math.max(1,Math.floor(Math.max(endurance.length,hill.length)/12))} />
                  <YAxis yAxisId="end" tick={{fontSize:9,fill:C.cyan}} tickFormatter={v => Math.round(v/100)} />
                  {hillHasData && <YAxis yAxisId="hill" orientation="right" tick={{fontSize:9,fill:C.amber}} tickFormatter={v => Math.round(v)} domain={["auto","auto"]} />}
                  <Tooltip contentStyle={{background:C.bg2,border:`1px solid ${C.border}`,borderRadius:8,fontSize:11}} formatter={(v,n) => [n === "Hill" ? Math.round(v) : Math.round(v/100), n]} />
                  <Line yAxisId="end" data={endurance} dataKey="value" stroke={C.cyan} strokeWidth={2} dot={false} name="Endurance" />
                  {hillHasData && <Line yAxisId="hill" data={hill} dataKey="value" stroke={C.amber} strokeWidth={2} dot={false} name="Hill" />}
                  <Legend wrapperStyle={{fontSize:10}} />
                </LineChart>
              </ResponsiveContainer>
              {!hillHasData && (
                <div style={{fontSize:10,color:C.textMuted,marginTop:6,fontStyle:"italic"}}>
                  No Hill Score captured yet. It isn't in the activity sync or the Garmin export ZIP — open the Hill Score chart in Garmin Connect (logged in), then run Sync to capture it.
                </div>
              )}
            </Card>
            );
          })()}

          <Card>
            <div style={{fontSize:13,fontWeight:700,color:C.text,marginBottom:12}}>Running Form (Weekly Avg)</div>
            <div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:16}}>
              <div>
                <div style={{fontSize:10,color:C.textMuted,marginBottom:4}}>Ground Contact Time (ms)</div>
                <ResponsiveContainer width="100%" height={120}>
                  <LineChart data={weekly.filter(w => w.gct)}>
                    <XAxis dataKey="week" tick={false} /><YAxis tick={{fontSize:9,fill:C.textMuted}} domain={["auto","auto"]} />
                    <Tooltip contentStyle={{background:C.bg2,border:`1px solid ${C.border}`,borderRadius:8,fontSize:10}} />
                    <ReferenceLine y={240} stroke={C.green+"60"} strokeDasharray="4 4" />
                    <Line dataKey="gct" stroke={C.cyan} strokeWidth={1.5} dot={false} />
                  </LineChart>
                </ResponsiveContainer>
              </div>
              <div>
                <div style={{fontSize:10,color:C.textMuted,marginBottom:4}}>Cadence (spm)</div>
                <ResponsiveContainer width="100%" height={120}>
                  <LineChart data={weekly.filter(w => w.avgCad)}>
                    <XAxis dataKey="week" tick={false} /><YAxis tick={{fontSize:9,fill:C.textMuted}} domain={["auto","auto"]} />
                    <Tooltip contentStyle={{background:C.bg2,border:`1px solid ${C.border}`,borderRadius:8,fontSize:10}} />
                    <ReferenceLine y={170} stroke={C.green+"60"} strokeDasharray="4 4" />
                    <Line dataKey="avgCad" stroke={C.green} strokeWidth={1.5} dot={false} />
                  </LineChart>
                </ResponsiveContainer>
              </div>
              <div>
                <div style={{fontSize:10,color:C.textMuted,marginBottom:4}}>Vertical Oscillation (cm)</div>
                <ResponsiveContainer width="100%" height={120}>
                  <LineChart data={weekly.filter(w => w.vo)}>
                    <XAxis dataKey="week" tick={false} /><YAxis tick={{fontSize:9,fill:C.textMuted}} domain={["auto","auto"]} />
                    <Tooltip contentStyle={{background:C.bg2,border:`1px solid ${C.border}`,borderRadius:8,fontSize:10}} />
                    <ReferenceLine y={8} stroke={C.green+"60"} strokeDasharray="4 4" />
                    <Line dataKey="vo" stroke={C.purple} strokeWidth={1.5} dot={false} />
                  </LineChart>
                </ResponsiveContainer>
              </div>
              <div>
                <div style={{fontSize:10,color:C.textMuted,marginBottom:4}}>Avg Power (W)</div>
                <ResponsiveContainer width="100%" height={120}>
                  <LineChart data={weekly.filter(w => w.power)}>
                    <XAxis dataKey="week" tick={false} /><YAxis tick={{fontSize:9,fill:C.textMuted}} domain={["auto","auto"]} />
                    <Tooltip contentStyle={{background:C.bg2,border:`1px solid ${C.border}`,borderRadius:8,fontSize:10}} />
                    <Line dataKey="power" stroke={C.amber} strokeWidth={1.5} dot={false} />
                  </LineChart>
                </ResponsiveContainer>
              </div>
              {/* Optional metrics — only rendered when the source data
                  has them. Older watches / non-HRM-Pro+ chest straps
                  don't measure vertRatio, gctBalance, strideLen,
                  stepSpeedLoss, or perfCondition; showing five blank
                  frames is just visual noise. */}
              {weekly.some(w => w.vertRatio) && (
                <div>
                  <div style={{fontSize:10,color:C.textMuted,marginBottom:4}}>Vertical Ratio (%)</div>
                  <ResponsiveContainer width="100%" height={120}>
                    <LineChart data={weekly.filter(w => w.vertRatio)}>
                      <XAxis dataKey="week" tick={false} /><YAxis tick={{fontSize:9,fill:C.textMuted}} domain={["auto","auto"]} />
                      <Tooltip contentStyle={{background:C.bg2,border:`1px solid ${C.border}`,borderRadius:8,fontSize:10}} />
                      <ReferenceLine y={7.5} stroke={C.green+"60"} strokeDasharray="4 4" />
                      <Line dataKey="vertRatio" stroke={C.cyan} strokeWidth={1.5} dot={false} />
                    </LineChart>
                  </ResponsiveContainer>
                </div>
              )}
              {weekly.some(w => w.gctBalance) && (
                <div>
                  <div style={{fontSize:10,color:C.textMuted,marginBottom:4}}>GCT Balance (% L)</div>
                  <ResponsiveContainer width="100%" height={120}>
                    <LineChart data={weekly.filter(w => w.gctBalance)}>
                      <XAxis dataKey="week" tick={false} /><YAxis tick={{fontSize:9,fill:C.textMuted}} domain={[48,52]} />
                      <Tooltip contentStyle={{background:C.bg2,border:`1px solid ${C.border}`,borderRadius:8,fontSize:10}} />
                      <ReferenceLine y={50} stroke={C.green+"60"} strokeDasharray="4 4" />
                      <Line dataKey="gctBalance" stroke={C.purple} strokeWidth={1.5} dot={false} />
                    </LineChart>
                  </ResponsiveContainer>
                </div>
              )}
              {weekly.some(w => w.strideLen) && (
                <div>
                  <div style={{fontSize:10,color:C.textMuted,marginBottom:4}}>Stride Length (m)</div>
                  <ResponsiveContainer width="100%" height={120}>
                    <LineChart data={weekly.filter(w => w.strideLen)}>
                      <XAxis dataKey="week" tick={false} /><YAxis tick={{fontSize:9,fill:C.textMuted}} domain={["auto","auto"]} />
                      <Tooltip contentStyle={{background:C.bg2,border:`1px solid ${C.border}`,borderRadius:8,fontSize:10}} />
                      <Line dataKey="strideLen" stroke={C.green} strokeWidth={1.5} dot={false} />
                    </LineChart>
                  </ResponsiveContainer>
                </div>
              )}
              {weekly.some(w => w.stepSpeedLoss) && (
                <div>
                  <div style={{fontSize:10,color:C.textMuted,marginBottom:4}}>Step Speed Loss (cm/s)</div>
                  <ResponsiveContainer width="100%" height={120}>
                    <LineChart data={weekly.filter(w => w.stepSpeedLoss)}>
                      <XAxis dataKey="week" tick={false} /><YAxis tick={{fontSize:9,fill:C.textMuted}} domain={["auto","auto"]} />
                      <Tooltip contentStyle={{background:C.bg2,border:`1px solid ${C.border}`,borderRadius:8,fontSize:10}} />
                      <ReferenceLine y={22} stroke={C.green+"60"} strokeDasharray="4 4" />
                      <ReferenceLine y={34} stroke={C.red+"60"} strokeDasharray="4 4" />
                      <Line dataKey="stepSpeedLoss" stroke={C.amber} strokeWidth={1.5} dot={false} />
                    </LineChart>
                  </ResponsiveContainer>
                </div>
              )}
              {weekly.some(w => w.perfCondition != null) && (
                <div>
                  <div style={{fontSize:10,color:C.textMuted,marginBottom:4}}>Performance Condition</div>
                  <ResponsiveContainer width="100%" height={120}>
                    <LineChart data={weekly.filter(w => w.perfCondition != null)}>
                      <XAxis dataKey="week" tick={false} /><YAxis tick={{fontSize:9,fill:C.textMuted}} domain={["auto","auto"]} />
                      <Tooltip contentStyle={{background:C.bg2,border:`1px solid ${C.border}`,borderRadius:8,fontSize:10}} />
                      <ReferenceLine y={0} stroke={C.textMuted+"60"} strokeDasharray="4 4" />
                      <Line dataKey="perfCondition" stroke={C.red} strokeWidth={1.5} dot={false} />
                    </LineChart>
                  </ResponsiveContainer>
                </div>
              )}
            </div>
            <div style={{marginTop:10,fontSize:10,color:C.textMuted,fontStyle:"italic",lineHeight:1.5}}>
              Reference lines: GCT ≤ 240 ms, Cadence ≥ 170 spm, Vert Osc ≤ 8 cm, Vert Ratio ≤ 7.5 %, GCT Balance = 50 % (symmetric), Step Speed Loss ≤ 22 cm/s (good) / ≥ 34 cm/s (work).
            </div>
          </Card>

          <Card>
            <div style={{fontSize:13,fontWeight:700,color:C.text,marginBottom:12}}>Monthly Volume</div>
            <ResponsiveContainer width="100%" height={180}>
              <ComposedChart data={monthly}>
                <XAxis dataKey="month" tick={{fontSize:9,fill:C.textMuted}} tickFormatter={m => m.slice(2)} />
                <YAxis tick={{fontSize:9,fill:C.textMuted}} />
                <Tooltip contentStyle={{background:C.bg2,border:`1px solid ${C.border}`,borderRadius:8,fontSize:11}} />
                <Bar dataKey="miles" fill={C.cyan+"60"} radius={[2,2,0,0]} name="Miles" />
                <Bar dataKey="runs" fill={C.green+"40"} radius={[2,2,0,0]} name="Runs" yAxisId="right" />
                <YAxis yAxisId="right" orientation="right" tick={{fontSize:9,fill:C.textMuted}} />
              </ComposedChart>
            </ResponsiveContainer>
          </Card>

          {(() => {
            // Year-plus of weekly stacks is unreadable — week-to-week
            // variance dominates the picture. Smooth with a 4-week
            // trailing average so the chart shows training pattern over
            // months, not the squiggle of any single week.
            const weeklyZones = weekly.filter(w => w.z1pct != null);
            const smoothed = weeklyZones.map((w, i) => {
              const win = weeklyZones.slice(Math.max(0, i - 3), i + 1);
              const avg = (k) => win.reduce((s, x) => s + (x[k] || 0), 0) / win.length;
              return { ...w, z1pct: parseFloat(avg("z1pct").toFixed(1)), z2pct: parseFloat(avg("z2pct").toFixed(1)), z345pct: parseFloat(avg("z345pct").toFixed(1)) };
            });
            return (
              <Card>
                <div style={{fontSize:13,fontWeight:700,color:C.text,marginBottom:12}}>HR Zone Distribution <span style={{fontSize:10,color:C.textMuted,fontWeight:400,marginLeft:6}}>· 4-week trailing average</span></div>
                <ResponsiveContainer width="100%" height={180}>
                  <ComposedChart data={smoothed}>
                    <XAxis dataKey="week" tick={{fontSize:9,fill:C.textMuted}} tickFormatter={w => w.slice(5)} interval={Math.max(1,Math.floor(smoothed.length/12))} />
                    <YAxis tick={{fontSize:9,fill:C.textMuted}} domain={[0,100]} tickFormatter={v => v+"%"} />
                    <Tooltip contentStyle={{background:C.bg2,border:`1px solid ${C.border}`,borderRadius:8,fontSize:10}} />
                    <Area dataKey="z1pct" stackId="1" fill={C.green+"40"} stroke={C.green} name="Z1 Easy" />
                    <Area dataKey="z2pct" stackId="1" fill={C.amber+"40"} stroke={C.amber} name="Z2 Grey" />
                    <Area dataKey="z345pct" stackId="1" fill={C.red+"40"} stroke={C.red} name="Z3-5 Hard" />
                    <ReferenceLine y={80} stroke={C.green} strokeDasharray="6 3" />
                  </ComposedChart>
                </ResponsiveContainer>
                <div style={{fontSize:10,color:C.textMuted,textAlign:"center",marginTop:4}}>Target: 80% Z1 (green line) — Polarized 80/20</div>
              </Card>
            );
          })()}

          {readiness.length > 0 && (() => {
            // Daily Training Readiness is too noisy across years of data to
            // see trend (the chart was a vertical-line forest). Keep daily
            // as a faded background; the 7-day moving average is the
            // primary signal.
            const readinessMA = readiness.map((r, i) => {
              const window = readiness.slice(Math.max(0, i - 6), i + 1).filter(w => w.value != null);
              const ma = window.length ? window.reduce((s, w) => s + w.value, 0) / window.length : null;
              return { ...r, ma7d: ma != null ? parseFloat(ma.toFixed(1)) : null };
            });
            return (
              <Card>
                <div style={{fontSize:13,fontWeight:700,color:C.text,marginBottom:12}}>Training Readiness <span style={{fontSize:10,color:C.textMuted,fontWeight:400,marginLeft:6}}>· 7-day moving average over daily values</span></div>
                <ResponsiveContainer width="100%" height={150}>
                  <LineChart data={readinessMA}>
                    <XAxis dataKey="date" tick={{fontSize:9,fill:C.textMuted}} tickFormatter={d => d.slice(5)} interval={Math.max(1,Math.floor(readinessMA.length/12))} />
                    <YAxis tick={{fontSize:9,fill:C.textMuted}} domain={[0,100]} />
                    <Tooltip contentStyle={{background:C.bg2,border:`1px solid ${C.border}`,borderRadius:8,fontSize:10}} />
                    <Line dataKey="value" stroke={C.green + "33"} strokeWidth={1} dot={false} name="Daily" />
                    <Line dataKey="ma7d" stroke={C.green} strokeWidth={2.5} dot={false} name="7d avg" />
                  </LineChart>
                </ResponsiveContainer>
              </Card>
            );
          })()}
        </div>
      );
    }

window.AppViews = Object.assign(window.AppViews || {}, { GarminHistory });
