// Source: index.html script block (extracted). Loaded via <script type="text/babel"> in index.html.
//
// Live-data version: reads /api/cohorts on mount and reflects real D1 counts.
// No seed data, no invented KPIs, no fake validator findings.

// ---- helpers -----------------------------------------------------------------

function initialsFromEmail(email) {
  if (!email) return "?";
  const local = String(email).split("@")[0] || "?";
  const parts = local.split(/[._\-+]/).filter(Boolean);
  if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase();
  return (parts[0] || local).slice(0, 2).toUpperCase();
}

// Simple stable hash → hue for deterministic avatar color per email.
function colorForEmail(email) {
  const s = String(email || "");
  let h = 0;
  for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) | 0;
  const hue = Math.abs(h) % 360;
  return `oklch(52% 0.1 ${hue})`;
}

function fmtDay(iso) {
  if (typeof iso !== "string") return "";
  return iso.slice(0, 10);
}

// Fill a 14-slot sparkline array aligned to the last 14 days (oldest first).
// `rows` is [{day_iso, count}, ...] ascending. Missing days → zero.
function buildSparkSeries(rows, days = 14) {
  const map = new Map();
  for (const r of (rows || [])) map.set(r.day_iso, r.count || 0);
  const out = [];
  const today = new Date();
  for (let i = days - 1; i >= 0; i--) {
    const d = new Date(today.getFullYear(), today.getMonth(), today.getDate() - i);
    const iso = d.toISOString().slice(0, 10);
    out.push({ day_iso: iso, count: map.get(iso) || 0 });
  }
  return out;
}

// ---- main page ---------------------------------------------------------------

