// Source: index.html script block (extracted). Loaded via <script type="text/babel"> in index.html.
//
// Shared assignment UI — used by editor.jsx (single-task dropdown in sidebar),
// queue.jsx (bulk-assign bar), and cohorts.jsx (cohort-level modal).
//
// <AssignDropdown {onPick, onClose, allowClear, allowRoundRobin}>
//   A floating list of users that fetches /api/users on mount. Caller wraps
//   it in a `position: relative` anchor and manages open/close state. The
//   dropdown handles its own click-outside + Escape via an internal ref.
//   `allowRoundRobin` adds a footer option that invokes onPick({roundRobin:true}).
//
// <AssignCohortModal {cohort, detail, onClose, onDone, onNotice}>
//   Full modal used from the Cohorts page: scope radio (all/needs-review/
//   unassigned), mode radio (single/round_robin), user picker(s), live
//   estimate, submit + busy state. Delegates the write to
//   window.tasksApi.assignBulk, falling back to a client-chunked path when
//   the cohort exceeds the server cap (50k).
//
// Chunking helpers (also on window for queue.jsx to reuse):
//   window.ASSIGN_CAP         — server-side cap per filter/task_ids call
//   window.ASSIGN_CHUNK_SIZE  — ids per assignBulk request during chunking
//   window.fetchAssignableIds — paginates /api/tasks to collect ids
//   window.assignIdsChunked   — submits assignBulk in 5k chunks, tracks progress

const ASSIGN_CAP = 50000;
const ASSIGN_CHUNK_SIZE = 5000;

// Fetches all task ids matching a cohort + scope via /api/tasks pagination.
// The /api/tasks endpoint caps limit at 500 and does not accept an
// `assignee_email` filter, so "unassigned" / "specific assignee" scopes are
// resolved client-side after fetching each page.
async function fetchAssignableIds(opts) {
  opts = opts || {};
  const { cohort, status, unassignedOnly, specificAssignee, onProgress } = opts;
  const ids = [];
  let cursor = null;
  let pages = 0;
  while (true) {
    const res = await window.tasksApi.list({
      cohort: cohort || undefined,
      status: status || undefined,
      limit: 500,
      cursor: cursor || undefined,
    });
    const items = (res && res.items) || [];
    for (const t of items) {
      if (unassignedOnly && t.assigneeEmail) continue;
      if (specificAssignee && t.assigneeEmail !== specificAssignee) continue;
      ids.push(t.id);
    }
    pages++;
    if (onProgress) onProgress({ fetched: ids.length, pages });
    if (!res || !res.next_cursor) break;
    cursor = res.next_cursor;
  }
  return ids;
}

// Submits assignBulk in ASSIGN_CHUNK_SIZE task_ids batches. For round-robin
// with multiple assignees, pre-partitions by `i % assignees.length` and sends
// per-email assign_all batches (avoids server having to rotate across a
// chunk boundary, and lets us report accurate by_assignee counts).
async function assignIdsChunked(opts) {
  opts = opts || {};
  const { ids, strategy, email, assignees, onProgress } = opts;
  const CHUNK = ASSIGN_CHUNK_SIZE;
  const byAssignee = {};
  let updated = 0;

  let chunks;
  if (strategy === "round_robin") {
    const buckets = new Map();
    ids.forEach((id, i) => {
      const e = assignees[i % assignees.length];
      if (!buckets.has(e)) buckets.set(e, []);
      buckets.get(e).push(id);
    });
    chunks = [];
    for (const [e, arr] of buckets) {
      for (let i = 0; i < arr.length; i += CHUNK) {
        chunks.push({ email: e, task_ids: arr.slice(i, i + CHUNK) });
      }
    }
  } else {
    chunks = [];
    for (let i = 0; i < ids.length; i += CHUNK) {
      chunks.push({ email: email == null ? null : email, task_ids: ids.slice(i, i + CHUNK) });
    }
  }

  for (let i = 0; i < chunks.length; i++) {
    if (onProgress) onProgress({ done: i, total: chunks.length });
    const res = await window.tasksApi.assignBulk({
      task_ids: chunks[i].task_ids,
      email: chunks[i].email,
    });
    updated += (res && res.updated) || 0;
    const key = chunks[i].email == null ? "(unassigned)" : chunks[i].email;
    byAssignee[key] = (byAssignee[key] || 0) + chunks[i].task_ids.length;
  }
  if (onProgress) onProgress({ done: chunks.length, total: chunks.length });
  return { updated, by_assignee: byAssignee };
}

