/* ConductBiology Study record tool (/study/app). One record per study: subjects, assay files,
   matched samples, a joined table, group statistics, and a record that can be replayed.
   Engine: window.CBStudy (lib/study/index.mjs). The Assays step is in study-assays.jsx. */

const STUDY_EXAMPLE_BASE = "/study/examples/";
const STUDY_EXAMPLE_RECORD = "tbi-cohort.study-record.json";
const STUDY_EXAMPLE_FILES = [
  ["subjects.csv", "Subject sheet"],
  ["qpcr-hippocampus-24h.txt", "qPCR export"],
  ["il6-elisa-24h.csv", "ELISA plate export"],
  ["il6-plate-map.csv", "Plate map"],
  ["open-field-day7.csv", "Open field table"],
];
const STUDY_TEMPLATE = "Animal ID,Group,Sex,Genotype\nM01,Control,F,WT\nM02,Control,M,WT\nM03,Treated,F,WT\nM04,Treated,M,WT\n";

function StudyTool() {
  const engine = useToolEngine("CBStudy");
  if (!engine) return <div className="page-wrap tool-body"><p className="tool-muted">Loading the Study record engine…</p></div>;
  return <StudyWorkspace engine={engine} savedId={toolSavedParam()} />;
}

function StudyModeChip({ status }) {
  if (status.registered) return <span className="chip chip-ok">Registered plan · {status.sha256.slice(0, 8)}</span>;
  if (status.locked) return <span className="chip chip-warn">Plan locked after data · exploratory</span>;
  return <span className="chip chip-warn">Exploratory</span>;
}

function StudyWorkspace({ engine, savedId }) {
  const { useState, useMemo, useEffect } = React;
  const [state, setState] = useState(() => engine.newStudy());
  const [step, setStep] = useState("study");
  const [busy, setBusy] = useState(null);
  const [error, setError] = useState(null);
  const [notice, setNotice] = useState(null);
  const [subjectsParse, setSubjectsParse] = useState(null);
  const [saved, setSaved] = useState(null);
  const [isExample, setIsExample] = useState(false);
  const [resetArmed, setResetArmed] = useState(false);

  const dataset = useMemo(() => engine.buildDataset(state), [engine, state]);
  const results = useMemo(() => engine.analyze(state, dataset), [engine, state, dataset]);
  const checks = useMemo(() => engine.checklist(state, dataset, results, subjectsParse), [engine, state, dataset, results, subjectsParse]);
  const status = results.plan;

  async function run(label, fn) {
    setBusy(label);
    setError(null);
    try {
      await fn();
    } catch (err) {
      setError(err.message || String(err));
    } finally {
      setBusy(null);
    }
  }

  useEffect(() => {
    if (!savedId) return undefined;
    let live = true;
    run("Opening the saved study record…", async () => {
      const item = await savedApi.get(savedId);
      if (!live) return;
      if (!item.payload || !item.payload.record) throw new Error("This saved item has no study record.");
      setState(engine.restoreRecord(item.payload.record));
      setSaved({ id: item.id, title: item.title, updatedAt: item.updatedAt, payloadSha256: item.payloadSha256 });
      setIsExample(/^made-up example/i.test(item.payload.record.study?.name || ""));
      setStep("results");
    });
    return () => { live = false; };
  }, [engine, savedId]);

  useEffect(() => {
    if (!savedId && new URLSearchParams(window.location.search).get("example")) loadExample();
  }, []);

  function open(record, { example = false, label } = {}) {
    setState(engine.restoreRecord(record));
    setSubjectsParse(null);
    setIsExample(example);
    setSaved(null);
    setStep("results");
    setNotice(label ? { tone: "ok", title: label } : null);
  }

  const loadExample = () => run("Loading the example study…", async () => {
    const response = await fetch(`${STUDY_EXAMPLE_BASE}${STUDY_EXAMPLE_RECORD}`, { cache: "no-cache" });
    if (!response.ok) throw new Error(`Could not load the example (${response.status}).`);
    open(await response.json(), { example: true });
  });

  const openRecordFile = (files) => run("Opening the study record…", async () => {
    const read = await toolReadFile(files[0]);
    let record;
    try {
      record = JSON.parse(read.text);
    } catch (_err) {
      throw new Error("That file is not a study record (.json).");
    }
    open(record, { label: `Opened ${read.name}` });
  });

  function resetAll() {
    if (!resetArmed) {
      setResetArmed(true);
      setTimeout(() => setResetArmed(false), 4000);
      return;
    }
    setResetArmed(false);
    setState(engine.newStudy());
    setSubjectsParse(null);
    setIsExample(false);
    setSaved(null);
    setNotice(null);
    setError(null);
    setStep("study");
    if (window.location.search) window.history.replaceState(null, "", "/study/app");
  }

  const unmatched = Object.values(dataset.assays).reduce((n, a) => n + a.unmatched.length, 0);
  const blocking = checks.filter((c) => c.level === "block").length;
  const steps = [
    { key: "study", label: "Study and subjects", done: state.subjects.length > 0 },
    { key: "plan", label: "Plan", done: status.locked },
    { key: "assays", label: "Assays", done: state.assays.length > 0 },
    { key: "match", label: "Match samples", done: state.assays.length > 0 && unmatched === 0 },
    { key: "results", label: "Results", done: results.analyses.length > 0 && blocking === 0 },
    { key: "record", label: "Record", done: false },
  ];

  return (
    <main>
      <div className="page-wrap">
        <div className="tool-head">
          <div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
            <span className="mono" style={{ fontSize: 11, color: "var(--brand-700)", letterSpacing: ".08em", textTransform: "uppercase", fontWeight: 600 }}>ConductBiology Study record</span>
            <StudyModeChip status={status} />
            {isExample ? <span className="chip chip-warn">Made-up example data</span> : null}
          </div>
          <h1>{state.study.name || "Study record"}</h1>
          <p>Bring the subject sheet and every assay file together: one row per animal, group statistics with figures, and a record you can replay.</p>
          <div className="tool-actions no-print">
            <button type="button" className="btn btn-ghost btn-tap" onClick={loadExample} disabled={Boolean(busy)}>Load example study</button>
            <label className="btn btn-ghost btn-tap" style={{ position: "relative", overflow: "hidden" }}>
              Open a study record
              <input type="file" accept=".json,application/json" style={{ position: "absolute", inset: 0, opacity: 0, cursor: "pointer" }}
                     onChange={(e) => { const f = Array.from(e.target.files || []); e.target.value = ""; if (f.length) openRecordFile(f); }} />
            </label>
            <button type="button" className="btn btn-link btn-tap" onClick={resetAll}>{resetArmed ? "Click again to clear everything" : "Start over"}</button>
            {busy ? <span className="tool-muted" style={{ fontSize: 13 }}>{busy}</span> : null}
          </div>
          <ToolPrivacyLine />
        </div>
        <div className="no-print"><ToolSteps steps={steps} current={step} onSelect={setStep} /></div>
      </div>

      <div className="page-wrap tool-body">
        {saved ? <ToolSavedBanner item={saved} module="study" /> : null}
        {error ? <div style={{ marginBottom: 16 }}><ToolNotice tone="block" title="Something needs attention">{error}</ToolNotice></div> : null}
        {notice ? <div style={{ marginBottom: 16 }}><ToolNotice tone={notice.tone} title={notice.title} /></div> : null}

        {step === "study" ? <StudySetupStep engine={engine} state={state} setState={setState} subjectsParse={subjectsParse} setSubjectsParse={setSubjectsParse} run={run} onNext={() => setStep("plan")} /> : null}
        {step === "plan" ? <StudyPlanStep engine={engine} state={state} setState={setState} status={status} run={run} onNext={() => setStep("assays")} /> : null}
        {step === "assays" ? (
          <>
            <StudyAssaysStep engine={engine} state={state} setState={setState} dataset={dataset} run={run} />
            {state.assays.length ? <div style={{ marginTop: 16 }}><button type="button" className="btn btn-primary btn-tap" onClick={() => setStep("match")}>Match samples</button></div> : null}
          </>
        ) : null}
        {step === "match" ? <StudyMatchStep engine={engine} state={state} setState={setState} dataset={dataset} onNext={() => setStep("results")} /> : null}
        {step === "results" ? <StudyResultsStep engine={engine} state={state} setState={setState} dataset={dataset} results={results} checks={checks} onNext={() => setStep("record")} /> : null}
        {step === "record" ? <StudyRecordStep engine={engine} state={state} dataset={dataset} results={results} saved={saved} setSaved={setSaved} isExample={isExample} /> : null}
      </div>
    </main>
  );
}