function Cohorts({ user, onNewTask }) {
  const { t: tr } = useLang();
  const [cohorts, setCohorts] = React.useState([]);
  const [loading, setLoading] = React.useState(true);
  const [loadError, setLoadError] = React.useState(null);
  const [uploadOpen, setUploadOpen] = React.useState(false);
  const [emptyOpen, setEmptyOpen] = React.useState(false);
  const [selected, setSelected] = React.useState(null);
  const [sort, setSort] = React.useState("recent");
  const [notice, setNotice] = React.useState(null);

  const canEdit = user && user.kind === "manager";

  const reload = React.useCallback(async () => {
    setLoadError(null);
    try {
      if (!window.tasksApi || window.tasksApi.OFFLINE) {
        throw Object.assign(new Error("offline"), { offline: true });
      }
      if (window.tasksApi.invalidateCohorts) window.tasksApi.invalidateCohorts();
      const res = await window.tasksApi.listCohorts();
      const items = Array.isArray(res && res.items) ? res.items : [];
      setCohorts(items);
      setSelected(prev => prev || (items[0] && items[0].id) || null);
    } catch (err) {
      if (err && err.offline) setLoadError("Backend offline — cohorts require a live D1.");
      else setLoadError((err && err.message) || "Failed to load cohorts.");
      setCohorts([]);
    } finally {
      setLoading(false);
    }
  }, []);

  React.useEffect(() => { reload(); }, [reload]);

  const showNotice = (msg) => {
    setNotice(msg);
    window.setTimeout(() => setNotice(n => (n === msg ? null : n)), 3500);
  };

  const sorted = React.useMemo(() => {
    const arr = cohorts.slice();
    if (sort === "size") arr.sort((a, b) => (b.total || 0) - (a.total || 0));
    else if (sort === "progress") arr.sort((a, b) => ((b.approved || 0) / Math.max(1, b.total || 0)) - ((a.approved || 0) / Math.max(1, a.total || 0)));
    else arr.sort((a, b) => String(b.created_at || "").localeCompare(String(a.created_at || "")));
    return arr;
  }, [cohorts, sort]);

  const active = cohorts.find(c => c.id === selected) || null;

  const totalCohorts = cohorts.length;
  const totalTasks = cohorts.reduce((a, c) => a + (c.total || c.task_count || 0), 0);
  const totalNeedsReview = cohorts.reduce((a, c) => a + (c.needsReview || 0), 0);
  const totalApproved = cohorts.reduce((a, c) => a + (c.approved || 0), 0);

  const kpis = [
    { l: "Total cohorts", v: totalCohorts.toLocaleString() },
    { l: "Total tasks", v: totalTasks.toLocaleString() },
    { l: "Awaiting review", v: totalNeedsReview.toLocaleString() },
    { l: "Approved", v: totalApproved.toLocaleString() },
  ];

  const tableCols = "1.6fr 1.1fr 0.7fr 1.6fr 1fr";

  const handleUploadCommit = async (cohortMeta, tasks) => {
    try {
      await window.tasksApi.importCohort({ cohort: cohortMeta, tasks });
      await reload();
      setSelected(cohortMeta.id || null);
      setUploadOpen(false);
      showNotice(`Imported ${tasks.length.toLocaleString()} tasks into "${cohortMeta.name}".`);
    } catch (err) {
      const msg = (err && err.body && err.body.error) || (err && err.message) || "Import failed.";
      showNotice(`Import failed: ${msg}`);
    }
  };

  const handleEmptyCommit = async (payload) => {
    try {
      const res = await window.tasksApi.createCohort(payload);
      await reload();
      const newId = (res && res.item && res.item.id) || payload.id;
      if (newId) setSelected(newId);
      setEmptyOpen(false);
      showNotice(`Created empty cohort "${payload.name}".`);
    } catch (err) {
      const msg = (err && err.body && err.body.error) || (err && err.message) || "Create failed.";
      showNotice(`Create failed: ${msg}`);
    }
  };

  return (
    <div className="cohorts">
      <div className="cohorts-head">
        <div>
          <div style={{fontSize:11.5, color:"var(--ink-mute)", fontFamily:"var(--font-mono)", textTransform:"uppercase", letterSpacing:"0.06em"}}>Admin · manager</div>
          <h1>{tr("coh_title")}</h1>
          <p>{tr("coh_subtitle")}</p>
        </div>
        <div style={{marginLeft:"auto", display:"flex", gap:8, alignItems:"center"}}>
          <button className="btn" onClick={reload} disabled={loading}>{Ico.reset ? Ico.reset() : null} Refresh</button>
          {canEdit && <button className="btn" onClick={()=>setEmptyOpen(true)} title={tr("coh_new_empty_hint")}>{Ico.plus()} {tr("coh_new_empty")}</button>}
          {canEdit && <button className="btn primary" onClick={()=>setUploadOpen(true)}>{Ico.upload()} {tr("coh_upload")}</button>}
        </div>
      </div>

      {notice && (
        <div style={{margin:"0 0 14px", padding:"8px 12px", fontSize:12.5, border:"1px solid var(--line)", background:"var(--bg-sunken)", borderRadius:"var(--r-md)", color:"var(--ink-dim)"}}>
          {notice}
        </div>
      )}
      {loadError && (
        <div style={{margin:"0 0 14px", padding:"8px 12px", fontSize:12.5, border:"1px solid var(--line)", background:"var(--bg-sunken)", borderRadius:"var(--r-md)", color:"var(--red, #b94a48)"}}>
          {loadError}
        </div>
      )}

      <div className="cohorts-stats">
        {kpis.map(s => (
          <div key={s.l} className="kpi">
            <div className="kpi-l">{s.l}</div>
            <div className="kpi-v">{s.v}</div>
          </div>
        ))}
      </div>

      <div className="cohorts-body">
        <div className="cohorts-table">
          <div className="cohorts-table-head">
            <h3>Cohorts</h3>
            <div style={{marginLeft:"auto", display:"flex", gap:6}}>
              {[["recent","Recent"],["size","Size"],["progress","Progress"]].map(([k,l])=>(
                <button key={k} className="filter-pill" data-active={sort===k} onClick={()=>setSort(k)}>{l}</button>
              ))}
            </div>
          </div>
          <div className="cohorts-table-grid cohorts-thr" style={{gridTemplateColumns: tableCols}}>
            <span>Name</span><span>Source</span><span>Tasks</span><span>Progress</span><span>Created</span>
          </div>
          {loading && (
            <div style={{padding:"16px 14px", color:"var(--ink-mute)", fontSize:12.5}}>Loading cohorts…</div>
          )}
          {!loading && sorted.length === 0 && (
            <div style={{padding:"20px 14px", color:"var(--ink-mute)", fontSize:12.5}}>
              No cohorts yet. {canEdit ? "Upload a tau2 task file or create an empty cohort to get started." : "Ask a manager to seed a cohort."}
            </div>
          )}
          {sorted.map(c => {
            const total = c.total || c.task_count || 0;
            const approved = c.approved || 0;
            const inProgress = c.inProgress || 0;
            const needsReview = c.needsReview || 0;
            const rejected = c.rejected || 0;
            const pct = (n) => (total > 0 ? (n / total * 100) : 0);
            return (
              <div key={c.id} className="cohorts-table-grid cohorts-row"
                data-active={selected===c.id}
                onClick={()=>setSelected(c.id)}
                style={{gridTemplateColumns: tableCols}}>
                <div>
                  <div style={{fontWeight:500}}>{c.name}</div>
                  <code style={{fontFamily:"var(--font-mono)", fontSize:10.5, color:"var(--ink-mute)"}}>{c.id}</code>
                </div>
                <div>
                  {c.source ? <span className="tag">{c.source}</span> : <span style={{color:"var(--ink-mute)", fontSize:11}}>—</span>}
                  {c.generator && <div style={{fontSize:10.5, color:"var(--ink-mute)", fontFamily:"var(--font-mono)", marginTop:2}}>{c.generator}</div>}
                </div>
                <div style={{fontFamily:"var(--font-mono)"}}>{total.toLocaleString()}</div>
                <div>
                  <div className="progress">
                    <span className="progress-approved" style={{width: pct(approved) + "%"}}/>
                    <span className="progress-inprog" style={{width: pct(inProgress) + "%"}}/>
                    <span className="progress-rejected" style={{width: pct(rejected) + "%"}}/>
                  </div>
                  <div style={{fontSize:10.5, color:"var(--ink-mute)", fontFamily:"var(--font-mono)", marginTop:3}}>
                    {approved} approved · {inProgress} in-prog · {needsReview} needs-review · {rejected} rejected
                  </div>
                </div>
                <div style={{fontSize:11.5, color:"var(--ink-dim)"}}>
                  {fmtDay(c.created_at) || "—"}
                  {c.created_by && <div style={{fontSize:10.5, color:"var(--ink-mute)", fontFamily:"var(--font-mono)"}}>by {c.created_by}</div>}
                </div>
              </div>
            );
          })}
        </div>

        {active && <CohortHealth c={active} user={user} canEdit={canEdit} onNewTask={onNewTask}
                     onDeleted={() => { setSelected(null); reload(); }}
                     onAssigned={() => reload()}
                     onNotice={showNotice}/>}
      </div>

      {uploadOpen && <UploadModal onClose={()=>setUploadOpen(false)} onCommit={handleUploadCommit} user={user}/>}
      {emptyOpen && <EmptyCohortModal onClose={()=>setEmptyOpen(false)} onCommit={handleEmptyCommit} user={user}/>}
    </div>
  );
}

