// running/src/whatIfView.jsx — the Simulate tab.
//
// "What happens to my race if I train differently?" Move the sliders, watch
// the fitness curve and the injury-risk band respond.
//
// The panel is organised around lib/whatIf.js's three tiers of claim and it
// renders them differently ON PURPOSE, because a reader who cannot tell them
// apart will believe the weakest one as hard as the strongest:
//
//   - the load projection (CTL / form / acute:chronic) is arithmetic on the
//     plan and is always shown;
//   - the race-fitness projection is fitted from this athlete's own history
//     and always carries its r², its sample count and its clamp;
//   - when the fit doesn't hold, the projection is ABSENT and the reason is
//     printed where the number would have been. An empty space with an
//     explanation is the honest rendering; a greyed-out number is not.
//
// Nothing here writes anything. It is a calculator over data the app already
// holds, so a scenario can be as reckless as the athlete wants to imagine.

const { C, Card, Stat, EmptyState, ChartTooltip } = window.SharedUI;
const useData = () => (window.__appUseData ? window.__appUseData() : null);
const { useState, useMemo } = React;
const {
  Line, Area, ComposedChart, XAxis, YAxis, CartesianGrid, Tooltip,
  ResponsiveContainer, ReferenceLine,
} = Recharts;

    const WI = window.WhatIf;

    // A slider that reads as a number you set, not a control you drag.
    function Knob({ label, value, min, max, step, unit, onChange, hint }) {
      return (
        <div>
          <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", marginBottom: 4 }}>
            <span style={{ fontSize: 10, color: C.textMuted, textTransform: "uppercase", letterSpacing: 1 }}>{label}</span>
            <span style={{ fontSize: 15, fontWeight: 700, color: C.cyan, fontFamily: "'IBM Plex Mono',monospace" }}>
              {value}{unit ? <span style={{ fontSize: 10, color: C.textMuted, marginLeft: 3 }}>{unit}</span> : null}
            </span>
          </div>
          <input type="range" min={min} max={max} step={step} value={value}
            onChange={e => onChange(parseFloat(e.target.value))}
            style={{ width: "100%", accentColor: C.cyan, cursor: "pointer" }} />
          {hint && <div style={{ fontSize: 10, color: C.textMuted, marginTop: 2, lineHeight: 1.4 }}>{hint}</div>}
        </div>
      );
    }

    function WhatIfView({ activities }) {
      const { athleteProfile, activePlan, trainingSnapshot } = useData() || {};
      const runs = useMemo(
        () => (activities || []).filter(a => window.RaceMath && window.RaceMath.isTrainingRun(a)),
        [activities]);

      // Seed from the athlete's actual state so the panel opens on "carry on
      // as you are" — the baseline every scenario is read against.
      const currentMiles = useMemo(() => {
        const snap = trainingSnapshot?.volume?.weeklyMilesAvg4w;
        if (Number.isFinite(snap) && snap > 0) return Math.round(snap);
        const t = window.RaceMath && window.RaceMath.trailingWeeklyMiles(runs);
        return t ? Math.round(t) : null;
      }, [trainingSnapshot, runs]);

      // Weeks to the next race, from the active plan if there is one.
      const planWeeks = useMemo(() => {
        const d = activePlan && (activePlan.raceDate || activePlan.goalRaceDate);
        if (!d) return null;
        const ms = Date.parse(d);
        if (!Number.isFinite(ms)) return null;
        const w = Math.round((ms - Date.now()) / (7 * 86400000));
        return w >= 2 && w <= 52 ? w : null;
      }, [activePlan]);

      const [weeksOut, setWeeksOut] = useState(() => planWeeks || 12);
      const [targetMiles, setTargetMiles] = useState(() => currentMiles || 40);
      const [qualityDays, setQualityDays] = useState(2);
      const [taperWeeks, setTaperWeeks] = useState(3);
      const [seeded, setSeeded] = useState(false);
      // Seed once the athlete's own numbers arrive, then leave the athlete's
      // edits alone — re-seeding on every data tick would yank the sliders
      // back mid-thought.
      if (!seeded && currentMiles) {
        setTargetMiles(currentMiles);
        if (planWeeks) setWeeksOut(planWeeks);
        setSeeded(true);
      }

      const hr = { maxHR: athleteProfile?.maxHR, restHR: athleteProfile?.restingHR };

      // Both scenarios in one pass: the one being edited, and the do-nothing
      // baseline it is being compared with. A projection with no baseline is
      // a number without a question.
      const sim = useMemo(() => {
        if (!WI || !runs.length || !currentMiles) return null;
        const common = { activities: runs, ...hr, weeksOut, currentWeeklyMiles: currentMiles };
        return {
          scenario: WI.simulate({ ...common, scenario: { targetWeeklyMiles: targetMiles, qualityDaysPerWeek: qualityDays, taperWeeks } }),
          baseline: WI.simulate({ ...common, scenario: { targetWeeklyMiles: currentMiles, qualityDaysPerWeek: qualityDays, taperWeeks } }),
        };
      }, [runs, currentMiles, weeksOut, targetMiles, qualityDays, taperWeeks, hr.maxHR, hr.restHR]);

      if (!WI) return null;
      if (!runs.length || !currentMiles) {
        return (
          <Card>
            <EmptyState icon="🧪" title="Nothing to simulate yet"
              sub="A scenario is projected forward from your own training — load per mile, fitness response, current volume. Sync some runs first." />
          </Card>
        );
      }
      const s = sim && sim.scenario;
      if (!s || !s.available) {
        return (
          <Card>
            <EmptyState icon="🧪" title="Can't project a block from this history"
              sub={(s && s.reason) || "Not enough training history."} />
          </Card>
        );
      }

      const base = sim.baseline && sim.baseline.available ? sim.baseline : null;
      const chart = s.weeks.map((w, i) => ({
        name: `W${w.weekNum}`,
        ctl: w.ctl, tsb: w.tsb, acwr: w.acwr, miles: w.miles,
        baseCtl: base && base.weeks[i] ? base.weeks[i].ctl : null,
      }));

      const deltaCtl = base && base.raceDay ? Math.round((s.raceDay.ctl - base.raceDay.ctl) * 10) / 10 : null;
      const risky = s.risk.weeksAboveAcwr.length > 0;

      // The fitted tier, or the reason there isn't one.
      const f = s.fitness;
      // The slope came with a particular mix of training. Adding easy volume
      // and expecting the gain that arrived alongside two workouts a week is
      // the specific mistake worth interrupting.
      const mixDrift = f && f.historicalQualityShare != null &&
        (qualityDays / 6) < f.historicalQualityShare * 0.6;

      // The implied time goes through predictRaceTimes, the same path the
      // Races page uses — not danielsPredict directly. Raw Daniels has no
      // endurance adjustment, so it would quote a marathon this athlete's
      // volume doesn't support and disagree with the projection they already
      // have. The scenario's own BUILD volume is what feeds that adjustment:
      // in a simulation the relevant mileage is the one being simulated, and
      // more volume legitimately helps the marathon twice — through fitness
      // and through the endurance exponent.
      // Plain computation, not useMemo: this sits after the early returns
      // above, and a hook that only runs on some renders desynchronises React's
      // hook order for the whole component.
      const buildWeeksOnly = s.weeks.filter(w => w.phase !== "taper");
      const buildMiles = buildWeeksOnly.length
        ? buildWeeksOnly.reduce((sum, w) => sum + w.miles, 0) / buildWeeksOnly.length : null;
      const RACE_METERS = { "5K": 5000, "10K": 10000, "Half Marathon": 21097, "Marathon": 42195 };
      const raceName = (activePlan && RACE_METERS[activePlan.raceDistance]) ? activePlan.raceDistance : "Marathon";
      const impliedTime = (vo2) => {
        const RM = window.RaceMath;
        if (!RM || !RM.predictRaceTimes || !vo2) return null;
        const rows = RM.predictRaceTimes(vo2, runs, null, {
          maxHR: hr.maxHR, avgWeeklyMiles: buildMiles || undefined,
        });
        const row = (rows || []).find(r => r.name === raceName);
        if (!row || !row.seconds) return null;
        const sec = Math.round(row.seconds);
        const h = Math.floor(sec / 3600), m = Math.round((sec % 3600) / 60);
        return h > 0 ? `${h}:${String(m).padStart(2, "0")}` : `${m} min`;
      };

      return (
        <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
          <div>
            <div style={{ fontFamily: "'Space Grotesk', sans-serif", fontSize: 26, fontWeight: 800, color: C.text, marginBottom: 4 }}>Simulate</div>
            <div style={{ color: C.textMuted, fontSize: 13 }}>
              Change the block and see where it lands — against carrying on exactly as you are.
            </div>
          </div>

          <Card>
            <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(180px, 1fr))", gap: 20 }}>
              <Knob label="Weeks to race" value={weeksOut} min={4} max={30} step={1} unit="wk"
                onChange={setWeeksOut} hint={planWeeks ? `your plan's race is ${planWeeks} weeks out` : null} />
              <Knob label="Target volume" value={targetMiles} min={10} max={120} step={1} unit="mi/wk"
                onChange={setTargetMiles} hint={`you're averaging ${currentMiles}`} />
              <Knob label="Quality days" value={qualityDays} min={0} max={3} step={1} unit="/wk"
                onChange={setQualityDays} hint="workouts, not counting the long run" />
              <Knob label="Taper" value={taperWeeks} min={0} max={4} step={1} unit="wk"
                onChange={setTaperWeeks} hint="volume down, sharpness kept" />
            </div>
            <div style={{ marginTop: 14, fontSize: 10, color: C.textMuted, lineHeight: 1.6 }}>
              The block ramps at no more than {Math.round(WI.MAX_RAMP_PCT * 100)}% a week with a cutback every fourth,
              and costs your own {s.rates.easyPerMile} TRIMP per easy mile and {s.rates.qualityPerMile} per quality mile —
              measured from {s.rates.easyRuns + s.rates.qualityRuns} of your runs, not from a table.
              {s.rates.inferredQualityRate && " You have no logged workouts, so the quality rate is your easy rate — treat any change in quality days as unmodelled."}
            </div>
          </Card>

          {/* ── Tier 1: arithmetic ─────────────────────────────────────── */}
          <Card>
            <div style={{ fontSize: 13, fontWeight: 600, color: C.textDim, marginBottom: 2 }}>Where the block lands</div>
            <div style={{ fontSize: 10, color: C.textMuted, marginBottom: 12, fontStyle: "italic", lineHeight: 1.5 }}>
              Fitness, form and acute:chronic ratio are the load model evaluated forward through this plan. Not a prediction — arithmetic on the training you'd do.
            </div>
            <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(110px, 1fr))", gap: 14 }}>
              <Stat label="Race-day fitness" value={Math.round(s.raceDay.ctl)} unit="CTL" color={C.cyan} />
              <Stat label="vs holding steady" value={deltaCtl == null ? "--" : (deltaCtl > 0 ? `+${deltaCtl}` : String(deltaCtl))} unit="CTL"
                color={deltaCtl == null ? C.textDim : deltaCtl > 0 ? C.green : C.textDim} />
              {/* Form has a ceiling as well as a floor. Arriving at +35 is not
                  twice as good as +18 — it means the taper ran long enough to
                  start giving back the fitness the block bought, which reads
                  as "extra fresh" and races as flat. */}
              <Stat label="Race-day form" value={Math.round(s.raceDay.tsb)} unit="TSB"
                color={s.raceDay.tsb > 30 ? C.amber : s.raceDay.tsb >= 5 ? C.green : s.raceDay.tsb >= -5 ? C.amber : C.red} />
              <Stat label="Peak load ratio" value={s.risk.peakAcwr == null ? "--" : s.risk.peakAcwr} unit="ACWR"
                color={risky ? C.red : s.risk.peakAcwr > 1.3 ? C.amber : C.green} />
              <Stat label="Biggest step up" value={`${Math.round(s.risk.biggestJumpPct * 100)}`} unit="%"
                color={s.risk.biggestJumpPct > 0.15 ? C.amber : C.textDim} />
            </div>

            <div style={{ marginTop: 16 }}>
              <div style={{ display: "flex", gap: 13, flexWrap: "wrap", fontSize: 10, marginBottom: 6 }}>
                <span style={{ color: C.cyan }}>● fitness (CTL)</span>
                <span style={{ color: C.textDim }}>● if you hold steady</span>
                <span style={{ color: C.amber }}>● form (TSB)</span>
              </div>
              <ResponsiveContainer width="100%" height={230}>
                <ComposedChart data={chart}>
                  <CartesianGrid strokeDasharray="3 3" stroke={C.border} vertical={false} />
                  <XAxis dataKey="name" tick={{ fill: C.textMuted, fontSize: 10 }} axisLine={false} tickLine={false} />
                  <YAxis yAxisId="l" tick={{ fill: C.textMuted, fontSize: 11 }} axisLine={false} tickLine={false} />
                  <YAxis yAxisId="r" orientation="right" tick={{ fill: C.textMuted, fontSize: 11 }} axisLine={false} tickLine={false} />
                  <Tooltip content={<ChartTooltip />} />
                  <ReferenceLine yAxisId="r" y={0} stroke={C.border} />
                  <Area yAxisId="l" type="monotone" dataKey="ctl" stroke={C.cyan} fill={C.cyan + "20"} strokeWidth={2} name="Fitness" isAnimationActive={false} />
                  <Line yAxisId="l" type="monotone" dataKey="baseCtl" stroke={C.textDim} strokeWidth={1.5} strokeDasharray="5 4" dot={false} name="Hold steady" isAnimationActive={false} />
                  <Line yAxisId="r" type="monotone" dataKey="tsb" stroke={C.amber} strokeWidth={2} dot={false} name="Form" isAnimationActive={false} />
                </ComposedChart>
              </ResponsiveContainer>
            </div>

            {s.raceDay.tsb > 30 && (
              <div style={{ marginTop: 12, padding: "10px 12px", background: `${C.amber}12`, border: `1px solid ${C.amber}40`, borderRadius: 8 }}>
                <div style={{ fontSize: 11, fontWeight: 700, color: C.amber, marginBottom: 4 }}>Over-tapered</div>
                <div style={{ fontSize: 10, color: C.textMuted, lineHeight: 1.6 }}>
                  You arrive at {Math.round(s.raceDay.tsb)} form, well past the +15 to +25 a race taper is aiming for.
                  Past that the taper is handing back fitness rather than fatigue — shorten it, or keep more volume in it.
                </div>
              </div>
            )}

            {risky && (
              <div style={{ marginTop: 12, padding: "10px 12px", background: `${C.red}12`, border: `1px solid ${C.red}40`, borderRadius: 8 }}>
                <div style={{ fontSize: 11, fontWeight: 700, color: C.red, marginBottom: 4 }}>
                  Load spike in week{s.risk.weeksAboveAcwr.length === 1 ? "" : "s"} {s.risk.weeksAboveAcwr.join(", ")}
                </div>
                <div style={{ fontSize: 10, color: C.textMuted, lineHeight: 1.6 }}>
                  Acute load runs more than 1.5× chronic there — the band where injury rates rise sharply (Williams 2017).
                  Lower the target, add weeks, or accept the risk knowingly.
                </div>
              </div>
            )}
          </Card>

          {/* ── Tier 2 / 3: fitted, or refused ─────────────────────────── */}
          <Card>
            <div style={{ fontSize: 13, fontWeight: 600, color: C.textDim, marginBottom: 2 }}>What that's worth</div>
            {!f ? (
              <>
                <div style={{ fontSize: 10, color: C.textMuted, marginBottom: 12, fontStyle: "italic", lineHeight: 1.5 }}>
                  Turning fitness into a race time needs a load→speed relationship, and there is no general one — only yours.
                </div>
                <div style={{ padding: "14px 16px", background: C.bg2, borderRadius: 8, border: `1px solid ${C.border}` }}>
                  <div style={{ fontSize: 12, color: C.amber, fontWeight: 600, marginBottom: 6 }}>No race-time projection</div>
                  <div style={{ fontSize: 11, color: C.textMuted, lineHeight: 1.6 }}>
                    {s.timeProjectionReason}. The load projection above still holds — it doesn't depend on this.
                  </div>
                </div>
              </>
            ) : (
              <>
                <div style={{ fontSize: 10, color: C.textMuted, marginBottom: 12, fontStyle: "italic", lineHeight: 1.5 }}>
                  Fitted from {f.samples} points across your own history, r²&nbsp;{f.r2.toFixed(2)}. An association between your training load and your
                  measured fitness — not a law, and not a promise. Read off the block's peak load
                  ({Math.round(f.fromCtl)} CTL), not race day: a taper lowers CTL deliberately and does not lower fitness.
                </div>
                <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(120px, 1fr))", gap: 14 }}>
                  <Stat label="Projected VO₂max" value={f.vo2} unit="ml/kg/min" color={C.cyan} />
                  <Stat label="Change" value={f.deltaVo2 == null ? "--" : (f.deltaVo2 > 0 ? `+${f.deltaVo2}` : String(f.deltaVo2))}
                    unit="" color={f.deltaVo2 > 0 ? C.green : C.textDim} />
                  {impliedTime(f.vo2) && <Stat label={`Implied ${raceName.toLowerCase()}`} value={impliedTime(f.vo2)} unit="" color={C.amber} />}
                </div>

                {f.clamped && (
                  <div style={{ marginTop: 12, padding: "10px 12px", background: `${C.amber}12`, border: `1px solid ${C.amber}40`, borderRadius: 8 }}>
                    <div style={{ fontSize: 11, fontWeight: 700, color: C.amber, marginBottom: 4 }}>Beyond your evidence</div>
                    <div style={{ fontSize: 10, color: C.textMuted, lineHeight: 1.6 }}>
                      This block projects {f.beyondBy} CTL past the highest training load you have ever held, so the
                      projection is held at that ceiling rather than carried up a straight line. Training beyond your
                      history may well work — this has no evidence about it either way, and would rather show you nothing
                      than an extrapolated time.
                    </div>
                  </div>
                )}

                {mixDrift && (
                  <div style={{ marginTop: 12, padding: "10px 12px", background: `${C.amber}12`, border: `1px solid ${C.amber}40`, borderRadius: 8 }}>
                    <div style={{ fontSize: 11, fontWeight: 700, color: C.amber, marginBottom: 4 }}>Different training to the fit</div>
                    <div style={{ fontSize: 10, color: C.textMuted, lineHeight: 1.6 }}>
                      The gain above came alongside {Math.round(f.historicalQualityShare * 100)}% of your miles run as quality.
                      This scenario runs {qualityDays} workout{qualityDays === 1 ? "" : "s"} a week, which is a lot less.
                      Extra easy volume is not the training that produced this relationship.
                    </div>
                  </div>
                )}
              </>
            )}
          </Card>

          <div style={{ fontSize: 10, color: C.textMuted, lineHeight: 1.6, padding: "0 4px" }}>
            Nothing here is saved and nothing here changes your plan. Load, form and the acute:chronic ratio use the same
            models as the Status page, so a number moved here means the same thing it means there.
          </div>
        </div>
      );
    }

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