// ---------------------------------------------------------------- step 1: study and subjects

function StudySetupStep({ engine, state, setState, subjectsParse, setSubjectsParse, run, onNext }) {
  const setStudy = (changes) => setState((s) => ({ ...s, study: { ...s.study, ...changes } }));

  const loadSubjects = (files) => run("Reading the subject sheet…", async () => {
    const read = await toolReadFile(files[0]);
    const parsed = engine.readSubjects(studyRowsOf(engine, read));
    setSubjectsParse(parsed);
    if (!parsed.subjects.length) throw new Error(parsed.errors.join(" ") || "No subjects were found.");
    setState((s) => engine.withSubjects(s, parsed, read));
  });

  const counts = state.design.groups.map((g) => [g, state.subjects.filter((s) => s.group === g).length]);
  const extras = [...new Set(state.subjects.flatMap((s) => Object.keys(s.extra || {})))].slice(0, 3);

  return (
    <div className="tool-grid">
      <div className="tool-stack">
        <ToolPanel title="Study">
          <div className="grid-2" style={{ gap: 14 }}>
            <label className="tool-field"><span>Name</span>
              <input className="tool-input" value={state.study.name} placeholder="For example: TBI cohort 2026" onChange={(e) => setStudy({ name: e.target.value })} />
            </label>
            <label className="tool-field"><span>Species and strain</span>
              <input className="tool-input" value={state.study.species} placeholder="Mouse (C57BL/6J)" onChange={(e) => setStudy({ species: e.target.value })} />
            </label>
          </div>
          <label className="tool-field" style={{ marginTop: 14 }}><span>Description</span>
            <textarea className="tool-input" value={state.study.description} onChange={(e) => setStudy({ description: e.target.value })} placeholder="Design, treatments, and timepoints" />
          </label>
        </ToolPanel>
        <ToolPanel title="Subjects" actions={<button type="button" className="btn btn-ghost btn-sm" style={{ minHeight: 36 }} onClick={() => toolDownload("subject-sheet-template.csv", STUDY_TEMPLATE, "text/csv")}>Template</button>}>
          <ToolDrop title={state.subjects.length ? "Replace the subject sheet" : "Load a subject sheet"} hint="CSV or Excel with an ID column, and group, sex, genotype, or any other columns" accept={STUDY_TABLE_ACCEPT} onFiles={loadSubjects} />
          {subjectsParse && subjectsParse.errors.length ? <div style={{ marginTop: 10 }}><ToolNotice tone="block" title="The subject sheet has problems" items={subjectsParse.errors} /></div> : null}
          {subjectsParse && subjectsParse.warnings.length ? <div style={{ marginTop: 10 }}><ToolNotice tone="warn" title="Notes" items={subjectsParse.warnings} /></div> : null}
          {state.subjects.length ? (
            <>
              <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(130px, 1fr))", gap: 10, margin: "14px 0" }}>
                <ToolStat label="Subjects" value={state.subjects.length} sub={state.subjectsFile ? state.subjectsFile.name : "entered here"} />
                {counts.map(([g, n]) => <ToolStat key={g} label={g} value={n} />)}
              </div>
              <div className="table-scroll" style={{ maxHeight: 420, overflowY: "auto" }}>
                <table className="tool-table">
                  <thead><tr><th>ID</th><th>Group</th><th>Sex</th><th>Genotype</th>{extras.map((x) => <th key={x}>{x}</th>)}</tr></thead>
                  <tbody>
                    {state.subjects.map((s) => (
                      <tr key={s.id}>
                        <td>{s.id}</td><td>{s.group || <span style={{ color: "var(--warn)" }}>missing</span>}</td>
                        <td>{s.sex || <span style={{ color: "var(--warn)" }}>missing</span>}</td><td>{s.genotype || ""}</td>
                        {extras.map((x) => <td key={x}>{(s.extra || {})[x] ?? ""}</td>)}
                      </tr>
                    ))}
                  </tbody>
                </table>
              </div>
            </>
          ) : null}
        </ToolPanel>
      </div>
      <div className="tool-stack">
        <StudyDesignPanel engine={engine} state={state} setState={setState} />
        <StudyExclusionsPanel state={state} setState={setState} />
        <button type="button" className="btn btn-primary btn-tap" disabled={!state.subjects.length} onClick={onNext}>Next: the plan</button>
      </div>
    </div>
  );
}