// ---- empty cohort modal ------------------------------------------------------

function EmptyCohortModal({ onClose, onCommit, user }) {
  const { t: tr } = useLang();
  const [name, setName] = React.useState("");
  const [source, setSource] = React.useState("Hand-authored");
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState(null);

  const submit = async () => {
    const trimmed = name.trim();
    if (!trimmed || busy) return;
    setBusy(true);
    setErr(null);
    const id = "coh_" + Date.now().toString(36);
    try {
      await onCommit({
        id,
        name: trimmed,
        source,
        generator: "hand · empty",
      });
    } catch (e) {
      setErr((e && e.message) || "Create failed.");
    } finally {
      setBusy(false);
    }
  };
  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal" onClick={e=>e.stopPropagation()} style={{maxWidth:480}}>
        <div className="modal-head">
          <h3>{tr("coh_new_empty")}</h3>
          <button className="btn ghost" onClick={onClose}>{Ico.x ? Ico.x() : null}</button>
        </div>
        <div style={{padding:"18px 22px 22px", display:"grid", gap:14}}>
          <p style={{margin:0, color:"var(--ink-dim)", fontSize:13}}>{tr("coh_new_empty_hint")}</p>
          {err && <div className="tag red" style={{alignSelf:"flex-start"}}>{err}</div>}
          <div className="form-field">
            <label>{tr("coh_new_empty_name")}</label>
            <input value={name} onChange={e=>setName(e.target.value)} autoFocus placeholder="e.g. Cardiology pilot — 2025 Q2"/>
          </div>
          <div className="form-field">
            <label>{tr("coh_new_empty_src")}</label>
            <select value={source} onChange={e=>setSource(e.target.value)}>
              <option>Hand-authored</option>
              <option>PrimeKG</option>
              <option>MedDialog</option>
              <option>MIMIC-IV</option>
              <option>FHIR Bundle</option>
            </select>
          </div>
          <div style={{display:"flex", gap:8, justifyContent:"flex-end", marginTop:6}}>
            <button className="btn ghost" onClick={onClose} disabled={busy}>{tr("sim_reset")}</button>
            <button className="btn primary" disabled={!name.trim() || busy} onClick={submit}>
              {Ico.plus()} {busy ? "Creating…" : tr("coh_new_empty_create")}
            </button>
          </div>
        </div>
      </div>
    </div>
  );
}

