/* anovalab.jsx — AnovaTutor: the one-way ANOVA / k-group bench. Built on window.BenchKit
   (loaded first), so this file is just the FIGURE + computeFields(). Defines window.AnovaTutor.
   computeFields() returns EXACTLY the fields server/benches/anova.js declares. */
(function () {
  const { useState, useMemo, useEffect, useRef } = React;
  const K = window.BenchKit, C = K.C, fmt = K.fmt, clamp = K.clamp, btn = K.btn;

  /* F-distribution survival P(F>x) via the regularized incomplete beta (Numerical Recipes). */
  function gammaln(x) {
    const cof = [76.18009172947146, -86.50532032941677, 24.01409824083091, -1.231739572450155, 0.1208650973866179e-2, -0.5395239384953e-5];
    let y = x, t = x + 5.5; t -= (x + 0.5) * Math.log(t); let ser = 1.000000000190015;
    for (let j = 0; j < 6; j++) { y++; ser += cof[j] / y; } return -t + Math.log(2.5066282746310005 * ser / x);
  }
  function betacf(a, b, x) {
    const MAXIT = 200, EPS = 3e-12, FPMIN = 1e-300;
    const qab = a + b, qap = a + 1, qam = a - 1;
    let c = 1, d = 1 - qab * x / qap; if (Math.abs(d) < FPMIN) d = FPMIN; d = 1 / d; let h = d;
    for (let m = 1; m <= MAXIT; m++) {
      const m2 = 2 * m;
      let aa = m * (b - m) * x / ((qam + m2) * (a + m2));
      d = 1 + aa * d; if (Math.abs(d) < FPMIN) d = FPMIN; c = 1 + aa / c; if (Math.abs(c) < FPMIN) c = FPMIN; d = 1 / d; h *= d * c;
      aa = -(a + m) * (qab + m) * x / ((a + m2) * (qap + m2));
      d = 1 + aa * d; if (Math.abs(d) < FPMIN) d = FPMIN; c = 1 + aa / c; if (Math.abs(c) < FPMIN) c = FPMIN; d = 1 / d;
      const del = d * c; h *= del; if (Math.abs(del - 1) < EPS) break;
    }
    return h;
  }
  function betai(a, b, x) {
    if (x <= 0) return 0; if (x >= 1) return 1;
    const bt = Math.exp(gammaln(a + b) - gammaln(a) - gammaln(b) + a * Math.log(x) + b * Math.log(1 - x));
    return x < (a + 1) / (a + b + 2) ? bt * betacf(a, b, x) / a : 1 - bt * betacf(b, a, 1 - x) / b;
  }
  const fSurvival = (F, d1, d2) => (F > 0 ? betai(d2 / 2, d1 / 2, d2 / (d2 + d1 * F)) : 1);

  const r2 = (x, d) => Math.round(x * Math.pow(10, d)) / Math.pow(10, d);
  const N = 10; // points per group (balanced)

  /* the physics: balanced one-way ANOVA. EXACTLY the fields anova.js declares. */
  function computeFields(means, spread) {
    const k = means.length, grand = means.reduce((a, b) => a + b, 0) / k;
    const ssB = N * means.reduce((s, m) => s + (m - grand) ** 2, 0);
    const dfB = k - 1, dfW = k * (N - 1);
    const msB = ssB / dfB, msW = spread * spread;
    const F = msW > 0 ? msB / msW : Infinity;
    const ssW = dfW * msW, ssT = ssB + ssW;
    const p = msW > 0 ? fSurvival(F, dfB, dfW) : 0;
    return {
      kGroups: k, nPerGroup: N, spread: r2(spread, 1), grandMean: r2(grand, 1),
      betweenVar: r2(msB, 2), withinVar: r2(msW, 2),
      fStat: isFinite(F) ? r2(F, 2) : 9999, dfBetween: dfB, dfWithin: dfW,
      pValue: r2(p, 4), etaSq: ssT > 0 ? r2(ssB / ssT, 3) : 0,
      meanRange: r2(Math.max(...means) - Math.min(...means), 1),
      rejected: p < 0.05, spreadHigh: spread > 8,
    };
  }

  const ZPATTERN = Array.from({ length: N }, (_, i) => ((i - (N - 1) / 2) / ((N - 1) / 2)) * 1.4);
  const ROWCOLORS = [C.teal, C.gold, C.blue];

  function AnovaFig({ task, setTask, tasks, report, event, done }) {
    const [means, setMeans] = useState([14, 25, 36]);
    const [spread, setSpread] = useState(6);
    const [drag, setDrag] = useState(null);          // index of group being dragged
    const svgRef = useRef(null), dragV = useRef(0);
    const fld = useMemo(() => computeFields(means, spread), [means, spread]);

    useEffect(() => { report(fld); }, [means, spread]); // eslint-disable-line

    const VBW = 600, X0 = 60, X1 = 560, VMIN = 0, VMAX = 50;
    const ROWY = [95, 160, 225];
    const px = (v) => X0 + (v - VMIN) / (VMAX - VMIN) * (X1 - X0);
    const grandX = px(fld.grandMean);

    useEffect(() => {
      if (drag == null) return;
      const move = (e) => {
        const rect = svgRef.current.getBoundingClientRect();
        const sx = (e.clientX - rect.left) / rect.width * VBW;
        const v = clamp(VMIN + (sx - X0) / (X1 - X0) * (VMAX - VMIN), VMIN, VMAX);
        dragV.current = Math.round(v); setMeans((m) => m.map((x, i) => (i === drag ? Math.round(v) : x)));
      };
      const up = () => { const f = computeFields(means.map((x, i) => (i === drag ? dragV.current : x)), spread); event("adjusted", `Set group ${drag + 1} mean to ${dragV.current}`, { response: `F ${fmt(f.fStat, 1)}, p ${fmt(f.pValue, 3)}` }, C.blue); setDrag(null); };
      window.addEventListener("pointermove", move); window.addEventListener("pointerup", up);
      return () => { window.removeEventListener("pointermove", move); window.removeEventListener("pointerup", up); };
    }, [drag, means, spread]); // eslint-disable-line

    const t = tasks.find((x) => x.id === task) || tasks[0];
    return (
      <>
        <K.TaskStrip tasks={tasks} cur={task} setCur={setTask} done={done} goal={t && t.goal} />
        <svg ref={svgRef} viewBox="0 0 600 270" style={{ width: "100%", background: K.SVG_BG, border: `1px solid ${C.line}`, borderRadius: 8, touchAction: "none" }}>
          {/* grand mean */}
          <line x1={grandX} y1={50} x2={grandX} y2={250} stroke={C.crimson} strokeWidth="1.5" strokeDasharray="5 4" />
          <text x={grandX} y={44} textAnchor="middle" fontSize="9.5" fill={C.crimson} fontFamily="monospace">grand {fmt(fld.grandMean)}</text>
          {means.map((mu, gi) => {
            const y = ROWY[gi];
            return (
              <g key={gi}>
                <line x1={X0} y1={y} x2={X1} y2={y} stroke={C.line} />
                {ZPATTERN.map((z, j) => <circle key={j} cx={px(clamp(mu + z * spread, VMIN, VMAX))} cy={y + ((j % 2) ? 5 : -5)} r="3.5" fill={ROWCOLORS[gi]} opacity="0.6" />)}
                {/* draggable mean handle */}
                <g style={{ cursor: "ew-resize" }} onPointerDown={() => setDrag(gi)}>
                  <line x1={px(mu)} y1={y - 22} x2={px(mu)} y2={y + 22} stroke={ROWCOLORS[gi]} strokeWidth="2.5" />
                  <circle cx={px(mu)} cy={y} r="8" fill={ROWCOLORS[gi]} opacity={drag === gi ? 0.95 : 0.55} />
                </g>
                <text x={X0 - 6} y={y + 4} textAnchor="end" fontSize="10" fill={C.mute} fontFamily="monospace">G{gi + 1}</text>
                <text x={px(mu)} y={y - 26} textAnchor="middle" fontSize="9" fill={ROWCOLORS[gi]} fontFamily="monospace">{mu}</text>
              </g>
            );
          })}
          {[0, 10, 20, 30, 40, 50].map((g) => <text key={g} x={px(g)} y={264} textAnchor="middle" fontSize="9" fill={C.faint} fontFamily="monospace">{g}</text>)}
        </svg>
        <div style={{ marginTop: 10, padding: "8px 12px", borderRadius: 6, background: C.panel, border: `1px solid ${fld.rejected ? C.teal : C.line}`, color: fld.rejected ? C.teal : C.mute, fontSize: 12.5 }}>
          {fld.rejected ? `F(${fld.dfBetween},${fld.dfWithin}) = ${fmt(fld.fStat, 2)}, p = ${fmt(fld.pValue, 3)} < .05 → groups differ significantly.` : `F = ${fmt(fld.fStat, 2)}, p = ${fmt(fld.pValue, 3)} ≥ .05 → not significant.`} Drag a group’s handle; tune the spread.
        </div>
        <div style={{ display: "flex", alignItems: "center", gap: 10, marginTop: 12 }}>
          <span style={{ fontSize: 12, color: C.mute, width: 118, fontFamily: "monospace" }}>within spread σ</span>
          <input type="range" min={2} max={15} step={1} value={spread} onChange={(e) => setSpread(parseInt(e.target.value))} onPointerUp={() => { const f = computeFields(means, spread); event("adjusted", `Set spread σ=${spread}`, { response: `F ${fmt(f.fStat, 1)}, p ${fmt(f.pValue, 3)}` }, C.amber); }} style={{ flex: 1 }} />
          <span style={{ color: C.amber, width: 40, textAlign: "right", fontFamily: "monospace" }}>{spread}</span>
        </div>
        <div style={{ display: "grid", gridTemplateColumns: "repeat(4,1fr)", gap: 8, marginTop: 12 }}>
          <K.StatMeter label="F ratio" v={fld.fStat === 9999 ? "∞" : fld.fStat} d={2} color={fld.rejected ? C.teal : C.ink} />
          <K.StatMeter label="p-value" v={fld.pValue} d={3} color={fld.rejected ? C.crimson : C.mute} />
          <K.StatMeter label="η² effect" v={fld.etaSq} d={3} color={C.blue} />
          <K.StatMeter label="MS within" v={fld.withinVar} d={1} color={C.amber} />
        </div>
      </>
    );
  }

  window.AnovaTutor = K.makeTutor(AnovaFig, { moduleLabel: "ANOVA bench", benchId: "anova" });
})();