function StudyDesignPanel({ engine, state, setState }) {
  const { groups, control, timepoints } = state.design;
  const [text, setText] = React.useState(timepoints.join(", "));
  React.useEffect(() => { setText(timepoints.join(", ")); }, [timepoints.join("|")]);
  const setDesign = (changes) => setState((s) => engine.withDesign(s, changes));
  const move = (index, delta) => {
    const next = [...groups];
    const target = index + delta;
    if (target < 0 || target >= next.length) return;
    [next[index], next[target]] = [next[target], next[index]];
    setDesign({ groups: next });
  };
  return (
    <ToolPanel title="Design">
      {groups.length ? (
        <div style={{ display: "grid", gap: 6 }}>
          <div className="tool-field"><span>Group order (figures and tables use it)</span></div>
          {groups.map((g, i) => (
            <div key={g} style={{ display: "flex", gap: 6, alignItems: "center" }}>
              <span style={{ flex: 1, fontSize: 14, overflowWrap: "anywhere" }}>{g}</span>
              <button type="button" className="btn btn-ghost btn-sm" style={{ minHeight: 40, minWidth: 40 }} aria-label={`Move ${g} up`} disabled={i === 0} onClick={() => move(i, -1)}>↑</button>
              <button type="button" className="btn btn-ghost btn-sm" style={{ minHeight: 40, minWidth: 40 }} aria-label={`Move ${g} down`} disabled={i === groups.length - 1} onClick={() => move(i, 1)}>↓</button>
            </div>
          ))}
        </div>
      ) : <p className="tool-muted" style={{ margin: 0, fontSize: 13 }}>Groups come from the subject sheet.</p>}
      <label className="tool-field" style={{ marginTop: 14 }}><span>Control group</span>
        <select className="tool-input" value={control || ""} onChange={(e) => setDesign({ control: e.target.value || null })}>
          <option value="">Choose</option>
          {groups.map((g) => <option key={g} value={g}>{g}</option>)}
        </select>
      </label>
      <label className="tool-field" style={{ marginTop: 14 }}><span>Timepoints, in order</span>
        <input className="tool-input" value={text} placeholder="baseline, 24h, 7d" onChange={(e) => setText(e.target.value)}
               onBlur={() => setDesign({ timepoints: text.split(",").map((t) => t.trim()).filter(Boolean) })} />
        <small>Sample labels such as M01-24h are matched to these. Leave empty for one timepoint.</small>
      </label>
    </ToolPanel>
  );
}

function StudyExclusionsPanel({ state, setState }) {
  const [subject, setSubject] = React.useState("");
  const [reason, setReason] = React.useState("");
  const taken = new Set(state.exclusions.map((e) => e.subject));
  const add = () => {
    if (!subject || !reason.trim()) return;
    setState((s) => ({ ...s, exclusions: [...s.exclusions, { subject, reason: reason.trim() }], history: [...s.history, { at: new Date().toISOString(), event: "subject excluded", detail: `${subject}: ${reason.trim()}` }] }));
    setSubject("");
    setReason("");
  };
  const remove = (id) => setState((s) => ({ ...s, exclusions: s.exclusions.filter((e) => e.subject !== id), history: [...s.history, { at: new Date().toISOString(), event: "exclusion removed", detail: id }] }));
  return (
    <ToolPanel title="Exclusions">
      <p className="tool-muted" style={{ margin: "0 0 10px", fontSize: 13 }}>Excluded animals stay in the joined table and leave the statistics. Each needs a reason, recorded in the methods.</p>
      {state.exclusions.map((e) => (
        <div key={e.subject} style={{ display: "flex", gap: 8, alignItems: "center", fontSize: 13.5, marginBottom: 6 }}>
          <strong>{e.subject}</strong><span style={{ flex: 1, color: "var(--ink-600)" }}>{e.reason}</span>
          <button type="button" className="btn btn-link btn-tap" onClick={() => remove(e.subject)}>Remove</button>
        </div>
      ))}
      <div style={{ display: "grid", gap: 8 }}>
        <select className="tool-input" value={subject} onChange={(e) => setSubject(e.target.value)} aria-label="Subject to exclude">
          <option value="">Subject</option>
          {state.subjects.filter((s) => !taken.has(s.id)).map((s) => <option key={s.id} value={s.id}>{s.id}</option>)}
        </select>
        <input className="tool-input" value={reason} placeholder="Reason, for example: humane endpoint on day 3" onChange={(e) => setReason(e.target.value)} aria-label="Reason" />
        <button type="button" className="btn btn-ghost btn-tap" disabled={!subject || !reason.trim()} onClick={add}>Exclude</button>
      </div>
    </ToolPanel>
  );
}

// ---------------------------------------------------------------- step 2: plan

const STUDY_OUTCOME_KINDS = [["any", "Any assay"], ["qpcr", "qPCR"], ["plate", "Plate assay"], ["table", "Behavior or table"]];