// ---- health panel ------------------------------------------------------------

function CohortHealth({ c, user, canEdit, onNewTask, onDeleted, onNotice, onAssigned }) {
  const { t: tr } = useLang();
  const [detail, setDetail] = React.useState(null);
  const [loadingDetail, setLoadingDetail] = React.useState(false);
  const [exporting, setExporting] = React.useState(false);
  const [deleting, setDeleting] = React.useState(false);
  const [assignOpen, setAssignOpen] = React.useState(false);

  const loadDetail = React.useCallback(async () => {
    try {
      const res = await window.tasksApi.getCohort(c.id);
      return res || null;
    } catch (err) {
      if (onNotice) onNotice(`Failed to load cohort detail: ${(err && err.message) || err}`);
      return null;
    }
  }, [c.id, onNotice]);

  React.useEffect(() => {
    let cancelled = false;
    setLoadingDetail(true);
    setDetail(null);
    (async () => {
      const res = await loadDetail();
      if (!cancelled) {
        setDetail(res);
        setLoadingDetail(false);
      }
    })();
    return () => { cancelled = true; };
  }, [c.id, loadDetail]);

  const total = (detail && detail.total) || c.total || c.task_count || 0;
  const approved = (detail && detail.approved) || c.approved || 0;
  const rejected = (detail && detail.rejected) || c.rejected || 0;
  const assignees = (detail && detail.assignees) || c.assignees || [];
  const spark = buildSparkSeries(detail && detail.recent_approvals, 14);
  const sparkMax = Math.max(1, ...spark.map(s => s.count));

  const approvedLast7 = spark.slice(-7).reduce((a, s) => a + s.count, 0);
  const throughputPerDay = approvedLast7 / 7;
  const rejectionBase = approved + rejected;
  const rejectionRate = rejectionBase > 0 ? (rejected / rejectionBase) * 100 : null;

  const exportApproved = async () => {
    if (exporting) return;
    setExporting(true);
    try {
      const collected = [];
      let cursor = null;
      // Page through approved tasks for this cohort. 1000 per page is enough
      // for day-to-day cohorts; big exports still complete in a few round-trips.
      do {
        const res = await window.tasksApi.list({ cohort: c.id, status: "approved", limit: 1000, cursor });
        const items = Array.isArray(res && res.items) ? res.items : [];
        collected.push(...items);
        cursor = res && res.next_cursor;
      } while (cursor);
      if (!collected.length) {
        if (onNotice) onNotice("No approved tasks to export.");
        return;
      }
      window.exportTasksJson(collected, `${c.id}.json`);
    } catch (err) {
      if (onNotice) onNotice(`Export failed: ${(err && err.body && err.body.error) || (err && err.message) || err}`);
    } finally {
      setExporting(false);
    }
  };

  const remove = async () => {
    const ok = window.confirm(
      `Delete cohort "${c.name}"? Its ${total.toLocaleString()} task(s) will be kept but un-grouped (cohort_id cleared). This cannot be undone.`,
    );
    if (!ok || deleting) return;
    setDeleting(true);
    try {
      await window.tasksApi.deleteCohort(c.id);
      if (onNotice) onNotice(`Deleted cohort "${c.name}".`);
      if (onDeleted) onDeleted();
    } catch (err) {
      if (onNotice) onNotice(`Delete failed: ${(err && err.body && err.body.error) || (err && err.message) || err}`);
    } finally {
      setDeleting(false);
    }
  };

  return (
    <div className="cohorts-health">
      <div className="section-head" style={{borderBottom:"1px solid var(--line)", padding:"10px 14px"}}>
        <h3>Health · {c.name}</h3>
        <div style={{marginLeft:"auto", display:"flex", alignItems:"center", gap:10}}>
          <span className="section-hint">rolling 14 days</span>
          {canEdit && onNewTask && <button className="btn" style={{padding:"3px 8px", fontSize:11.5}} onClick={onNewTask}>{Ico.plus()} Add task</button>}
          {canEdit && (
            <button
              className="btn"
              style={{padding:"3px 8px", fontSize:11.5}}
              onClick={()=>setAssignOpen(true)}
              disabled={total === 0}
              title="Bulk-assign tasks in this cohort"
            >{Ico.tool ? Ico.tool() : null} Assign cohort</button>
          )}
          <button className="btn" style={{padding:"3px 8px", fontSize:11.5}} onClick={exportApproved} disabled={exporting} title={tr("export_approved_hint")}>
            {Ico.download()} {exporting ? "Exporting…" : tr("export_approved")}
          </button>
          {canEdit && (
            <button className="btn ghost" style={{padding:"3px 8px", fontSize:11.5}} onClick={remove} disabled={deleting}>
              {deleting ? "Deleting…" : "Delete"}
            </button>
          )}
        </div>
      </div>
      <div style={{padding:"14px"}}>
        <div style={{display:"grid", gridTemplateColumns:"1fr 1fr", gap:14, marginBottom:16}}>
          <div className="mini-stat">
            <div className="mini-l">Throughput</div>
            <div className="mini-v">{throughputPerDay.toFixed(1)}/day</div>
            <div className="mini-s">approved, 7-day avg</div>
          </div>
          <div className="mini-stat">
            <div className="mini-l">Rejection rate</div>
            <div className="mini-v">{rejectionRate == null ? "—" : rejectionRate.toFixed(1) + "%"}</div>
            <div className="mini-s">{rejected} rejected / {rejectionBase} reviewed</div>
          </div>
        </div>
        <div style={{fontSize:11.5, color:"var(--ink-mute)", fontFamily:"var(--font-mono)", textTransform:"uppercase", letterSpacing:"0.05em", marginBottom:8}}>Daily approvals</div>
        <div className="spark">
          {spark.map((s, i)=>(
            <span key={s.day_iso + "_" + i}
              style={{height: (s.count / sparkMax * 100) + "%"}}
              title={`${s.day_iso} · ${s.count} approved`}/>
          ))}
        </div>
        <div style={{display:"flex", justifyContent:"space-between", fontSize:10.5, color:"var(--ink-mute)", fontFamily:"var(--font-mono)", marginTop:4}}>
          <span>−14d</span><span>today</span>
        </div>

        <div style={{fontSize:11.5, color:"var(--ink-mute)", fontFamily:"var(--font-mono)", textTransform:"uppercase", letterSpacing:"0.05em", margin:"18px 0 8px"}}>Assigned clinicians</div>
        {loadingDetail && assignees.length === 0 && (
          <div style={{padding:"8px 0", color:"var(--ink-mute)", fontSize:12}}>Loading…</div>
        )}
        {!loadingDetail && assignees.length === 0 && (
          <div style={{padding:"8px 0", color:"var(--ink-mute)", fontSize:12}}>No assignments yet.</div>
        )}
        {assignees.map(a => {
          const initials = a.name
            ? a.name.split(/\s+/).filter(Boolean).map(p => p[0]).slice(0,2).join("").toUpperCase()
            : initialsFromEmail(a.email);
          return (
            <div key={a.email} className="assignee-row">
              {a.avatar_url
                ? <img src={a.avatar_url} alt="" className="avatar" style={{width:22, height:22}}/>
                : <div className="avatar" style={{width:22, height:22, fontSize:9.5, background: colorForEmail(a.email)}}>{initials}</div>
              }
              <span style={{flex:1, fontSize:12}}>
                {a.name || a.email}
                {a.name && <span style={{color:"var(--ink-mute)", fontSize:10.5, fontFamily:"var(--font-mono)", marginLeft:6}}>{a.email}</span>}
              </span>
              <span style={{fontFamily:"var(--font-mono)", fontSize:11, color:"var(--ink-mute)"}}>{a.done_count}/{a.assigned_count}</span>
            </div>
          );
        })}
      </div>
      {assignOpen && (
        <AssignCohortModal
          cohort={c}
          detail={detail}
          onClose={()=>setAssignOpen(false)}
          onDone={async ()=>{
            const fresh = await loadDetail();
            if (fresh) setDetail(fresh);
            if (onAssigned) onAssigned();
          }}
          onNotice={onNotice}
        />
      )}
    </div>
  );
}