function AssignDropdown({ onPick, onClose, allowClear, allowRoundRobin, anchorRef }) {
  const ref = React.useRef(null);
  const [users, setUsers] = React.useState([]);
  const [err, setErr] = React.useState(null);
  const [loaded, setLoaded] = React.useState(false);
  // When mounted inside a scrollable container (e.g. .editor-side with overflow:auto),
  // absolute positioning clips the dropdown. We render with position:fixed and pin
  // to the trigger button's bounding rect instead.
  const [anchorRect, setAnchorRect] = React.useState(null);
  React.useLayoutEffect(() => {
    const computeRect = () => {
      const el = anchorRef?.current
        || (ref.current ? ref.current.parentElement?.querySelector("button") : null)
        || (ref.current ? ref.current.parentElement : null);
      if (el && el.getBoundingClientRect) setAnchorRect(el.getBoundingClientRect());
    };
    computeRect();
    window.addEventListener("resize", computeRect);
    window.addEventListener("scroll", computeRect, true);
    return () => {
      window.removeEventListener("resize", computeRect);
      window.removeEventListener("scroll", computeRect, true);
    };
  }, [anchorRef]);

  React.useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        const res = await window.tasksApi?.listUsers?.();
        const list = Array.isArray(res) ? res : (Array.isArray(res?.items) ? res.items : []);
        if (!cancelled) { setUsers(list); setErr(null); setLoaded(true); }
      } catch (e) {
        if (!cancelled) { setErr(e?.message || String(e)); setLoaded(true); }
      }
    })();
    const onDocMouseDown = (ev) => {
      if (ref.current && !ref.current.contains(ev.target)) onClose && onClose();
    };
    const onKey = (ev) => { if (ev.key === "Escape") onClose && onClose(); };
    document.addEventListener("mousedown", onDocMouseDown);
    document.addEventListener("keydown", onKey);
    return () => {
      cancelled = true;
      document.removeEventListener("mousedown", onDocMouseDown);
      document.removeEventListener("keydown", onKey);
    };
  }, [onClose]);

  const pos = anchorRect
    ? {
        position: "fixed",
        top: Math.min(anchorRect.bottom + 4, window.innerHeight - 320),
        left: Math.max(8, Math.min(anchorRect.right - 260, window.innerWidth - 268)),
      }
    : { position: "absolute", right: 0, top: "calc(100% + 4px)" };

  return (
    <div
      ref={ref}
      className="assign-dropdown"
      style={{
        ...pos,
        background: "var(--bg)",
        border: "1px solid var(--line)",
        minWidth: 260,
        maxHeight: 300,
        overflowY: "auto",
        zIndex: 100,
        padding: 4,
        boxShadow: "0 4px 12px rgba(0,0,0,0.08)",
      }}
    >
      {err && (
        <div style={{color:"var(--red)", fontSize:11, padding:"4px 6px"}}>{err}</div>
      )}
      {!loaded && !err && (
        <div style={{fontSize:11.5, color:"var(--ink-mute)", padding:"6px", fontStyle:"italic"}}>
          loading…
        </div>
      )}
      {loaded && !err && users.length === 0 && (
        <div style={{fontSize:11.5, color:"var(--ink-mute)", padding:"6px", fontStyle:"italic"}}>
          no users
        </div>
      )}
      {users.map(u => (
        <button
          key={u.email}
          className="btn ghost"
          style={{display:"block", width:"100%", textAlign:"left", padding:"4px 8px", fontSize:12}}
          onClick={()=>onPick && onPick(u.email)}
          title={u.name ? `${u.name} (${u.email})` : u.email}
        >
          {u.name ? (
            <>
              <span>{u.name}</span>{" "}
              <span style={{color:"var(--ink-mute)", fontFamily:"var(--font-mono)", fontSize:10.5}}>{u.email}</span>
            </>
          ) : u.email}
        </button>
      ))}
      {(allowClear || allowRoundRobin) && (
        <div style={{borderTop:"1px solid var(--line)", marginTop:4, paddingTop:4}}>
          {allowClear && (
            <button
              className="btn ghost"
              style={{display:"block", width:"100%", textAlign:"left", padding:"4px 8px", fontSize:12, color:"var(--ink-mute)"}}
              onClick={()=>onPick && onPick(null)}
            >Unassign</button>
          )}
          {allowRoundRobin && (
            <button
              className="btn ghost"
              style={{display:"block", width:"100%", textAlign:"left", padding:"4px 8px", fontSize:12, color:"var(--ink-mute)"}}
              onClick={()=>onPick && onPick({ roundRobin: true })}
            >Round-robin across team…</button>
          )}
        </div>
      )}
    </div>
  );
}