function StudyPlanStep({ engine, state, setState, status, run, onNext }) {
  const plan = state.plan;
  const groups = state.design.groups;
  const planGroups = plan.groups.length ? plan.groups : groups;
  const control = plan.control ?? state.design.control;
  const errors = engine.planErrors(plan, state);
  const update = (changes) => setState((s) => engine.withPlan(s, changes));
  const setOutcome = (id, changes) => update({ outcomes: plan.outcomes.map((o) => (o.id === id ? { ...o, ...changes } : o)) });
  const addOutcome = () => {
    let n = plan.outcomes.length + 1;
    while (plan.outcomes.some((o) => o.id === `o${n}`)) n += 1;
    update({ outcomes: [...plan.outcomes, { id: `o${n}`, name: "", kind: "any", measure: null }] });
  };
  const toggleGroup = (g) => {
    const next = planGroups.includes(g) ? planGroups.filter((x) => x !== g) : groups.filter((x) => planGroups.includes(x) || x === g);
    update({ groups: next });
  };
  const toggleTimepoint = (t) => update({ timepoints: plan.timepoints.includes(t) ? plan.timepoints.filter((x) => x !== t) : state.design.timepoints.filter((x) => plan.timepoints.includes(x) || x === t) });
  const lock = () => run("Locking the plan…", async () => { setState(await engine.lockPlan(state)); });

  if (status.locked) {
    return (
      <div className="tool-grid">
        <ToolPanel title="Locked plan">
          <ToolNotice tone={status.registered ? "ok" : "warn"} title={status.registered ? "Registered" : "Locked after assay files were added"}>
            Locked {toolFmtTime(status.lockedAt)} · SHA-256 <span className="tool-mono">{status.sha256.slice(0, 16)}…</span>.{" "}
            {status.registered ? "Outcomes linked to measures are reported as registered." : "Results are exploratory because data came first."}
          </ToolNotice>
          <dl style={{ display: "grid", gridTemplateColumns: "minmax(0, 140px) minmax(0, 1fr)", gap: "8px 14px", margin: "14px 0 0", fontSize: 14 }}>
            <dt className="tool-muted">Outcomes</dt><dd style={{ margin: 0 }}>{plan.outcomes.map((o) => <div key={o.id}>{o.name} <span className="tool-muted">({(STUDY_OUTCOME_KINDS.find((k) => k[0] === o.kind) || ["", "any"])[1]})</span></div>)}</dd>
            <dt className="tool-muted">Groups</dt><dd style={{ margin: 0 }}>{planGroups.join(", ")}</dd>
            <dt className="tool-muted">Control</dt><dd style={{ margin: 0 }}>{control || "not set"}</dd>
            <dt className="tool-muted">Timepoints</dt><dd style={{ margin: 0 }}>{plan.timepoints.length ? plan.timepoints.join(", ") : "all"}</dd>
            <dt className="tool-muted">Statistics</dt><dd style={{ margin: 0 }}>{plan.test === "rank" ? "Rank-based (Mann-Whitney U, Kruskal-Wallis)" : "Welch's t-test; one-way ANOVA for three or more groups"}</dd>
            {plan.notes ? <><dt className="tool-muted">Notes</dt><dd style={{ margin: 0 }}>{plan.notes}</dd></> : null}
          </dl>
        </ToolPanel>
        <div className="tool-stack">
          <button type="button" className="btn btn-primary btn-tap" onClick={onNext}>Next: assays</button>
          <ToolPanel title="Change the plan">
            <p style={{ margin: "0 0 10px", fontSize: 13.5, lineHeight: 1.5, color: "var(--ink-700)" }}>
              Unlocking is recorded. {state.assays.length ? "Assay files are already in, so a new lock makes the results exploratory." : "No assay file is in yet, so a new lock keeps the outcomes registered."}
            </p>
            <button type="button" className="btn btn-ghost btn-tap" onClick={() => setState((s) => engine.unlockPlan(s))}>Unlock the plan</button>
          </ToolPanel>
        </div>
      </div>
    );
  }

  return (
    <div className="tool-grid">
      <div className="tool-stack">
        <ToolPanel title="Outcomes">
          <p className="tool-muted" style={{ margin: "0 0 12px", fontSize: 13 }}>Name the measurements that answer your question. After the files are in, you link each outcome to its column.</p>
          {plan.outcomes.map((o) => (
            <div key={o.id} className="study-outcome-edit" style={{ display: "grid", gridTemplateColumns: "minmax(0, 1fr) minmax(0, 160px) auto", gap: 8, marginBottom: 8, alignItems: "center" }}>
              <input className="tool-input" value={o.name} placeholder="For example: Serum IL-6 at 24 h" aria-label="Outcome name" onChange={(e) => setOutcome(o.id, { name: e.target.value })} />
              <select className="tool-input" value={o.kind} aria-label="Assay kind" onChange={(e) => setOutcome(o.id, { kind: e.target.value })}>
                {STUDY_OUTCOME_KINDS.map(([k, label]) => <option key={k} value={k}>{label}</option>)}
              </select>
              <button type="button" className="btn btn-link btn-tap" onClick={() => update({ outcomes: plan.outcomes.filter((x) => x.id !== o.id) })}>Remove</button>
            </div>
          ))}
          <button type="button" className="btn btn-ghost btn-tap" onClick={addOutcome}>Add an outcome</button>
        </ToolPanel>
        <ToolPanel title="Comparisons">
          <div className="tool-field"><span>Groups compared</span>
            <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
              {groups.map((g) => <button key={g} type="button" className={`btn btn-sm ${planGroups.includes(g) ? "btn-primary" : "btn-ghost"}`} style={{ minHeight: 40 }} aria-pressed={planGroups.includes(g)} onClick={() => toggleGroup(g)}>{g}</button>)}
              {!groups.length ? <span className="tool-muted" style={{ fontSize: 13 }}>Load the subject sheet first.</span> : null}
            </div>
          </div>
          <label className="tool-field" style={{ marginTop: 14 }}><span>Control group</span>
            <select className="tool-input" value={control || ""} onChange={(e) => update({ control: e.target.value || null })}>
              <option value="">Choose</option>
              {planGroups.map((g) => <option key={g} value={g}>{g}</option>)}
            </select>
          </label>
          {state.design.timepoints.length ? (
            <div className="tool-field" style={{ marginTop: 14 }}><span>Timepoints analyzed (none selected = all)</span>
              <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
                {state.design.timepoints.map((t) => <button key={t} type="button" className={`btn btn-sm ${plan.timepoints.includes(t) ? "btn-primary" : "btn-ghost"}`} style={{ minHeight: 40 }} aria-pressed={plan.timepoints.includes(t)} onClick={() => toggleTimepoint(t)}>{t}</button>)}
              </div>
            </div>
          ) : null}
          <div className="tool-field" style={{ marginTop: 14 }}><span>Statistics</span>
            <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }} role="group" aria-label="Statistics">
              {[["parametric", "Welch's t-test / ANOVA"], ["rank", "Rank-based (Mann-Whitney / Kruskal-Wallis)"]].map(([k, label]) => (
                <button key={k} type="button" className={`btn btn-sm ${plan.test === k ? "btn-primary" : "btn-ghost"}`} style={{ minHeight: 40 }} aria-pressed={plan.test === k} onClick={() => update({ test: k })}>{label}</button>
              ))}
            </div>
            <small>Three or more groups: an overall test, then each group against the control, Holm-adjusted.</small>
          </div>
          <label className="tool-field" style={{ marginTop: 14 }}><span>Notes</span>
            <textarea className="tool-input" value={plan.notes} placeholder="Exclusion rules, the primary comparison, anything decided now" onChange={(e) => update({ notes: e.target.value })} />
          </label>
        </ToolPanel>
      </div>
      <div className="tool-stack">
        <ToolPanel title="Lock the plan">
          <p style={{ margin: "0 0 10px", fontSize: 13.5, lineHeight: 1.5, color: "var(--ink-700)" }}>
            Locking records a fingerprint and the time. {state.assays.length ? "Assay files are already in, so results will still be exploratory." : "Lock before adding assay files and your outcomes are registered."}
          </p>
          {errors.length ? <div style={{ marginBottom: 10 }}><ToolNotice tone="warn" items={errors} /></div> : null}
          <button type="button" className="btn btn-primary btn-tap" style={{ width: "100%" }} disabled={errors.length > 0} onClick={lock}>Lock the plan</button>
        </ToolPanel>
        <button type="button" className="btn btn-ghost btn-tap" onClick={onNext}>Skip: explore without a plan</button>
      </div>
    </div>
  );
}