// ---- upload modal ------------------------------------------------------------

function UploadModal({ onClose, onCommit, user }) {
  const [step, setStep] = React.useState(1); // 1 upload, 2 preview, 3 metadata
  const [file, setFile] = React.useState(null);
  const [parsed, setParsed] = React.useState(null);
  const [name, setName] = React.useState("");
  const [source, setSource] = React.useState("PrimeKG");
  const [generator, setGenerator] = React.useState("uploaded · tau2 v2.0");
  const [dragOver, setDragOver] = React.useState(false);
  const [busy, setBusy] = React.useState(false);
  const [commitErr, setCommitErr] = React.useState(null);

  const readAndParse = async (f) => {
    setFile(f);
    let text = "";
    try { text = await f.text(); } catch { return; }
    let tasks = [];
    let format = "";
    const trimmed = text.trim();
    try {
      if (trimmed.startsWith("[")) {
        tasks = JSON.parse(trimmed); format = "JSON array";
      } else if (trimmed.startsWith("{")) {
        const obj = JSON.parse(trimmed);
        if (Array.isArray(obj.tasks)) { tasks = obj.tasks; format = "JSON object (.tasks)"; }
        else { tasks = [obj]; format = "Single JSON object"; }
      } else {
        tasks = trimmed.split(/\r?\n/).filter(Boolean).map(l => JSON.parse(l));
        format = "JSONL (newline-delimited)";
      }
    } catch (e) {
      setParsed({ error: e.message, raw: text.slice(0,400) });
      setStep(2);
      return;
    }

    const required = ["id","description","user_scenario","evaluation_criteria"];
    const issues = [];
    const domains = {}; const severities = {}; const withDx = [];
    for (let i=0; i<Math.min(tasks.length,200); i++) {
      const t = tasks[i];
      const missing = required.filter(k => !(k in t));
      if (missing.length) issues.push({ i, kind:"err", msg:`missing: ${missing.join(", ")}` });
      const d = t?.user_scenario?.instructions?.domain || t?.domain || "Unknown";
      domains[d] = (domains[d]||0)+1;
      const s = t?.user_scenario?.instructions?.severity || "—";
      severities[s] = (severities[s]||0)+1;
      if (t?.user_scenario?.instructions?.suspected_dx || t?.diagnosis) withDx.push(i);
      const p = t?.user_scenario?.persona || "";
      const g = t?.medical_persona?.gender;
      if (g && typeof p === "string" && p.toLowerCase().includes(g === "male" ? "female" : "male")) {
        issues.push({ i, kind:"warn", msg:`persona/gender mismatch at task[${i}]` });
      }
    }
    setParsed({ tasks, format, total: tasks.length, issues, domains, severities, withDx: withDx.length, sample: tasks[0] });
    setName(f.name.replace(/\.(jsonl?|JSONL?)$/,"") + " · " + new Date().toISOString().slice(0,10));
    setStep(2);
  };

  const onDrop = (e) => {
    e.preventDefault(); setDragOver(false);
    const f = e.dataTransfer.files?.[0];
    if (f) readAndParse(f);
  };

  const commit = async () => {
    if (!parsed || !parsed.tasks || busy) return;
    setBusy(true);
    setCommitErr(null);
    const id = "coh_" + Date.now().toString(36);
    try {
      await onCommit(
        { id, name, source, generator },
        parsed.tasks,
      );
    } catch (e) {
      setCommitErr((e && e.message) || "Commit failed.");
    } finally {
      setBusy(false);
    }
  };

  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal" onClick={e=>e.stopPropagation()}>
        <div className="modal-head">
          <h3>New cohort{parsed && !parsed.error ? " · "+parsed.total+" tasks" : ""}</h3>
          <div className="modal-stepper">
            {["Upload","Preview & validate","Metadata & assign"].map((s,i)=>(
              <span key={s} className="modal-step" data-state={step>i+1?"done":step===i+1?"active":"pending"}>
                <span className="modal-step-n">{i+1}</span>{s}
              </span>
            ))}
          </div>
          <button className="btn ghost" onClick={onClose}>{Ico.x()}</button>
        </div>
        <div className="modal-body">
          {step === 1 && (
            <div style={{padding:"20px 22px"}}>
              <label
                className="drop-zone" data-drag={dragOver}
                onDragOver={e=>{e.preventDefault(); setDragOver(true);}}
                onDragLeave={()=>setDragOver(false)}
                onDrop={onDrop}
              >
                <input type="file" accept=".json,.jsonl,application/json" style={{display:"none"}} onChange={e=>e.target.files[0] && readAndParse(e.target.files[0])}/>
                <div style={{fontSize:24, fontFamily:"var(--font-serif)", color:"var(--ink)"}}>Drop a task file to begin</div>
                <div style={{color:"var(--ink-mute)", fontSize:13, margin:"6px 0 14px"}}>.json or .jsonl · tau2 schema · up to 500MB</div>
                <span className="btn primary">{Ico.upload()} Choose file</span>
                <div style={{fontSize:11.5, color:"var(--ink-mute)", fontFamily:"var(--font-mono)", marginTop:14}}>
                  expected path: <code>data/tau2/domains/clinical/primekg/all_generated_tasks.json</code>
                </div>
              </label>
              <div style={{marginTop:18, display:"grid", gridTemplateColumns:"1fr 1fr 1fr", gap:10}}>
                <div className="upload-tip"><b>Array or JSONL</b><br/><span>[task, task, …] or one JSON task per line</span></div>
                <div className="upload-tip"><b>Schema-checked</b><br/><span>Required: id, description, user_scenario, evaluation_criteria</span></div>
                <div className="upload-tip"><b>Dry-run first</b><br/><span>You'll see a validation report before commit</span></div>
              </div>
            </div>
          )}

          {step === 2 && parsed && !parsed.error && (
            <div className="preview">
              <div className="preview-stats">
                <div className="kpi"><div className="kpi-l">Format</div><div className="kpi-v" style={{fontSize:15}}>{parsed.format}</div></div>
                <div className="kpi"><div className="kpi-l">Tasks found</div><div className="kpi-v">{parsed.total.toLocaleString()}</div></div>
                <div className="kpi"><div className="kpi-l">Errors</div><div className="kpi-v" style={{color: parsed.issues.filter(i=>i.kind==='err').length?'var(--red)':'var(--green)'}}>{parsed.issues.filter(i=>i.kind==='err').length}</div></div>
                <div className="kpi"><div className="kpi-l">Warnings</div><div className="kpi-v" style={{color:'var(--amber)'}}>{parsed.issues.filter(i=>i.kind==='warn').length}</div></div>
                <div className="kpi"><div className="kpi-l">With suspected dx</div><div className="kpi-v">{parsed.withDx}</div></div>
              </div>
              <div style={{display:"grid", gridTemplateColumns:"1fr 1fr", gap:14}}>
                <div>
                  <div className="preview-section-label">By domain</div>
                  <div className="distro">
                    {Object.entries(parsed.domains).slice(0,8).map(([d,n])=>{
                      const pct = n / Math.max(...Object.values(parsed.domains)) * 100;
                      return (<div key={d} className="distro-row"><span>{d}</span><div className="distro-bar"><span style={{width:pct+"%"}}/></div><span style={{fontFamily:"var(--font-mono)"}}>{n}</span></div>);
                    })}
                  </div>
                </div>
                <div>
                  <div className="preview-section-label">By severity</div>
                  <div className="distro">
                    {Object.entries(parsed.severities).map(([d,n])=>{
                      const pct = n / Math.max(...Object.values(parsed.severities)) * 100;
                      return (<div key={d} className="distro-row"><span>{d}</span><div className="distro-bar"><span style={{width:pct+"%"}}/></div><span style={{fontFamily:"var(--font-mono)"}}>{n}</span></div>);
                    })}
                  </div>
                </div>
              </div>
              <div>
                <div className="preview-section-label">Validator findings ({parsed.issues.length})</div>
                {parsed.issues.length === 0 ? (
                  <div style={{padding:14, border:"1px solid var(--line)", borderRadius:6, color:"var(--green)", fontSize:12.5}}>
                    {Ico.check()} No issues detected — all tasks passed schema + heuristic checks
                  </div>
                ) : (
                  <div style={{border:"1px solid var(--line)", borderRadius:6, maxHeight:140, overflow:"auto"}}>
                    {parsed.issues.slice(0,20).map((iss,i)=>(
                      <div key={i} className="issue-row">
                        <span className={"tag "+(iss.kind==='err'?'red':'amber')}>{iss.kind}</span>
                        <code>task[{iss.i}]</code>
                        <span>{iss.msg}</span>
                      </div>
                    ))}
                  </div>
                )}
              </div>
              <div>
                <div className="preview-section-label">Sample · task[0]</div>
                <pre className="sample-json">
{JSON.stringify(parsed.sample, null, 2).slice(0, 1800)}{JSON.stringify(parsed.sample, null, 2).length > 1800 ? "\n  …" : ""}
                </pre>
              </div>
            </div>
          )}

          {step === 2 && parsed?.error && (
            <div style={{padding:24}}>
              <div className="tag red" style={{fontSize:13, padding:"6px 12px"}}>Parse error</div>
              <pre style={{marginTop:10, background:"var(--bg-sunken)", padding:12, borderRadius:6, fontSize:12, color:"var(--red)"}}>{parsed.error}</pre>
              <pre style={{marginTop:10, background:"var(--bg-sunken)", padding:12, borderRadius:6, fontSize:11, color:"var(--ink-dim)"}}>{parsed.raw}</pre>
            </div>
          )}

          {step === 3 && parsed && (
            <div style={{padding:"20px 22px", display:"grid", gridTemplateColumns:"1fr 1fr", gap:16}}>
              <div className="form-field"><label>Cohort name</label><input value={name} onChange={e=>setName(e.target.value)}/></div>
              <div className="form-field"><label>Source</label>
                <select className="inp" value={source} onChange={e=>setSource(e.target.value)}>
                  <option>PrimeKG</option><option>MedDialog</option><option>Hand-authored</option><option>Other KG</option>
                </select>
              </div>
              <div className="form-field" style={{gridColumn:"1 / -1"}}>
                <label>Generator version</label>
                <input value={generator} onChange={e=>setGenerator(e.target.value)}/>
              </div>
              <div className="form-field" style={{gridColumn:"1 / -1", color:"var(--ink-dim)", fontSize:12.5}}>
                Tasks will be uploaded with <code>status = "needs-review"</code> and <code>assignee_email = null</code>.
                Assign annotators from the Users page after commit.
              </div>
              {commitErr && (
                <div className="tag red" style={{gridColumn:"1 / -1"}}>{commitErr}</div>
              )}
            </div>
          )}
        </div>
        <div className="modal-foot">
          {step > 1 && <button className="btn ghost" onClick={()=>setStep(s=>s-1)} disabled={busy}>← Back</button>}
          <div style={{marginLeft:"auto", display:"flex", gap:8}}>
            <button className="btn ghost" onClick={onClose} disabled={busy}>Cancel</button>
            {step === 2 && !parsed?.error && <button className="btn primary" onClick={()=>setStep(3)}>Continue → Metadata</button>}
            {step === 3 && <button className="btn accent" onClick={commit} disabled={busy || !name.trim()}>
              {Ico.check()} {busy ? "Uploading…" : `Commit ${parsed?.total.toLocaleString()} tasks to cohort`}
            </button>}
          </div>
        </div>
      </div>
    </div>
  );
}

window.Cohorts = Cohorts;
window.UploadModal = UploadModal;
window.EmptyCohortModal = EmptyCohortModal;