// ---------------------------------------------------------------------------
// <AssignCohortModal/> — manager-only bulk assign from the Cohorts page.
// Takes the cohort row (c) and its detail (for status counts); falls back to
// the row's totals when detail isn't loaded yet.
// ---------------------------------------------------------------------------

function AssignCohortModal({ cohort, detail, onClose, onDone, onNotice }) {
  const c = cohort || {};
  const d = detail || {};
  const total = d.task_count || d.total || c.total || c.task_count || 0;
  const needsReview = d.needsReview || c.needsReview || 0;
  // The cohort detail response caps assignees at the top 8, so summing their
  // assigned_count is an *upper bound* estimate of already-assigned tasks.
  // That makes unassigned a *lower bound*; the backend reports the exact count
  // on submit. Good enough for a preview.
  const assignedFromList = Array.isArray(d.assignees)
    ? d.assignees.reduce((a, x) => a + (x.assigned_count || 0), 0)
    : 0;
  const unassigned = Math.max(0, total - assignedFromList);

  const [scope, setScope] = React.useState("all"); // all | needs-review | unassigned
  const [mode, setMode] = React.useState("single"); // single | round_robin
  const [users, setUsers] = React.useState([]);
  const [loadErr, setLoadErr] = React.useState(null);
  const [singleEmail, setSingleEmail] = React.useState("");
  const [picked, setPicked] = React.useState(() => new Set()); // round-robin set of emails
  const [busy, setBusy] = React.useState(false);
  const [busyErr, setBusyErr] = React.useState(null);
  // Chunked-path progress: { phase: "fetching" | "assigning", fetched?, done?, total? }
  const [progress, setProgress] = React.useState(null);

  React.useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        const res = await window.tasksApi?.listUsers?.();
        const list = Array.isArray(res) ? res : (Array.isArray(res?.items) ? res.items : []);
        if (!cancelled) {
          setUsers(list);
          setSingleEmail(prev => prev || (list[0] && list[0].email) || "");
        }
      } catch (e) {
        if (!cancelled) setLoadErr(e?.message || String(e));
      }
    })();
    return () => { cancelled = true; };
  }, []);

  const estimatedCount = scope === "all" ? total
    : scope === "needs-review" ? needsReview
    : unassigned;

  const togglePicked = (email) => {
    setPicked(prev => {
      const next = new Set(prev);
      if (next.has(email)) next.delete(email);
      else next.add(email);
      return next;
    });
  };

  const submit = async () => {
    if (busy) return;
    setBusyErr(null);
    setProgress(null);
    // Build filter from scope.
    const filter = { cohort_id: c.id };
    if (scope === "needs-review") filter.status = "needs-review";
    else if (scope === "unassigned") filter.assignee_email = null;

    const payload = { filter };
    if (mode === "round_robin") {
      const list = Array.from(picked);
      if (list.length === 0) { setBusyErr("Pick at least one assignee for round-robin."); return; }
      payload.strategy = "round_robin";
      payload.assignees = list;
    } else {
      if (!singleEmail) { setBusyErr("Pick an assignee."); return; }
      payload.email = singleEmail;
    }

    setBusy(true);
    try {
      // Try the filter path first. If the cohort exceeds the server cap
      // (50k), the server returns {matched, cap}; we then paginate ids
      // ourselves and submit in 5k chunks.
      let res;
      let needsChunking = estimatedCount > ASSIGN_CAP;
      if (!needsChunking) {
        try {
          res = await window.tasksApi.assignBulk(payload);
        } catch (e) {
          const b = e && e.body;
          if (b && typeof b.matched === "number" && b.matched > (b.cap || ASSIGN_CAP)) {
            needsChunking = true;
          } else {
            throw e;
          }
        }
      }

      if (needsChunking) {
        setProgress({ phase: "fetching", fetched: 0, pages: 0 });
        const ids = await fetchAssignableIds({
          cohort: c.id,
          status: scope === "needs-review" ? "needs-review" : undefined,
          unassignedOnly: scope === "unassigned",
          onProgress: (p) => setProgress({ phase: "fetching", ...p }),
        });
        if (ids.length === 0) {
          res = { updated: 0, by_assignee: {} };
        } else {
          setProgress({ phase: "assigning", done: 0, total: 0 });
          res = await assignIdsChunked({
            ids,
            strategy: mode === "round_robin" ? "round_robin" : "assign_all",
            email: mode === "round_robin" ? null : singleEmail,
            assignees: mode === "round_robin" ? payload.assignees : null,
            onProgress: (p) => setProgress({ phase: "assigning", ...p }),
          });
        }
      }

      if (onNotice) {
        const who = mode === "round_robin"
          ? `round-robin across ${payload.assignees.length}`
          : singleEmail;
        onNotice(`Assigned ${(res && res.updated) || 0} task(s) (${who}).`);
      }
      if (onDone) onDone(res);
      onClose && onClose();
    } catch (e) {
      const msg = (e && e.body && e.body.error) || (e && e.message) || "Assign failed.";
      const missing = e && e.body && e.body.missing;
      setBusyErr(missing && missing.length ? `${msg}: ${missing.join(", ")}` : msg);
    } finally {
      setBusy(false);
      setProgress(null);
    }
  };

  const submitLabel = (() => {
    if (!busy) return `Assign ${estimatedCount.toLocaleString()}`;
    if (progress && progress.phase === "fetching") {
      return `Fetching ids… (${(progress.fetched || 0).toLocaleString()})`;
    }
    if (progress && progress.phase === "assigning" && progress.total) {
      return `Assigning (${progress.done}/${progress.total})…`;
    }
    return "Assigning…";
  })();

  const summaryAssignee = mode === "round_robin"
    ? (picked.size ? `round-robin · ${picked.size} assignee${picked.size > 1 ? "s" : ""}` : "— pick assignees")
    : (singleEmail || "— pick assignee");

  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal" onClick={e=>e.stopPropagation()} style={{maxWidth:560}}>
        <div className="modal-head">
          <h3>Assign cohort · {c.name}</h3>
          <button className="btn ghost" style={{marginLeft:"auto"}} onClick={onClose}>{Ico.x ? Ico.x() : "×"}</button>
        </div>
        <div style={{padding:"18px 22px", display:"grid", gap:16}}>
          {loadErr && <div className="tag red">{loadErr}</div>}
          <div>
            <div style={{fontSize:11.5, color:"var(--ink-mute)", fontFamily:"var(--font-mono)", textTransform:"uppercase", letterSpacing:"0.05em", marginBottom:6}}>Scope</div>
            <div style={{display:"flex", gap:6, flexWrap:"wrap"}}>
              {[
                ["all", `All tasks (${total.toLocaleString()})`],
                ["needs-review", `Only needs-review (${needsReview.toLocaleString()})`],
                ["unassigned", `Only unassigned (${unassigned.toLocaleString()})`],
              ].map(([k, l]) => (
                <button
                  key={k}
                  className="filter-pill"
                  data-active={scope === k}
                  onClick={()=>setScope(k)}
                >{l}</button>
              ))}
            </div>
          </div>

          <div>
            <div style={{fontSize:11.5, color:"var(--ink-mute)", fontFamily:"var(--font-mono)", textTransform:"uppercase", letterSpacing:"0.05em", marginBottom:6}}>Mode</div>
            <div style={{display:"flex", gap:14, alignItems:"center", fontSize:12.5}}>
              <label style={{display:"flex", gap:6, alignItems:"center", cursor:"pointer"}}>
                <input type="radio" checked={mode === "single"} onChange={()=>setMode("single")}/> Single assignee
              </label>
              <label style={{display:"flex", gap:6, alignItems:"center", cursor:"pointer"}}>
                <input type="radio" checked={mode === "round_robin"} onChange={()=>setMode("round_robin")}/> Round-robin
              </label>
            </div>
          </div>

          {mode === "single" && (
            <div>
              <div style={{fontSize:11.5, color:"var(--ink-mute)", fontFamily:"var(--font-mono)", textTransform:"uppercase", letterSpacing:"0.05em", marginBottom:6}}>Assignee</div>
              <select
                className="inp"
                style={{width:"100%"}}
                value={singleEmail}
                onChange={e=>setSingleEmail(e.target.value)}
              >
                {users.length === 0 && <option value="">(no users)</option>}
                {users.map(u => (
                  <option key={u.email} value={u.email}>
                    {u.name ? `${u.name} · ${u.email}` : u.email}
                  </option>
                ))}
              </select>
            </div>
          )}

          {mode === "round_robin" && (
            <div>
              <div style={{fontSize:11.5, color:"var(--ink-mute)", fontFamily:"var(--font-mono)", textTransform:"uppercase", letterSpacing:"0.05em", marginBottom:6}}>
                Assignees ({picked.size} selected)
              </div>
              <div style={{display:"flex", gap:6, flexWrap:"wrap"}}>
                {users.length === 0 && (
                  <div style={{fontSize:12, color:"var(--ink-mute)", fontStyle:"italic"}}>no users</div>
                )}
                {users.map(u => (
                  <button
                    key={u.email}
                    className="filter-pill"
                    data-active={picked.has(u.email)}
                    onClick={()=>togglePicked(u.email)}
                    title={u.email}
                  >
                    {u.name || u.email}
                  </button>
                ))}
              </div>
            </div>
          )}

          <div style={{padding:"10px 12px", background:"var(--bg-sunken)", border:"1px solid var(--line)", borderRadius:"var(--r-md)", fontSize:12.5, color:"var(--ink-dim)"}}>
            Estimated: <b style={{color:"var(--ink)"}}>{estimatedCount.toLocaleString()}</b> task{estimatedCount === 1 ? "" : "s"} → {summaryAssignee}
            {estimatedCount > ASSIGN_CAP && (
              <div style={{marginTop:6, color:"var(--ink-mute)", fontSize:11.5}}>
                Exceeds server cap ({ASSIGN_CAP.toLocaleString()}) — will paginate ids and submit in {ASSIGN_CHUNK_SIZE.toLocaleString()}-task batches.
              </div>
            )}
          </div>

          {busyErr && <div className="tag red">{busyErr}</div>}

          <div style={{display:"flex", gap:8, justifyContent:"flex-end"}}>
            <button className="btn ghost" onClick={onClose} disabled={busy}>Cancel</button>
            <button
              className="btn primary"
              disabled={busy || estimatedCount === 0}
              onClick={submit}
            >
              {submitLabel}
            </button>
          </div>
        </div>
      </div>
    </div>
  );
}

Object.assign(window, {
  AssignDropdown,
  AssignCohortModal,
  ASSIGN_CAP,
  ASSIGN_CHUNK_SIZE,
  fetchAssignableIds,
  assignIdsChunked,
});