// ---------------------------------------------------------------- step 4: match

const STUDY_METHOD_TONE = { exact: "chip-ok", normalized: "chip-ok", token: "chip-info", manual: "chip-info", unmatched: "chip-block" };

function StudyMatchStep({ engine, state, setState, dataset, onNext }) {
  const [onlyOpen, setOnlyOpen] = React.useState(false);
  const timepoints = [...new Set([...state.design.timepoints, ...dataset.timepoints])];
  if (!state.assays.length) return <ToolNotice tone="info" title="Add assay files first." />;
  return (
    <div className="tool-stack">
      <label style={{ display: "flex", gap: 8, alignItems: "center", fontSize: 14, minHeight: 44 }}>
        <input type="checkbox" checked={onlyOpen} onChange={(e) => setOnlyOpen(e.target.checked)} style={{ width: 20, height: 20 }} />
        Show only labels that need a fix
      </label>
      {state.assays.map((assay) => {
        const info = dataset.assays[assay.id];
        if (!info) return null;
        const rows = info.labels.filter((l) => !onlyOpen || (!l.subject && l.method !== "manual"));
        const set = (label, target) => setState((s) => engine.withMapping(s, assay.id, label, target));
        return (
          <ToolPanel key={assay.id} title={assay.file.name}
                     actions={<span className="tool-muted" style={{ fontSize: 12.5 }}>{info.labels.length - info.unmatched.length - info.ignored.length} of {info.labels.length} matched{info.ignored.length ? `, ${info.ignored.length} ignored` : ""}</span>}>
            {rows.length === 0 ? <p className="tool-muted" style={{ margin: 0, fontSize: 13 }}>Every label is matched.</p> : (
              <div className="table-scroll" style={{ maxHeight: 460, overflowY: "auto" }}>
                <table className="tool-table">
                  <thead><tr><th>Label in the file</th><th>Subject</th><th>Timepoint</th><th>How</th><th /></tr></thead>
                  <tbody>
                    {rows.map((l) => (
                      <tr key={l.label}>
                        <td style={{ overflowWrap: "anywhere" }}>{l.label}{l.candidates.length ? <div className="tool-muted" style={{ fontSize: 12 }}>Could be {l.candidates.join(", ")}</div> : null}</td>
                        <td>
                          <select className="tool-input" value={l.subject || (l.method === "manual" ? "__ignore" : "")} aria-label={`Subject for ${l.label}`}
                                  onChange={(e) => set(l.label, { subject: e.target.value === "__ignore" || e.target.value === "" ? null : e.target.value })}>
                            <option value="">Not matched</option>
                            <option value="__ignore">Ignore this label</option>
                            {state.subjects.map((s) => <option key={s.id} value={s.id}>{s.id}{s.group ? ` (${s.group})` : ""}</option>)}
                          </select>
                        </td>
                        <td>
                          <select className="tool-input" value={l.timepoint || ""} aria-label={`Timepoint for ${l.label}`}
                                  onChange={(e) => set(l.label, { subject: l.subject, timepoint: e.target.value || null })}>
                            <option value="">None</option>
                            {timepoints.map((t) => <option key={t} value={t}>{t}</option>)}
                          </select>
                        </td>
                        <td><span className={`chip ${STUDY_METHOD_TONE[l.method] || ""}`}>{l.method}</span></td>
                        <td>{l.method === "manual" ? <button type="button" className="btn btn-link btn-tap" onClick={() => set(l.label, undefined)}>Undo</button> : null}</td>
                      </tr>
                    ))}
                  </tbody>
                </table>
              </div>
            )}
          </ToolPanel>
        );
      })}
      <div><button type="button" className="btn btn-primary btn-tap" onClick={onNext}>See the results</button></div>
    </div>
  );
}

