// cmj-pca.jsx  v1  —  CMJ 向心曲线形态监控（PC3 趋势）
// 职责：从已存 trial.curve.f + trial.keyPts.e 提取向心段 → PCA → PC3 时序图
// 依赖：无。由 DSIPanel 传入 cmjSessions 数组。
// 注：每周监控场景下 PC3 反映跨-session 曲线形态一致性，非急性疲劳分类。
// 参考：Gathercole et al. 2019 PLOS ONE PMC6619745

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

  const N_INTERP = 50;  // concentric curve interpolation length
  const MIN_SESS = 3;   // minimum sessions for PCA

  // ── helpers ──────────────────────────────────────────────────────────────

  function interp(arr, nOut) {
    const n = arr.length;
    if (n < 2) return Array(nOut).fill(arr[0] ?? 0);
    return Array.from({ length: nOut }, (_, i) => {
      const t  = i / (nOut - 1) * (n - 1);
      const lo = Math.floor(t);
      const hi = Math.min(n - 1, lo + 1);
      return arr[lo] + (arr[hi] - arr[lo]) * (t - lo);
    });
  }

  function dot(a, b) { return a.reduce((s, x, i) => s + x * b[i], 0); }

  function matvec(M, v) { return M.map(row => dot(row, v)); }

  // Power iteration: returns dominant eigenvector of a symmetric matrix
  function powerIter(M, iters = 120) {
    const n = M.length;
    // Deterministic seed: normalize first non-zero row
    let v = M.find(row => row.some(Boolean)) || M[0];
    v = v.slice();
    const nm = Math.sqrt(dot(v, v)) || 1;
    v = v.map(x => x / nm);
    for (let k = 0; k < iters; k++) {
      const Mv = matvec(M, v);
      const len = Math.sqrt(dot(Mv, Mv)) || 1;
      v = Mv.map(x => x / len);
    }
    return v;
  }

  // Deflate: remove one eigenvector component from symmetric matrix
  function deflate(M, v) {
    const lambda = dot(v, matvec(M, v));
    return M.map((row, i) => row.map((x, j) => x - lambda * v[i] * v[j]));
  }

  // ── PCA computation ───────────────────────────────────────────────────────
  // Returns { scores: [{date, pc3}], mean, sd } or null if data insufficient.

  function computePCA(sessions) {
    // 1. Extract concentric curves from each session's representative trial
    const rows = [];
    sessions.forEach(sess => {
      const trial = sess.trials?.find(item =>
        item && String(item.index) === String(sess.representative?.index)
      ) ?? sess.trials?.[0];
      if (!trial?.curve?.f || !trial?.keyPts?.e) return;

      const f     = trial.curve.f;
      const start = Math.round(trial.keyPts.e * (f.length - 1)); // zeroCross index
      const conc  = f.slice(start);
      if (conc.length < 5) return;

      rows.push({ date: sess.date, vec: interp(conc, N_INTERP) });
    });

    if (rows.length < MIN_SESS) return null;

    const N = rows.length;
    const D = N_INTERP;

    // 2. Column-wise mean centering
    const mean_col = Array(D).fill(0);
    rows.forEach(r => r.vec.forEach((v, j) => { mean_col[j] += v / N; }));
    const X = rows.map(r => r.vec.map((v, j) => v - mean_col[j]));

    // 3. Covariance matrix D×D
    const cov = Array.from({ length: D }, () => Array(D).fill(0));
    X.forEach(row => {
      for (let i = 0; i < D; i++)
        for (let j = i; j < D; j++) {
          const v = row[i] * row[j] / (N - 1);
          cov[i][j] += v;
          if (i !== j) cov[j][i] += v;
        }
    });

    // 4. Extract PC1, PC2, PC3 via power iteration + deflation
    const pc1  = powerIter(cov);
    const cov2 = deflate(cov, pc1);
    const pc2  = powerIter(cov2);
    const cov3 = deflate(cov2, pc2);
    const pc3  = powerIter(cov3);

    // 5. Project onto PC3
    const scores = rows.map((r, i) => ({ date: r.date, pc3: dot(X[i], pc3) }));

    // 6. Global mean / SD for reference band
    const mu = scores.reduce((s, r) => s + r.pc3, 0) / N;
    const sd = Math.sqrt(scores.reduce((s, r) => s + (r.pc3 - mu) ** 2, 0) / N) || 1;

    return { scores, mean: mu, sd };
  }

  // ── Chart component ───────────────────────────────────────────────────────

  function CMJPCAChart({ cmjSessions }) {
    const sorted = useMemo(
      () => [...cmjSessions].sort((a, b) => a.date.localeCompare(b.date)),
      [cmjSessions]
    );

    const result = useMemo(() => computePCA(sorted), [sorted]);

    const [hover, setHover] = useState(null);

    // Not enough data
    if (!result) {
      const validN = sorted.filter(s => s.trials?.[0]?.curve?.f).length;
      return (
        <div style={{ padding: '10px 0 2px', fontSize: 11, color: 'var(--muted)' }}>
          需至少 {MIN_SESS} 次含完整曲线的 CMJ session（MARS 汇总导入不计）。当前有效：{validN} 次。
        </div>
      );
    }

    const { scores, mean, sd } = result;
    const N  = scores.length;
    const W  = 640, H = 160, ML = 44, MR = 12, MT = 14, MB = 28;
    const PW = W - ML - MR, PH = H - MT - MB;

    // Visible range: mean ± 2.5 SD, but always include all actual scores
    const allPc3 = scores.map(s => s.pc3);
    const visLo  = Math.min(mean - 2.5 * sd, Math.min(...allPc3));
    const visHi  = Math.max(mean + 2.5 * sd, Math.max(...allPc3));
    const span   = visHi - visLo || 1;

    const xAt = i => ML + (N < 2 ? PW / 2 : i / (N - 1) * PW);
    const yAt = v => MT + PH * (1 - (Math.max(visLo, Math.min(visHi, v)) - visLo) / span);

    const dotColor = v => {
      const z = Math.abs(v - mean) / sd;
      return z > 1.5 ? 'var(--neg)' : z > 1.0 ? 'var(--warn)' : 'var(--pos)';
    };

    // Band y-coords
    const yMean  = yAt(mean);
    const y1hi   = yAt(mean + sd),   y1lo   = yAt(mean - sd);
    const y15hi  = yAt(mean + 1.5 * sd), y15lo = yAt(mean - 1.5 * sd);

    return (
      <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
        <svg viewBox={`0 0 ${W} ${H}`} style={{ width: '100%', height: 'auto', display: 'block' }}>
          <rect width={W} height={H} fill="var(--panel)" rx="6" />

          {/* ±1 SD green band */}
          <rect x={ML} y={y1hi.toFixed(1)} width={PW} height={Math.max(0, y1lo - y1hi).toFixed(1)}
            fill="var(--pos)" opacity="0.08" />
          {/* ±1–1.5 SD orange bands */}
          <rect x={ML} y={y15hi.toFixed(1)} width={PW} height={Math.max(0, y1hi - y15hi).toFixed(1)}
            fill="var(--warn)" opacity="0.07" />
          <rect x={ML} y={y1lo.toFixed(1)} width={PW} height={Math.max(0, y15lo - y1lo).toFixed(1)}
            fill="var(--warn)" opacity="0.07" />

          {/* Mean dashed line */}
          <line x1={ML} y1={yMean.toFixed(1)} x2={ML + PW} y2={yMean.toFixed(1)}
            stroke="var(--muted-2)" strokeWidth="1" strokeDasharray="4 3" />

          {/* Axes */}
          <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" />

          {/* Y labels */}
          {[{ v: mean - 1.5 * sd, lbl: '−1.5σ' }, { v: mean, lbl: 'μ' }, { v: mean + 1.5 * sd, lbl: '+1.5σ' }].map(({ v, lbl }) => (
            <text key={lbl} x={ML - 4} y={(yAt(v) + 3.5).toFixed(1)}
              textAnchor="end" fontSize="8" fill="var(--muted)" fontFamily="var(--font-mono)">{lbl}</text>
          ))}

          {/* PC3 line */}
          <polyline
            points={scores.map((s, i) => `${xAt(i).toFixed(1)},${yAt(s.pc3).toFixed(1)}`).join(' ')}
            fill="none" stroke="var(--accent)" strokeWidth="1.8" />

          {/* Dots + date labels */}
          {scores.map((s, i) => {
            const cx = xAt(i).toFixed(1), cy = yAt(s.pc3).toFixed(1);
            return (
              <g key={i}
                onMouseEnter={() => setHover(i)}
                onMouseLeave={() => setHover(null)}
                style={{ cursor: 'default' }}>
                <circle cx={cx} cy={cy} r="4" fill={dotColor(s.pc3)} />
                <text x={cx} y={(MT + PH + 14).toFixed(1)}
                  textAnchor="middle" fontSize="7.5" fill="var(--muted)"
                  fontFamily="var(--font-mono)">{s.date.slice(5)}</text>
              </g>
            );
          })}

          {/* Hover tooltip */}
          {hover != null && (() => {
            const s   = scores[hover];
            const cx  = xAt(hover);
            const cy  = yAt(s.pc3);
            const z   = (s.pc3 - mean) / sd;
            const tip = `${s.date}  z=${z.toFixed(2)}`;
            const toRight = cx < W * 0.65;
            const tx  = toRight ? cx + 6 : cx - 6;
            return (
              <g>
                <rect x={(toRight ? tx : tx - 100).toFixed(1)} y={(cy - 18).toFixed(1)}
                  width="100" height="16" rx="3" fill="var(--text)" opacity="0.82" />
                <text x={(toRight ? tx + 4 : tx - 4).toFixed(1)} y={(cy - 6).toFixed(1)}
                  textAnchor={toRight ? 'start' : 'end'}
                  fontSize="8.5" fill="white" fontFamily="var(--font-mono)">{tip}</text>
              </g>
            );
          })()}
        </svg>

        <div style={{ fontSize: 9, color: 'var(--muted-2)', lineHeight: 1.5 }}>
          PC3 = 向心段力-时曲线第3主成分得分 · 绿带=±1σ正常 · 橙带=1–1.5σ · 红点=偏移&gt;1.5σ
          · 每周监控场景：PC3 体现曲线形态跨-session 一致性，非急性疲劳量化 · Gathercole 2019 PLOS ONE
        </div>
      </div>
    );
  }

  window.CMJPCAChart = CMJPCAChart;
})();
