// dsi.jsx  v2  —  力量剖析仪表板：DSI + EUR + SSC 利用率
// 职责：匹配 CMJ propulsive peak force 与 IMTP peak force → 计算 DSI → 训练导向建议
// 依赖：cmjStore · imtpStore（来自 app.jsx）·
//       window.DerivedForceMetricRegistry（DSI/EUR 单一来源，FORCE-WS-5b；FORCE-WS-5b② 起随页面选择器）
// 参考：Sheppard 2008 · Suchomel 2015 · NSCA JSCR

(function () {
  const { useState, useMemo } = React;

  // ── DSI zones ─────────────────────────────────────────────────────────────
  // Sheppard (2008) / Suchomel (2015):
  //   DSI < 0.6  → Strength-power deficit: high isometric but can't express dynamically
  //   0.6–0.8   → Prioritise ballistic/strength-speed methods
  //   0.8–1.0   → Optimal balanced profile
  //   > 1.0     → Strength-first: maximal strength is the limiter
  const DSI_ZONES = [
    { lo: 0,   hi: 0.6,  label: '力量-爆发缺口',    en: 'Strength-Power Deficit', color: '#ef4444', bg: 'rgba(239,68,68,.08)',
      tip: '等长峰力远高于 CMJ 发力峰值——运动员无法将力量储备转化为动态爆发输出。',
      rx: ['优先安排弹道类动作：跳跃变式、爆发上举、壶铃摆', '2–3 次/周爆发力专项组（3–5 组 × 3–5 次，休息 3 min）', '减少纯力量训练比例至每周 ≤1 次，待 DSI >0.6 再调回'] },
    { lo: 0.6, hi: 0.8,  label: '偏弹射型',          en: 'Ballistic-Bias',          color: '#f59e0b', bg: 'rgba(245,158,11,.08)',
      tip: 'CMJ 推力输出接近最大等长能力的 60–80%——爆发力尚可但最大力量尚有提升空间。',
      rx: ['保留 1–2 次/周弹道训练（跳跃、投掷）', '加入 1 次/周最大力量日（3–5 RM 深蹲 / RDL）', '4–6 周后复测 DSI，目标移入 0.8–1.0 区间'] },
    { lo: 0.8, hi: 1.0,  label: '均衡',              en: 'Balanced',                color: '#22c55e', bg: 'rgba(34,197,94,.08)',
      tip: 'CMJ 推力与 IMTP 峰力比例均衡——当前力量-爆发训练结构最优。',
      rx: ['维持现有训练结构；按周期化计划进行强度波动', '每 4–6 周复测 DSI 防止漂移', '关注 SSC 利用率：若偏低可加入弹性伸缩周期专项动作'] },
    { lo: 1.0, hi: 99,   label: '最大力量优先',      en: 'Maximal Strength First',  color: '#6366f1', bg: 'rgba(99,102,241,.08)',
      tip: 'CMJ 动态峰力接近或超过等长峰力——最大力量为当前主要短板（或 IMTP 测试技术受限）。',
      rx: ['重点增加最大力量训练：5×5 深蹲 / 硬拉 / RFE 分腿蹲', '暂时减少纯爆发力动作，待 DSI 降至 <1.0 再恢复均衡', '检查 IMTP 测试技术；如发现技术问题应先规范再解读'] },
  ];

  function getDSIZone(dsi) {
    return DSI_ZONES.find(z => dsi >= z.lo && dsi < z.hi) || DSI_ZONES[DSI_ZONES.length - 1];
  }

  // ── DSI Gauge ─────────────────────────────────────────────────────────────
  function DSIGauge({ value }) {
    const W = 600, H = 56, ML = 20, MR = 20, barH = 18, barY = 12;
    const PW = W - ML - MR;
    const SCALE_LO = 0.3, SCALE_HI = 1.3;
    const xS = v => ML + (v - SCALE_LO) / (SCALE_HI - SCALE_LO) * PW;

    const segments = DSI_ZONES.map(z => ({
      ...z,
      x1: xS(Math.max(z.lo, SCALE_LO)),
      x2: xS(Math.min(z.hi, SCALE_HI)),
      color: z.color,
    }));

    const needleX = value != null ? Math.max(ML, Math.min(ML + PW, xS(value))) : null;

    return (
      <svg viewBox={`0 0 ${W} ${H}`} style={{ width: '100%', height: 'auto', display: 'block' }}>
        {/* Zone bars */}
        {segments.map((s, i) => (
          <rect key={i} x={s.x1.toFixed(1)} y={barY} width={Math.max(0, s.x2 - s.x1).toFixed(1)} height={barH}
            fill={s.color} opacity="0.22" rx={i === 0 ? '4 0 0 4' : i === segments.length - 1 ? '0 4 4 0' : '0'} />
        ))}
        {/* Zone borders */}
        {[0.6, 0.8, 1.0].map(v => (
          <line key={v} x1={xS(v).toFixed(1)} y1={barY} x2={xS(v).toFixed(1)} y2={barY + barH}
            stroke="rgba(15,23,42,.2)" strokeWidth="1" />
        ))}
        {/* Scale labels */}
        {[0.4, 0.6, 0.8, 1.0, 1.2].map(v => (
          <text key={v} x={xS(v).toFixed(1)} y={barY + barH + 14} textAnchor="middle"
            fontSize="9" fill="var(--muted)" fontFamily="var(--font-mono)">{v.toFixed(1)}</text>
        ))}
        {/* Needle */}
        {needleX != null && (
          <g>
            <line x1={needleX.toFixed(1)} y1={barY - 6} x2={needleX.toFixed(1)} y2={barY + barH + 4}
              stroke="var(--text)" strokeWidth="2" />
            <polygon points={`${needleX},${barY - 6} ${needleX - 5},${barY - 14} ${needleX + 5},${barY - 14}`}
              fill="var(--text)" />
          </g>
        )}
        {/* DSI value label above needle */}
        {needleX != null && value != null && (
          <text x={needleX.toFixed(1)} y={barY - 17} textAnchor="middle"
            fontSize="13" fontWeight="700" fill="var(--text)" fontFamily="var(--font-mono)">
            {value.toFixed(3)}
          </text>
        )}
      </svg>
    );
  }

  // ── Session picker ────────────────────────────────────────────────────────
  function SessionPicker({ label, sessions, selectedId, onSelect, metricKey, metricLabel, metricUnit }) {
    if (!sessions.length) {
      return (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
          <div style={{ fontSize: 11, fontWeight: 600, color: 'var(--text-2)' }}>{label}</div>
          <div style={{ fontSize: 11, color: 'var(--muted)', padding: '10px 12px', background: 'var(--panel-2)', border: '1px solid var(--border)', borderRadius: 8 }}>
            暂无已保存的 session。
          </div>
        </div>
      );
    }

    return (
      <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
        <div style={{ fontSize: 11, fontWeight: 600, color: 'var(--text-2)' }}>{label}</div>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 4, maxHeight: 220, overflowY: 'auto' }}>
          {[...sessions].sort((a, b) => b.date.localeCompare(a.date)).map(s => {
            const active = s.id === selectedId;
            const val = s.best?.[metricKey];
            return (
              <button key={s.id} onClick={() => onSelect(s.id)} style={{
                display: 'flex', alignItems: 'center', justifyContent: 'space-between',
                padding: '8px 12px', borderRadius: 7, cursor: 'pointer', textAlign: 'left',
                background: active ? 'var(--accent-soft)' : 'var(--panel-2)',
                border: `1px solid ${active ? 'rgba(59,130,246,.4)' : 'var(--border)'}`,
                color: 'inherit', font: 'inherit',
              }}>
                <div style={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
                  <span style={{ fontSize: 12, color: active ? 'var(--accent-2)' : 'var(--text-2)', fontWeight: active ? 600 : 400 }}>{s.date}</span>
                  <span style={{ fontSize: 10, color: 'var(--muted)' }}>{s.fileName || '—'}</span>
                </div>
                <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 1 }}>
                  <span className="mono" style={{ fontSize: 14, fontWeight: 600, color: active ? 'var(--pos)' : 'var(--text)' }}>{val ?? '—'}</span>
                  <span style={{ fontSize: 9, color: 'var(--muted)' }}>{metricLabel} ({metricUnit})</span>
                </div>
              </button>
            );
          })}
        </div>
      </div>
    );
  }

  // ── Historical DSI trend ──────────────────────────────────────────────────
  // Auto-matches each CMJ session to the closest IMTP session within 14 days.
  // FORCE-WS-5b: each pair's DSI goes through DerivedForceMetricRegistry (single
  // source) instead of a local computeDSI call — same math, one computation path.
  function DSITrend({ cmjSessions, imtpSessions }) {
    const DFR = window.DerivedForceMetricRegistry;
    const pairs = useMemo(() => {
      const sorted = [...cmjSessions].sort((a, b) => a.date.localeCompare(b.date));
      return sorted.map(cmj => {
        if (!cmj.best?.peakPropForce) return null;
        const cmjMs = new Date(cmj.date).getTime();
        let closest = null, minDiff = Infinity;
        imtpSessions.forEach(i => {
          if (!i.best?.peakForce) return;
          const diff = Math.abs(new Date(i.date).getTime() - cmjMs);
          if (diff < minDiff && diff <= 14 * 86400000) { minDiff = diff; closest = i; }
        });
        if (!closest) return null;
        const result = (DFR && DFR.computeDerived) ? DFR.computeDerived('dsi', { cmj, imtp: closest }) : null;
        const dsi = result ? result.value : null;
        return dsi != null ? { date: cmj.date, dsi, cmjDate: cmj.date, imtpDate: closest.date } : null;
      }).filter(Boolean);
    }, [cmjSessions, imtpSessions]);

    if (pairs.length < 2) return null;

    const W = 680, H = 180, ML = 52, MR = 12, MT = 12, MB = 34;
    const PW = W - ML - MR, PH = H - MT - MB;
    const SCALE_LO = 0.4, SCALE_HI = 1.2;
    const yS = v => MT + PH * (1 - (v - SCALE_LO) / (SCALE_HI - SCALE_LO));
    const xS = i => ML + (pairs.length < 2 ? PW / 2 : i / (pairs.length - 1) * PW);

    const yTicks = [0.4, 0.6, 0.8, 1.0, 1.2];

    return (
      <div style={{ background: 'var(--panel)', border: '1px solid var(--border)', borderRadius: 10, padding: '10px 6px 6px' }}>
        <div style={{ padding: '0 10px 6px', fontSize: 11, fontWeight: 600, color: 'var(--text-2)' }}>历史 DSI 趋势（自动配对）</div>
        <svg viewBox={`0 0 ${W} ${H}`} style={{ width: '100%', height: 'auto', display: 'block' }}>
          <rect width={W} height={H} fill="var(--panel)" rx="6" />
          {/* Zone bands */}
          {DSI_ZONES.map((z, i) => {
            const y1 = Math.max(MT, yS(Math.min(z.hi, SCALE_HI)));
            const y2 = Math.min(MT + PH, yS(Math.max(z.lo, SCALE_LO)));
            const h = Math.max(0, y2 - y1);
            return <rect key={i} x={ML} y={y1.toFixed(1)} width={PW} height={h.toFixed(1)} fill={z.color} opacity="0.07" />;
          })}
          {/* Y ticks */}
          {yTicks.map(v => (
            <g key={v}>
              <line x1={ML} y1={yS(v).toFixed(1)} x2={ML + PW} y2={yS(v).toFixed(1)}
                stroke="var(--chart-grid)" strokeWidth="1" />
              <text x={ML - 5} y={yS(v) + 4} textAnchor="end" fontSize="9" fill="var(--muted)" fontFamily="var(--font-mono)">{v.toFixed(1)}</text>
            </g>
          ))}
          <line x1={ML} y1={MT} x2={ML} y2={MT + PH} stroke="var(--chart-axis)" strokeWidth="1" />
          <line x1={ML} y1={MT + PH} x2={ML + PW} y2={MT + PH} stroke="var(--chart-axis)" strokeWidth="1" />
          <polyline
            points={pairs.map((p, i) => `${xS(i).toFixed(1)},${yS(p.dsi).toFixed(1)}`).join(' ')}
            fill="none" stroke="var(--accent)" strokeWidth="1.8" />
          {pairs.map((p, i) => {
            const cx = xS(i), cy = yS(p.dsi);
            const zone = getDSIZone(p.dsi);
            return (
              <g key={i}>
                <circle cx={cx.toFixed(1)} cy={cy.toFixed(1)} r="3.5" fill={zone.color} />
                <text x={cx.toFixed(1)} y={(cy - 7).toFixed(1)} textAnchor="middle" fontSize="8.5" fill="var(--text-2)" fontFamily="var(--font-mono)">{p.dsi.toFixed(2)}</text>
                <text x={cx.toFixed(1)} y={MT + PH + 14} textAnchor="middle" fontSize="7.5" fill="var(--muted)" fontFamily="var(--font-mono)">{p.date.slice(5)}</text>
              </g>
            );
          })}
        </svg>
        <div style={{ padding: '4px 10px 0', fontSize: 9, color: 'var(--muted-2)' }}>
          配对规则：每次 CMJ session 匹配日期最近的 IMTP session（±14 天内）。
        </div>
      </div>
    );
  }

  // ── Main panel ────────────────────────────────────────────────────────────
  // ── SSC Utilization zones ────────────────────────────────────────────────
  const SSC_ZONES = [
    { lo: -Infinity, hi: 5,  label: '极低', color: '#ef4444', tip: 'CMJ 几乎未受益于预拉伸。建议重点加入 SSC 专项：落降跳、绳梯、弹力带辅助跳跃。' },
    { lo: 5,         hi: 15, label: '偏低', color: '#f59e0b', tip: '弹性力量利用率低于典型运动员水平。可安排跳跃变式训练（深跳、单腿快速跳）及橡皮筋弹射练习。' },
    { lo: 15,        hi: 30, label: '正常', color: '#22c55e', tip: 'SSC 利用率在正常运动员范围内。按计划维持，配合 CMJ / SJ 定期复测。' },
    { lo: 30,        hi: Infinity, label: '优秀', color: '#6366f1', tip: '弹性力量利用率高——优秀的拉伸缩短周期效率。继续保持并监测长期趋势。' },
  ];
  function getSSCZone(v) { return SSC_ZONES.find(z => v >= z.lo && v < z.hi) || SSC_ZONES[SSC_ZONES.length - 1]; }

  // ── Derived-metric card (FORCE-WS-5b · single-sourced from
  //    DerivedForceMetricRegistry). Mirrors force-report.jsx's ForceDerivedCard:
  //    value (or an honest 数据不足) + the FULL formula string + each source
  //    session's date — a cross-day pairing (source dates differ) is marked,
  //    never hidden. RED LINE: transparent-ratio display only, never a composite. ──
  function DerivedMetricCard({ entry, result }) {
    const sources = result && Array.isArray(result.sources) ? result.sources : null;
    const dates = sources ? sources.map(s => s.date) : [];
    const distinct = dates.filter(Boolean).filter((d, i, a) => a.indexOf(d) === i);
    const crossDay = distinct.length > 1;
    const inputRows = sources || entry.inputs.map(i => ({ type: i.type, date: null }));
    const inputLabel = entry.inputs.reduce((m, i) => { m[i.type] = i.label; return m; }, {});
    return (
      <div style={{ flex: 1, minWidth: 200, background: 'var(--panel-2)', border: '1px solid var(--border)', borderRadius: 8, padding: '12px 14px' }}>
        <div style={{ fontSize: 12, fontWeight: 600, color: 'var(--text)' }}>{entry.label}</div>
        {result ? (
          <div style={{ display: 'flex', alignItems: 'baseline', gap: 5, marginTop: 4 }}>
            <span className="mono" style={{ fontSize: 24, fontWeight: 700, color: 'var(--text)' }}>{result.value.toFixed(3)}</span>
            {entry.unit ? <span style={{ fontSize: 11, color: 'var(--muted)' }}>{entry.unit}</span> : null}
          </div>
        ) : (
          <div style={{ fontSize: 12, color: 'var(--muted)', marginTop: 4 }}>数据不足 · 缺少来源会话</div>
        )}
        <div style={{ fontSize: 10.5, color: 'var(--muted)', background: 'var(--panel)', borderRadius: 6, padding: '5px 8px', margin: '8px 0 6px', fontFamily: 'var(--font-mono)' }}>{entry.formula}</div>
        <div style={{ fontSize: 9.5, color: 'var(--muted-2)', lineHeight: 1.7, fontFamily: 'var(--font-mono)' }}>
          {inputRows.map((s, i) => (
            <div key={i}>
              {(inputLabel[s.type] || s.type.toUpperCase())} · {s.date ? String(s.date).slice(0, 10) : '—'}
              {i === 0 && crossDay ? (
                <span style={{ display: 'inline-block', marginLeft: 8, padding: '1px 7px', borderRadius: 999, fontSize: 9, letterSpacing: '.05em', background: 'rgba(251,146,60,.14)', color: '#b45309', border: '1px solid rgba(251,146,60,.4)' }}>跨日配对</span>
              ) : null}
            </div>
          ))}
        </div>
      </div>
    );
  }

  function DSIPanel({ athletes = [], cmjStore = {}, sjStore = {}, imtpStore = {}, onBack }) {
    const [athleteId, setAthleteId] = useState(athletes[0]?.id ?? null);
    const [cmjId,     setCmjId]     = useState(null);
    const [sjId,      setSjId]      = useState(null);
    const [imtpId,    setImtpId]    = useState(null);

    const FSS = window.ForceSessionSource;
    const effectiveSessions = sessions => (sessions || []).map(session =>
      FSS && typeof FSS.resolveEffectiveSession === 'function'
        ? FSS.resolveEffectiveSession(session)
        : session
    );
    const cmjSessions  = effectiveSessions(cmjStore[athleteId]  || []);
    const sjSessions   = effectiveSessions(sjStore[athleteId]   || []);
    const imtpSessions = effectiveSessions(imtpStore[athleteId] || []);

    const cmjSession  = cmjSessions.find(s => s.id === cmjId)  || null;
    const sjSession   = sjSessions.find(s => s.id === sjId)    || null;
    const imtpSession = imtpSessions.find(s => s.id === imtpId) || null;

    const cmjPropForce  = cmjSession?.best?.peakPropForce  ?? null;
    const imtpPeakForce = imtpSession?.best?.peakForce ?? null;
    const cmjJH         = cmjSession?.best?.jumpHeight ?? null;
    const sjJH          = sjSession?.best?.jumpHeight ?? null;

    // ── FORCE-WS-5b · 单一衍生指标源 ──────────────────────────────────────────
    // DerivedForceMetricRegistry is the SINGLE source for DSI + EUR (and any future
    // entry — listDerived() is iterated generically below, never a hardcoded DSI-only
    // list). FORCE-WS-5b② (GPT audit P1 fix, 2026-07-12): the selection is the PAGE's
    // OWN session pickers (cmjSession/sjSession/imtpSession) — the SAME sessions SSC +
    // IMTP 早期发力 read — so the whole profile page stays coherent when the user picks
    // a historical session. Single COMPUTATION source ≠ single SELECTION strategy: the
    // report page keeps its own latest-per-type selection; this interactive page follows
    // its pickers. (Was ForceSessionSource.defaultSelection — that split the page: DSI/EUR
    // stayed latest while SSC followed the picker.)
    const DFR = window.DerivedForceMetricRegistry;
    const derivedList = (DFR && DFR.listDerived) ? DFR.listDerived() : [];
    const derivedSelection = { cmj: cmjSession, sj: sjSession, imtp: imtpSession };
    const derivedResults = {};
    derivedList.forEach(entry => {
      derivedResults[entry.id] = (DFR && DFR.computeDerived) ? DFR.computeDerived(entry.id, derivedSelection) : null;
    });

    // DSI is now rendered FROM the registry (not a separate inline computeDSI
    // call) — the registry reuses data.js computeDSI internally, so the value
    // is identical to the pre-FORCE-WS-5b display.
    const dsiResult = derivedResults.dsi || null;
    const dsi  = dsiResult ? dsiResult.value : null;
    const zone = dsi != null ? getDSIZone(dsi) : null;

    const sscUtil = (cmjJH != null && sjJH != null && sjJH > 0)
      ? (cmjJH - sjJH) / sjJH * 100
      : null;
    const sscZone = sscUtil != null ? getSSCZone(sscUtil) : null;

    // ── IMTP time-window force (from selected IMTP session best) ─────────
    const imtpF150   = imtpSession?.best?.f150   ?? null;
    const imtpF200   = imtpSession?.best?.f200   ?? null;
    const imtpNimp200 = imtpSession?.best?.nimp200 ?? null;
    const imtpPeak    = imtpSession?.best?.peakForce ?? null;

    // Auto-select most recent sessions when athlete changes
    React.useEffect(() => {
      const latestCMJ  = [...cmjSessions].sort((a, b) => b.date.localeCompare(a.date))[0];
      const latestSJ   = [...sjSessions].sort((a, b) => b.date.localeCompare(a.date))[0];
      const latestIMTP = [...imtpSessions].sort((a, b) => b.date.localeCompare(a.date))[0];
      setCmjId(latestCMJ?.id  ?? null);
      setSjId(latestSJ?.id    ?? null);
      setImtpId(latestIMTP?.id ?? null);
    }, [athleteId]);

    return (
      <main style={{ flex: 1, minWidth: 0, overflowY: 'auto', padding: '20px 24px 40px', display: 'flex', flexDirection: 'column', gap: 16 }}>
        {/* Header */}
        <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
          {onBack && (
            <button onClick={onBack} style={{ background: 'var(--panel-2)', border: '1px solid var(--border)', borderRadius: 6, padding: '5px 12px', fontSize: 12, cursor: 'pointer', color: 'var(--text-2)', fontFamily: 'var(--font-sans)' }}>← 返回</button>
          )}
          <div style={{ width: 32, height: 32, borderRadius: 8, background: 'linear-gradient(135deg,#f59e0b,#d97706)', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
            <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
              <circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/>
            </svg>
          </div>
          <div>
            <div style={{ fontSize: 17, fontWeight: 700, letterSpacing: '-.01em' }}>力量剖析仪表板 · Strength Profiling</div>
            <div style={{ fontSize: 11, color: 'var(--muted)' }}>DSI · SSC 利用率 · IMTP 早期发力 · Sheppard 2008 · Suchomel 2015</div>
          </div>
        </div>

        {/* Athlete selector */}
        <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
          <span style={{ fontSize: 11, color: 'var(--muted)', whiteSpace: 'nowrap' }}>运动员</span>
          <select value={athleteId || ''} onChange={e => setAthleteId(e.target.value)}
            style={{ fontSize: 12, padding: '5px 10px', borderRadius: 6, border: '1px solid var(--border)', background: 'var(--panel-2)', color: 'var(--text)', fontFamily: 'var(--font-sans)' }}>
            {athletes.map(a => <option key={a.id} value={a.id}>{a.name}</option>)}
          </select>
          <span style={{ fontSize: 10, color: 'var(--muted)' }}>
            CMJ: {cmjSessions.length} · SJ: {sjSessions.length} · IMTP: {imtpSessions.length} sessions
          </span>
        </div>

        {/* Session pickers */}
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 14 }}>
          <div style={{ background: 'var(--panel)', border: '1px solid var(--border)', borderRadius: 10, padding: '12px 14px' }}>
            <SessionPicker
              label="CMJ Session"
              sessions={cmjSessions}
              selectedId={cmjId}
              onSelect={setCmjId}
              metricKey="peakPropForce"
              metricLabel="Prop Peak F"
              metricUnit="N"
            />
          </div>
          <div style={{ background: 'var(--panel)', border: '1px solid var(--border)', borderRadius: 10, padding: '12px 14px' }}>
            <SessionPicker
              label="SJ Session"
              sessions={sjSessions}
              selectedId={sjId}
              onSelect={setSjId}
              metricKey="jumpHeight"
              metricLabel="Jump Height"
              metricUnit="cm"
            />
          </div>
          <div style={{ background: 'var(--panel)', border: '1px solid var(--border)', borderRadius: 10, padding: '12px 14px' }}>
            <SessionPicker
              label="IMTP Session"
              sessions={imtpSessions}
              selectedId={imtpId}
              onSelect={setImtpId}
              metricKey="peakForce"
              metricLabel="Peak Force"
              metricUnit="N"
            />
          </div>
        </div>

        {/* ── 衍生指标 · DerivedForceMetricRegistry 单一来源（FORCE-WS-5b）──────────
             每个 listDerived() 条目在此渲染（目前 DSI + EUR；未来新增指标零改动自动出现），
             取自下方 CMJ/SJ/IMTP session 选择器所选的同一批会话——与 SSC 利用率、IMTP 早期
             发力口径一致，改选历史会话时整页同步变化。缺来源 session 时诚实显示"数据不足"，
             公式与来源日期恒定展示（跨日配对会标注）。 */}
        <div style={{ background: 'var(--panel)', border: '1px solid var(--border)', borderRadius: 10, padding: '16px 18px', display: 'flex', flexDirection: 'column', gap: 10 }}>
          <div style={{ fontSize: 12, fontWeight: 700, color: 'var(--text-2)', letterSpacing: '.04em', textTransform: 'uppercase' }}>衍生指标 · 跨类型透明比值</div>
          <div style={{ fontSize: 10.5, color: 'var(--muted)', lineHeight: 1.5 }}>EUR 与下方 SSC 利用率是同一关系的两种表达：SSC% =（EUR−1）×100，非两项独立证据。</div>
          <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
            {derivedList.length
              ? derivedList.map(entry => (
                  <DerivedMetricCard key={entry.id} entry={entry} result={derivedResults[entry.id]} />
                ))
              : <div style={{ fontSize: 11, color: 'var(--muted)' }}>未注册衍生指标。</div>}
          </div>
        </div>

        {/* DSI result */}
        {(cmjPropForce != null || imtpPeakForce != null) && (
          <div style={{ background: 'var(--panel)', border: '1px solid var(--border)', borderRadius: 10, padding: '16px 18px', display: 'flex', flexDirection: 'column', gap: 14 }}>

            {/* ── DSI 区域 ─────────────────────────────────────── */}
            <div style={{ fontSize: 12, fontWeight: 700, color: 'var(--text-2)', letterSpacing: '.04em', textTransform: 'uppercase' }}>Dynamic Strength Index · DSI</div>

            {/* Gauge */}
            {dsi != null && (
              <div>
                <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 9, color: 'var(--muted)', marginBottom: 2 }}>
                  {DSI_ZONES.map(z => <span key={z.label} style={{ color: z.color, fontWeight: 600 }}>{z.en}</span>)}
                </div>
                <DSIGauge value={dsi} />
              </div>
            )}

            {/* DSI Prescription */}
            {zone && (
              <div style={{ padding: '10px 14px', background: zone.bg, border: `1px solid ${zone.color}30`, borderRadius: 8 }}>
                <div style={{ fontSize: 11, fontWeight: 600, color: zone.color, marginBottom: 4 }}>DSI 训练方向</div>
                <div style={{ fontSize: 11, color: 'var(--text-2)', lineHeight: 1.6, marginBottom: 6 }}>{zone.tip}</div>
                <ul style={{ margin: 0, paddingLeft: 16, display: 'flex', flexDirection: 'column', gap: 3 }}>
                  {zone.rx.map((r, i) => (
                    <li key={i} style={{ fontSize: 11, color: 'var(--text-2)', lineHeight: 1.5 }}>{r}</li>
                  ))}
                </ul>
              </div>
            )}

            {/* ── SSC 利用率区域 ────────────────────────────────── */}
            {(cmjJH != null || sjJH != null) && (
              <>
                <div style={{ borderTop: '1px solid var(--border)', paddingTop: 14 }}>
                  <div style={{ fontSize: 12, fontWeight: 700, color: 'var(--text-2)', letterSpacing: '.04em', textTransform: 'uppercase', marginBottom: 10 }}>
                    SSC 利用率 · Stretch-Shortening Cycle Utilization
                  </div>
                  <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', alignItems: 'center' }}>
                    <div style={{ flex: 1, minWidth: 120, background: 'var(--panel-2)', border: '1px solid var(--border)', borderRadius: 8, padding: '10px 14px' }}>
                      <div style={{ fontSize: 10, color: 'var(--muted)', textTransform: 'uppercase', letterSpacing: '.04em' }}>CMJ Jump Height</div>
                      <div style={{ display: 'flex', alignItems: 'baseline', gap: 4, marginTop: 4 }}>
                        <span className="mono" style={{ fontSize: 22, fontWeight: 700, color: cmjJH != null ? 'var(--text)' : 'var(--muted)' }}>{cmjJH != null ? cmjJH.toFixed(1) : '—'}</span>
                        <span style={{ fontSize: 11, color: 'var(--muted)' }}>cm</span>
                      </div>
                    </div>
                    <div style={{ display: 'flex', alignItems: 'center', fontSize: 14, color: 'var(--muted)' }}>−</div>
                    <div style={{ flex: 1, minWidth: 120, background: 'var(--panel-2)', border: '1px solid var(--border)', borderRadius: 8, padding: '10px 14px' }}>
                      <div style={{ fontSize: 10, color: 'var(--muted)', textTransform: 'uppercase', letterSpacing: '.04em' }}>SJ Jump Height</div>
                      <div style={{ display: 'flex', alignItems: 'baseline', gap: 4, marginTop: 4 }}>
                        <span className="mono" style={{ fontSize: 22, fontWeight: 700, color: sjJH != null ? 'var(--text)' : 'var(--muted)' }}>{sjJH != null ? sjJH.toFixed(1) : '—'}</span>
                        <span style={{ fontSize: 11, color: 'var(--muted)' }}>cm</span>
                      </div>
                    </div>
                    <div style={{ display: 'flex', alignItems: 'center', fontSize: 14, color: 'var(--muted)' }}>÷ SJ =</div>
                    {sscUtil != null ? (
                      <div style={{ flex: 1, minWidth: 120, background: sscZone.color + '15', border: `1px solid ${sscZone.color}40`, borderRadius: 8, padding: '10px 14px' }}>
                        <div style={{ fontSize: 10, color: 'var(--muted)', textTransform: 'uppercase', letterSpacing: '.04em' }}>SSC 利用率</div>
                        <div style={{ display: 'flex', alignItems: 'baseline', gap: 4, marginTop: 4 }}>
                          <span className="mono" style={{ fontSize: 26, fontWeight: 700, color: sscUtil < 0 ? 'var(--neg)' : sscZone.color }}>{sscUtil.toFixed(1)}</span>
                          <span style={{ fontSize: 11, color: 'var(--muted)' }}>%</span>
                        </div>
                        <div style={{ fontSize: 10, fontWeight: 600, color: sscUtil < 0 ? 'var(--neg)' : sscZone.color, marginTop: 2 }}>{sscUtil < 0 ? 'CM 污染 / 技术问题' : sscZone.label}</div>
                      </div>
                    ) : (
                      <div style={{ flex: 1, minWidth: 120, background: 'var(--panel-2)', border: '1px solid var(--border)', borderRadius: 8, padding: '10px 14px' }}>
                        <div style={{ fontSize: 10, color: 'var(--muted)' }}>需选择 CMJ + SJ session</div>
                      </div>
                    )}
                  </div>

                  {/* SSC Prescription */}
                  {sscZone && sscUtil != null && sscUtil >= 0 && (
                    <div style={{ marginTop: 10, padding: '10px 14px', background: sscZone.color + '12', border: `1px solid ${sscZone.color}30`, borderRadius: 8 }}>
                      <div style={{ fontSize: 11, fontWeight: 600, color: sscZone.color, marginBottom: 3 }}>SSC 建议</div>
                      <div style={{ fontSize: 11, color: 'var(--text-2)', lineHeight: 1.6 }}>{sscZone.tip}</div>
                    </div>
                  )}
                  {sscUtil != null && sscUtil < 0 && (
                    <div style={{ marginTop: 10, padding: '10px 14px', background: 'rgba(239,68,68,.08)', border: '1px solid rgba(239,68,68,.3)', borderRadius: 8 }}>
                      <div style={{ fontSize: 11, fontWeight: 600, color: 'var(--neg)', marginBottom: 3 }}>注意：SSC 为负值</div>
                      <div style={{ fontSize: 11, color: 'var(--text-2)', lineHeight: 1.6 }}>
                        CMJ 低于 SJ 通常由 SJ 中存在反向运动（CM 污染）或 CMJ 技术问题导致。建议复核两次测试的原始录像和力-时曲线后再解读。
                      </div>
                    </div>
                  )}

                  <div style={{ fontSize: 9.5, color: 'var(--muted-2)', marginTop: 8 }}>
                    SSC 利用率 = (CMJ JH − SJ JH) / SJ JH × 100%　·　典型运动员范围 15–30%　·　参考：Moran & Wallace 2011; Gheller et al. 2015
                  </div>
                </div>
              </>
            )}

            {/* ── IMTP 早期发力 区域（四·#12）────────────────────────── */}
            {imtpPeak != null && (imtpF150 != null || imtpF200 != null || imtpNimp200 != null) && (
              <div style={{ borderTop: '1px solid var(--border)', paddingTop: 14 }}>
                <div style={{ fontSize: 12, fontWeight: 700, color: 'var(--text-2)', letterSpacing: '.04em', textTransform: 'uppercase', marginBottom: 10 }}>
                  IMTP 早期发力 · Early Phase Force（Thomas 2018 · F150/F200/IMP200）
                </div>
                <div style={{ display: 'flex', gap: 10, flexWrap: 'wrap' }}>
                  {[
                    { label: 'F150', sub: '150ms 时刻力值', val: imtpF150, pct: imtpF150 && imtpPeak ? (imtpF150 / imtpPeak * 100).toFixed(0) : null },
                    { label: 'F200', sub: '200ms 时刻力值', val: imtpF200, pct: imtpF200 && imtpPeak ? (imtpF200 / imtpPeak * 100).toFixed(0) : null },
                    { label: 'IMP₀₋₂₀₀', sub: '0–200ms 净冲量', val: imtpNimp200, unit: 'N·s', noRatio: true },
                  ].map(m => (
                    m.val != null && (
                      <div key={m.label} style={{ flex: 1, minWidth: 120, background: 'var(--panel-2)', border: '1px solid var(--border)', borderRadius: 8, padding: '10px 14px' }}>
                        <div style={{ fontSize: 10, color: 'var(--muted)', textTransform: 'uppercase', letterSpacing: '.04em' }}>{m.label}</div>
                        <div style={{ display: 'flex', alignItems: 'baseline', gap: 4, marginTop: 4 }}>
                          <span className="mono" style={{ fontSize: 20, fontWeight: 700 }}>{m.val}</span>
                          <span style={{ fontSize: 10, color: 'var(--muted)' }}>{m.unit || 'N'}</span>
                        </div>
                        <div style={{ fontSize: 9.5, color: 'var(--muted)', marginTop: 2 }}>{m.sub}</div>
                        {!m.noRatio && m.pct != null && (
                          <div style={{ fontSize: 9.5, color: 'var(--muted-2)', marginTop: 1 }}>{m.pct}% of Peak</div>
                        )}
                      </div>
                    )
                  ))}
                  {/* Peak for reference */}
                  <div style={{ flex: 1, minWidth: 120, background: 'var(--panel-2)', border: '1px solid var(--border)', borderRadius: 8, padding: '10px 14px', opacity: 0.7 }}>
                    <div style={{ fontSize: 10, color: 'var(--muted)', textTransform: 'uppercase', letterSpacing: '.04em' }}>Peak Force</div>
                    <div style={{ display: 'flex', alignItems: 'baseline', gap: 4, marginTop: 4 }}>
                      <span className="mono" style={{ fontSize: 20, fontWeight: 700 }}>{imtpPeak}</span>
                      <span style={{ fontSize: 10, color: 'var(--muted)' }}>N</span>
                    </div>
                    <div style={{ fontSize: 9.5, color: 'var(--muted)', marginTop: 2 }}>参考值 (100%)</div>
                  </div>
                </div>
                <div style={{ fontSize: 9, color: 'var(--muted-2)', marginTop: 8 }}>
                  F150/F200：ICC ≥0.7 · IMP200：ICC=0.87，CV=8.5% · 早期指标比峰力提前反映疲劳 · Thomas et al. 2018（MDPI Sports）
                </div>
              </div>
            )}

            {/* Citation */}
            <div style={{ fontSize: 9.5, color: 'var(--muted-2)', borderTop: '1px solid var(--border)', paddingTop: 8 }}>
              DSI = CMJ 推力峰值 / IMTP 峰力 · Sheppard 2008; Suchomel 2015 · SSC Utilization: Moran & Wallace 2011
            </div>
          </div>
        )}

        {/* No data state */}
        {cmjPropForce == null && imtpPeakForce == null && cmjJH == null && sjJH == null && (
          <div style={{ padding: '32px 24px', textAlign: 'center', color: 'var(--muted)', fontSize: 12 }}>
            <div style={{ fontSize: 20, marginBottom: 8 }}>📊</div>
            <div>此运动员暂无 CMJ / SJ / IMTP session 记录。</div>
            <div style={{ fontSize: 10, marginTop: 4 }}>请先在 CMJ / SJ / IMTP 分析页上传并保存 session。</div>
          </div>
        )}

        {/* Historical trend */}
        <DSITrend cmjSessions={cmjSessions} imtpSessions={imtpSessions} />

        {/* CMJ 向心曲线形态监控 */}
        {(() => {
          const PCAChart = window.CMJPCAChart;
          if (!PCAChart || cmjSessions.length < 3) return null;
          return (
            <div style={{ background: 'var(--panel)', border: '1px solid var(--border)', borderRadius: 10, padding: '10px 14px' }}>
              <div style={{ fontSize: 11, fontWeight: 600, color: 'var(--text-2)', marginBottom: 8 }}>
                CMJ 向心曲线形态监控 · PC3 趋势
              </div>
              <PCAChart cmjSessions={cmjSessions} />
            </div>
          );
        })()}
      </main>
    );
  }

  window.DSIPanel = DSIPanel;
})();