// ---------------------------------------------------------------- step 5: results

function StudyChecklist({ checks }) {
  const counts = { block: 0, warn: 0, ok: 0 };
  for (const c of checks) counts[c.level] += 1;
  const tone = { block: "block", warn: "warn", ok: "ok" };
  return (
    <ToolPanel title="Readiness" actions={<span className="tool-muted" style={{ fontSize: 12.5 }}>{counts.block} blocking · {counts.warn} to review · {counts.ok} fine</span>}>
      <div style={{ display: "grid", gap: 8 }}>
        {checks.map((c, i) => (
          <div key={`${c.id}-${i}`} className={`tool-notice tone-${tone[c.level]}`}>
            <div style={{ minWidth: 0, width: "100%" }}>
              {c.items && c.items.length ? (
                <details>
                  <summary style={{ cursor: "pointer", padding: "8px 0", minHeight: 44, boxSizing: "border-box" }}><strong>{c.text}</strong></summary>
                  <ul>{c.items.map((x, j) => <li key={j}>{x}</li>)}</ul>
                </details>
              ) : <strong>{c.text}</strong>}
            </div>
          </div>
        ))}
      </div>
    </ToolPanel>
  );
}

function StudyOutcomes({ engine, state, setState, dataset, results }) {
  if (!state.plan.outcomes.length) return null;
  return (
    <ToolPanel title="Outcomes">
      <div style={{ display: "grid", gap: 10 }}>
        {results.outcomes.map((o) => {
          const options = dataset.measures.filter((m) => o.kind === "any" || m.kind === o.kind);
          return (
            <div key={o.id} style={{ display: "grid", gridTemplateColumns: "minmax(0, 1fr) minmax(0, 300px)", gap: 10, alignItems: "center" }} className="study-outcome-row">
              <div style={{ minWidth: 0 }}>
                <div style={{ fontWeight: 600, fontSize: 14.5, overflowWrap: "anywhere" }}>{o.name}</div>
                <span className={`chip ${o.status === "registered" ? "chip-ok" : o.status === "exploratory" ? "chip-warn" : "chip-block"}`} style={{ marginTop: 4 }}>{o.status === "unlinked" ? "not linked" : o.status}</span>
              </div>
              <select className="tool-input" value={o.measure || ""} aria-label={`Measure for ${o.name}`} onChange={(e) => setState((s) => engine.linkOutcome(s, o.id, e.target.value || null))}>
                <option value="">Link to a measure…</option>
                {options.map((m) => <option key={m.id} value={m.id}>{m.name}{m.unit ? ` (${m.unit})` : ""} · {m.file}</option>)}
              </select>
            </div>
          );
        })}
      </div>
    </ToolPanel>
  );
}

// "p < 0.0001" or "p = 0.0123".
function studyPText(engine, p) {
  const text = engine.formatP(p);
  return text.startsWith("<") ? `p ${text}` : `p = ${text}`;
}

function StudyAnalysis({ engine, analysis }) {
  const figure = React.useMemo(() => engine.analysisFigure(analysis), [engine, analysis]);
  const slug = toolSlug(`${analysis.name}${analysis.timepoint ? `-${analysis.timepoint}` : ""}`, "measure");
  const testName = { welch: "Welch's t", mann_whitney: "Mann-Whitney U", anova: "One-way ANOVA", kruskal: "Kruskal-Wallis" };
  return (
    <ToolPanel title={`${analysis.name}${analysis.timepoint ? ` · ${analysis.timepoint}` : ""}`}
               actions={<span className={`chip ${analysis.status === "registered" ? "chip-ok" : "chip-warn"}`}>{analysis.status}</span>}>
      <div className="split-2" style={{ gap: 18, alignItems: "start" }}>
        <StudyFigure figure={figure} name={slug} />
        <div style={{ display: "grid", gap: 12, minWidth: 0 }}>
          <div className="table-scroll">
            <table className="tool-table">
              <thead><tr><th>Group</th><th className="num">n</th><th className="num">Mean</th><th className="num">SD</th><th className="num">SEM</th><th className="num">Median</th></tr></thead>
              <tbody>
                {analysis.groups.map((g) => (
                  <tr key={g.name}><td>{g.name}{g.name === analysis.control ? <span className="tool-muted"> (control)</span> : null}</td><td className="num">{g.n}</td><td className="num">{studyFmt(g.mean)}</td><td className="num">{studyFmt(g.sd)}</td><td className="num">{studyFmt(g.sem)}</td><td className="num">{studyFmt(g.median)}</td></tr>
                ))}
              </tbody>
            </table>
          </div>
          {analysis.omnibus ? (
            <div style={{ fontSize: 13.5 }}>
              {testName[analysis.omnibus.test] || analysis.omnibus.test}: {analysis.omnibus.test === "kruskal" ? `H = ${studyFmt(analysis.omnibus.h)}, df = ${analysis.omnibus.df}` : `F(${analysis.omnibus.df1}, ${studyFmt(analysis.omnibus.df2)}) = ${studyFmt(analysis.omnibus.f)}`}, {studyPText(engine, analysis.omnibus.p)}
            </div>
          ) : null}
          {analysis.warnings.map((w, i) => <ToolNotice key={i} tone="warn" title={w} />)}
        </div>
      </div>
      {analysis.comparisons.length ? (
        <div className="table-scroll" style={{ marginTop: 14 }}>
          <table className="tool-table">
            <thead><tr><th>Compared with {analysis.control}</th><th className="num">Difference (95% CI)</th><th className="num">{analysis.test === "rank" ? "U" : "t (df)"}</th><th className="num">p</th>{analysis.comparisons.length > 1 ? <th className="num">p, Holm-adjusted</th> : null}{analysis.test === "rank" ? null : <th className="num">Hedges' g</th>}</tr></thead>
            <tbody>
              {analysis.comparisons.map((c) => (
                <tr key={c.group}>
                  <td>{c.group}</td>
                  <td className="num">{c.difference === null ? "n/a" : `${studyFmt(c.difference)}${c.ci95 ? ` (${studyFmt(c.ci95[0])} to ${studyFmt(c.ci95[1])})` : ""}`}</td>
                  <td className="num">{c.statistic === null ? "n/a" : analysis.test === "rank" ? studyFmt(c.statistic) : `${studyFmt(c.statistic)} (${studyFmt(c.df)})`}</td>
                  <td className="num" style={{ whiteSpace: "nowrap" }}>{engine.formatP(c.p)}</td>
                  {analysis.comparisons.length > 1 ? <td className="num" style={{ whiteSpace: "nowrap" }}>{engine.formatP(c.pAdjusted)}</td> : null}
                  {analysis.test === "rank" ? null : <td className="num">{studyFmt(c.hedgesG)}</td>}
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      ) : null}
    </ToolPanel>
  );
}

function StudyJoinedTable({ engine, state, dataset }) {
  const table = React.useMemo(() => engine.joinedTable(state, dataset), [engine, state, dataset]);
  const [limit, setLimit] = React.useState(60);
  return (
    <ToolPanel title="Joined table" actions={<button type="button" className="btn btn-ghost btn-sm" style={{ minHeight: 36 }} onClick={() => toolDownload(`${toolSlug(state.study.name, "study")}-joined.csv`, engine.joinedCsv(state, dataset), "text/csv")}>CSV</button>}>
      <p className="tool-muted" style={{ margin: "0 0 10px", fontSize: 13 }}>One row per subject and timepoint. Excluded animals are greyed out.</p>
      <div className="table-scroll" style={{ maxHeight: 520, overflowY: "auto" }}>
        <table className="tool-table">
          <thead><tr><th>Subject</th><th>Group</th><th>Sex</th><th>Timepoint</th>{table.measures.map((m) => <th key={m.id} className="num" title={m.file}>{m.name}{m.unit ? ` (${m.unit})` : ""}</th>)}</tr></thead>
          <tbody>
            {table.rows.slice(0, limit).map((r, i) => (
              <tr key={`${r.subject}-${r.timepoint}-${i}`} style={r.excluded ? { opacity: 0.45 } : undefined}>
                <td>{r.subject}{r.excluded ? " (excluded)" : ""}</td><td>{r.group || ""}</td><td>{r.sex || ""}</td><td>{r.timepoint || ""}</td>
                {table.measures.map((m) => <td key={m.id} className="num">{r.values[m.id] === null ? "" : studyFmt(r.values[m.id])}</td>)}
              </tr>
            ))}
          </tbody>
        </table>
      </div>
      {table.rows.length > limit ? <button type="button" className="btn btn-ghost btn-tap" style={{ marginTop: 10 }} onClick={() => setLimit(limit + 200)}>Show more rows ({table.rows.length - limit} left)</button> : null}
    </ToolPanel>
  );
}

function StudyResultsStep({ engine, state, setState, dataset, results, checks, onNext }) {
  const [showOthers, setShowOthers] = React.useState(false);
  const linked = new Set(state.plan.outcomes.map((o) => o.measure).filter(Boolean));
  const planned = results.analyses.filter((a) => linked.has(a.measure));
  const others = results.analyses.filter((a) => !linked.has(a.measure));
  return (
    <div className="tool-stack">
      <StudyChecklist checks={checks} />
      <StudyOutcomes engine={engine} state={state} setState={setState} dataset={dataset} results={results} />
      {!results.analyses.length ? <ToolNotice tone="info" title="No results yet">Add a subject sheet and assay files, and match the samples.</ToolNotice> : null}
      {planned.map((a) => <StudyAnalysis key={`${a.measure}-${a.timepoint}`} engine={engine} analysis={a} />)}
      {others.length ? (
        <div>
          <button type="button" className="btn btn-ghost btn-tap" onClick={() => setShowOthers(!showOthers)}>
            {showOthers ? "Hide" : "Show"} {others.length} other measure{others.length === 1 ? "" : "s"} (exploratory)
          </button>
        </div>
      ) : null}
      {showOthers ? others.map((a) => <StudyAnalysis key={`${a.measure}-${a.timepoint}`} engine={engine} analysis={a} />) : null}
      {dataset.measures.length ? <StudyJoinedTable engine={engine} state={state} dataset={dataset} /> : null}
      {results.analyses.length ? <div><button type="button" className="btn btn-primary btn-tap" onClick={onNext}>Methods, exports, and saving</button></div> : null}
    </div>
  );
}

// ---------------------------------------------------------------- step 6: record

function StudyRecordStep({ engine, state, dataset, results, saved, setSaved, isExample }) {
  const [record, setRecord] = React.useState(null);
  const [replay, setReplay] = React.useState(null);
  const [fileCheck, setFileCheck] = React.useState(null);
  React.useEffect(() => {
    let live = true;
    engine.buildRecord(state, dataset, results).then((r) => { if (live) setRecord(r); });
    return () => { live = false; };
  }, [engine, state, dataset, results]);
  if (!record) return <p className="tool-muted">Building the record…</p>;
  const files = engine.exportFiles(state, dataset, results, record);
  const methods = files.find((f) => f.name === "methods.md").data;
  const base = toolSlug(state.study.name, "study-record");

  async function checkRecord(list) {
    try {
      const read = await toolReadFile(list[0]);
      const other = JSON.parse(read.text);
      const r = await engine.replayRecord(other);
      setReplay({ name: read.name, same: r.same, saved: r.saved, recomputed: r.recomputed, matchesThis: other.resultsDigest === record.resultsDigest });
    } catch (err) {
      setReplay({ name: list[0].name, error: err.message || String(err) });
    }
  }

  async function checkFile(list) {
    const out = [];
    for (const file of list) {
      const sha = file.size <= STUDY_HASH_LIMIT ? await window.CBHash.sha256Hex(new Uint8Array(await file.arrayBuffer())) : null;
      const known = [
        state.subjectsFile && { ...state.subjectsFile, role: "subject sheet" },
        ...state.assays.map((a) => ({ ...a.file, role: `${STUDY_KIND_LABEL[a.kind]} file` })),
        ...state.assays.filter((a) => a.data.mapFile).map((a) => ({ ...a.data.mapFile, role: "plate map" })),
        ...state.references.map((r) => ({ name: r.name, sha256: r.sha256, role: "reference file" })),
      ].filter(Boolean);
      const hit = sha ? known.find((k) => k.sha256 === sha) : null;
      out.push({ name: file.name, hit, sha });
    }
    setFileCheck(out);
  }

  return (
    <div className="tool-grid">
      <div className="tool-stack">
        <ToolPanel title="Methods and provenance" actions={<button type="button" className="btn btn-ghost btn-sm" style={{ minHeight: 36 }} onClick={() => toolDownload(`${base}-methods.md`, methods, "text/markdown")}>Download</button>}>
          {isExample ? <div style={{ marginBottom: 10 }}><ToolNotice tone="warn" title="Made-up example data">This record comes from the example files, not a real study.</ToolNotice></div> : null}
          <pre style={{ whiteSpace: "pre-wrap", margin: 0, fontSize: 13, lineHeight: 1.55, fontFamily: "inherit", overflowWrap: "anywhere" }}>{methods}</pre>
        </ToolPanel>
        <ToolPanel title="History">
          <ul style={{ margin: 0, paddingLeft: 18, fontSize: 13.5, display: "grid", gap: 4 }}>
            {state.history.map((h, i) => <li key={i}><span className="tool-muted">{toolFmtTime(h.at)}</span> · {h.event}{h.detail ? `: ${h.detail}` : ""}</li>)}
          </ul>
        </ToolPanel>
      </div>
      <div className="tool-stack">
        <ToolPanel title="Exports">
          <button type="button" className="btn btn-primary btn-tap" style={{ width: "100%" }} onClick={() => toolDownload(`${base}.zip`, new Blob([engine.zipFiles(files)], { type: "application/zip" }))}>
            Download everything (ZIP, {files.length} files)
          </button>
          <ul style={{ margin: "12px 0 0", padding: 0, listStyle: "none", display: "grid", gap: 2 }}>
            {files.map((f) => (
              <li key={f.name} style={{ display: "flex", gap: 8, alignItems: "center", fontSize: 13 }}>
                <span className="tool-mono" style={{ flex: 1 }}>{f.name}</span>
                <button type="button" className="btn btn-link" style={{ fontSize: 13, minHeight: 32 }}
                        onClick={() => toolDownload(f.name.split("/").pop(), f.data, f.name.endsWith(".svg") ? "image/svg+xml" : f.name.endsWith(".json") ? "application/json" : f.name.endsWith(".csv") ? "text/csv" : "text/markdown")}>Download</button>
              </li>
            ))}
          </ul>
          <p className="tool-muted" style={{ fontSize: 12, margin: "10px 0 0" }}>Results fingerprint <span className="tool-mono">{record.resultsDigest.slice(0, 16)}…</span></p>
        </ToolPanel>
        <ToolSaveToAccount module="study" defaultTitle={saved ? saved.title : state.study.name || "Study record"} savedId={saved ? saved.id : null}
                           summary={engine.recordSummary(state, results)} getPayload={async () => ({ record })}
                           onSaved={(item) => setSaved({ id: item.id, title: item.title, updatedAt: item.updatedAt, payloadSha256: item.payloadSha256 })} />
        <ToolPanel title="Replay a study record">
          <p style={{ margin: "0 0 10px", fontSize: 13.5, color: "var(--ink-700)", lineHeight: 1.5 }}>Load a study record (.json). The tool recomputes its results from the record and compares fingerprints.</p>
          <ToolDrop title="Load a study record (.json)" accept=".json,application/json" onFiles={checkRecord} />
          {replay ? (
            <div style={{ marginTop: 10 }}>
              {replay.error ? <ToolNotice tone="block" title="Could not replay">{replay.error}</ToolNotice>
                : replay.same ? <ToolNotice tone="ok" title="Same results, same fingerprint">{replay.name} recomputes to {replay.recomputed.slice(0, 12)}…{replay.matchesThis ? " and matches this study." : ", a different study from the one open here."}</ToolNotice>
                  : <ToolNotice tone="block" title="The results differ">{replay.name} says {replay.saved.slice(0, 12)}…, recomputed {replay.recomputed.slice(0, 12)}….</ToolNotice>}
            </div>
          ) : null}
        </ToolPanel>
        <ToolPanel title="Check an original file">
          <p style={{ margin: "0 0 10px", fontSize: 13.5, color: "var(--ink-700)", lineHeight: 1.5 }}>Drop the original files. Each one's SHA-256 is compared with the files this record names.</p>
          <ToolDrop title="Drop files to check" multiple onFiles={checkFile} />
          {fileCheck ? (
            <ul style={{ margin: "10px 0 0", paddingLeft: 18, fontSize: 13.5 }}>
              {fileCheck.map((f, i) => <li key={i}>{f.name}: {f.hit ? <span style={{ color: "var(--ok)" }}>matches the {f.hit.role} {f.hit.name}</span> : f.sha ? <span style={{ color: "var(--block)" }}>not in this record</span> : "too large to fingerprint here"}</li>)}
            </ul>
          ) : null}
        </ToolPanel>
        {isExample ? (
          <ToolPanel title="Example files">
            <p className="tool-muted" style={{ margin: "0 0 8px", fontSize: 13 }}>Made-up input files, to try the steps yourself.</p>
            <ul style={{ margin: 0, paddingLeft: 18, fontSize: 13.5 }}>
              {STUDY_EXAMPLE_FILES.map(([name, label]) => <li key={name}><a className="btn-link" href={`${STUDY_EXAMPLE_BASE}${name}`} download>{label}</a></li>)}
            </ul>
          </ToolPanel>
        ) : null}
      </div>
    </div>
  );
}

Object.assign(window, { StudyTool });
