// src/features/admin/ui/admin-students-card.jsx
//
// Per-student card + the class-history sub-component. Extracted from
// admin-students.jsx for size.
// Public on window: ASCard, ASClassHistory.
// Reads modals off window — admin-students-modals.jsx loads first.

const { ASConfirmDelete, ASEditStudent, ASRechargeModal } = window;

// Friendly text for the assignment-meet backend's skip reasons, so the admin
// understands WHY a one-click Meet link couldn't be made. Anything we don't
// recognise falls back to a generic retry message.
const asMeetReason = (reason) => {
  switch (reason) {
    case 'not connected':
    case 'no refresh token':
      return "This teacher hasn't connected their Google Calendar yet.";
    case 'instructor has no user_id':
      return 'This teacher has no login account to generate from.';
    case 'no-session':
      return 'Your session expired — refresh and try again.';
    case 'network':
      return 'Network error — try again.';
    default:
      return 'Could not generate a link. Try again.';
  }
};

// Inline per-assignment meeting-URL editor. One URL per (student, instructor)
// pair — both the student's dashboard and the instructor's dashboard pick
// this up for their Join button. Per-booking meeting_link on bookings still
// overrides this when set (rare — used for one-off lessons).
//
// "Generate" mints a persistent Google Meet room from the teacher's connected
// Google Calendar (works only when that teacher has connected Google);
// "Copy" puts the current link on the clipboard.
const ASMeetingUrlEditor = ({ assignment }) => {
  const data = window.useAdminData();
  const [val, setVal]   = React.useState(assignment.meetingUrl || '');
  const [busy, setBusy] = React.useState(false);
  const [saved, setSaved] = React.useState(false);
  const [gen, setGen]   = React.useState(false);
  const [genMsg, setGenMsg] = React.useState('');
  const [copied, setCopied] = React.useState(false);
  React.useEffect(() => { setVal(assignment.meetingUrl || ''); }, [assignment.meetingUrl]);

  const save = async () => {
    const next = (val || '').trim();
    if (next === (assignment.meetingUrl || '')) return;
    setBusy(true); setSaved(false);
    try {
      await data.updateAssignment(assignment.id, { meetingUrl: next });
      setSaved(true);
      setTimeout(() => setSaved(false), 1500);
    } catch (e) { alert(e.message || 'Save failed.'); }
    finally { setBusy(false); }
  };

  // One click → a Google Meet room for this student↔teacher pair. The backend
  // writes assignments.meeting_url itself (service role) and our data method
  // refreshes the cache, so we just mirror the URL into the box. A non-success
  // result carries a reason we turn into a sentence.
  const generate = async () => {
    setGen(true); setGenMsg('');
    try {
      const res = await data.generateAssignmentMeet(assignment.id);
      if (res && res.ok && res.meetingUrl) { setVal(res.meetingUrl); }
      else { setGenMsg(asMeetReason(res && res.reason)); }
    } catch (e) {
      setGenMsg('Could not generate a link. Try again.');
    } finally { setGen(false); }
  };

  const copy = async () => {
    const text = (val || '').trim();
    if (!text) return;
    let ok = false;
    // Prefer the async Clipboard API. It can reject (permissions policy) OR
    // hang indefinitely when the tab isn't focused, so race it against a short
    // timeout — we never want the button to wedge waiting on it.
    if (navigator.clipboard && navigator.clipboard.writeText) {
      try {
        await Promise.race([
          navigator.clipboard.writeText(text).then(() => { ok = true; }),
          new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 1000)),
        ]);
      } catch (_) { /* fall through to the textarea path */ }
    }
    if (!ok) {
      // Synchronous fallback — works without focus and on older browsers.
      try {
        const ta = document.createElement('textarea');
        ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0';
        document.body.appendChild(ta); ta.focus(); ta.select();
        ok = document.execCommand('copy');
        document.body.removeChild(ta);
      } catch (_) { /* no clipboard available — leave the link visible to copy by hand */ }
    }
    if (ok) { setCopied(true); setTimeout(() => setCopied(false), 1500); }
  };

  const btnBase = { padding:'4px 9px', borderRadius:6, border:'1px solid oklch(88% 0.02 265)', background:'#fff', fontSize:11, fontWeight:700, whiteSpace:'nowrap' };
  const hasVal = !!(val && val.trim());

  return (
    <div style={{ padding:'4px 0', fontSize:11 }}>
      <div style={{ display:'flex', alignItems:'center', gap:6, flexWrap:'wrap' }}>
        <span style={{ color:'oklch(58% 0.03 265)', fontWeight:600, whiteSpace:'nowrap' }}>
          Meeting URL · {assignment.instructor?.name || '—'}:
        </span>
        <input
          type="url" value={val} placeholder="https://zoom.us/j/123…"
          onChange={e => setVal(e.target.value)}
          onBlur={save}
          onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); save(); } }}
          disabled={busy || gen}
          style={{ flex:1, minWidth:160, padding:'4px 8px', borderRadius:6, border:'1px solid oklch(88% 0.02 265)', fontSize:11, fontFamily:"'Plus Jakarta Sans', sans-serif", background:'#fff' }}
        />
        <button
          type="button" onClick={generate} disabled={gen || busy}
          title="Create a Google Meet room from the teacher's connected Google Calendar"
          style={{ ...btnBase, color: gen ? 'oklch(58% 0.03 265)' : 'oklch(42% 0.13 265)', borderColor:'oklch(82% 0.06 265)', cursor: (gen || busy) ? 'default' : 'pointer' }}
        >
          {gen ? 'Generating…' : 'Generate'}
        </button>
        {hasVal && (
          <button
            type="button" onClick={copy}
            title="Copy meeting link"
            style={{ ...btnBase, color: copied ? 'oklch(45% 0.15 150)' : 'oklch(45% 0.03 265)', cursor:'pointer' }}
          >
            {copied ? 'Copied' : 'Copy'}
          </button>
        )}
        {busy && <span style={{ color:'oklch(58% 0.03 265)' }}>saving…</span>}
        {saved && <span style={{ color:'oklch(30% 0.13 150)', fontWeight:700 }}>saved</span>}
      </div>
      {genMsg && (
        <div style={{ marginTop:4, color:'oklch(52% 0.14 35)', fontSize:11 }}>{genMsg}</div>
      )}
    </div>
  );
};

// Small format helper used by ASClassHistory rows. Kept inline here
// because nothing else outside this file uses it.
const ASfmtH = h => h === 0 ? '12 AM' : h === 12 ? '12 PM' : h > 12 ? (h-12)+' PM' : h+' AM';

const ASClassHistory = ({ student, bookings, instructors }) => {
  const data = window.useAdminData();
  const [busyId, setBusyId] = React.useState(null);
  const [deleteTarget, setDeleteTarget] = React.useState(null);

  const my = bookings.filter(b => b.studentId === student.id)
    .sort((a,b) => (b.scheduledAt || '').localeCompare(a.scheduledAt || ''));
  const today = new Date(); today.setHours(0,0,0,0);
  const upcoming = my.filter(b => new Date(b.scheduledAt) >= today && b.status !== 'cancelled');
  const past     = my.filter(b => new Date(b.scheduledAt) <  today || b.status === 'cancelled');

  const act = async (id, patch) => {
    setBusyId(id);
    try { await data.setBookingFlags(id, patch); }
    catch (e) { alert(e.message || 'Update failed'); }
    finally { setBusyId(null); }
  };

  const Row = ({ b }) => {
    const d = new Date(b.scheduledAt);
    const inst = instructors.find(i => i.id === b.instructorId);
    const busy = busyId === b.id;
    const isPast = new Date(b.scheduledAt) < today;
    const cancelled = b.status === 'cancelled';
    const rd = b.rescheduleRequestedAt ? new Date(b.rescheduleRequestedAt) : null;
    return (
      <div style={{ display:'flex', justifyContent:'space-between', alignItems:'flex-start', padding:'9px 0', borderBottom:'1px solid oklch(95% 0.005 60)', fontSize:12, gap:12, flexWrap:'wrap' }}>
        <div style={{ flex:1, minWidth:200 }}>
          <div style={{ fontWeight:600, color: cancelled ? 'oklch(60% 0.03 265)' : 'oklch(26% 0.05 265)', textDecoration: cancelled ? 'line-through' : 'none' }}>
            {d.toLocaleDateString(undefined, { month:'short', day:'numeric', year:'numeric' })} · {ASfmtH(d.getHours())} <span style={{ fontWeight:600, color:'oklch(54% 0.04 265)' }}>{window.adminSchTzAbbrev?.(d) || ''}</span> · {b.durationMinutes}m
            {b.isRecurring && <span style={{ marginLeft:6, fontSize:10, color:'oklch(50% 0.04 265)' }}>↻ recurring</span>}
          </div>
          <div style={{ color:'oklch(56% 0.03 265)', marginTop:2 }}>
            {inst?.name || '?'} · {ASfmt$(b.studentRate)} student / {ASfmt$(b.teacherRate)} teacher
            {b.paid && <span style={{ marginLeft:6, fontSize:11, color:'oklch(30% 0.13 150)', fontWeight:600 }}>paid</span>}
            {b.paymentMethod === 'cash' && <span style={{ marginLeft:4, fontSize:11, color:'oklch(44% 0.04 265)' }}>cash</span>}
            {cancelled && <span style={{ marginLeft:6, color:'oklch(40% 0.04 265)', fontStyle:'italic' }}>cancelled</span>}
          </div>
          {/* Notes the student attached to this lesson — admin-only by RLS.
              Joe explicitly wanted these visible to him for context, but
              hidden from the student (already hidden — they only see message
              they wrote, not anyone else's notes about it). */}
          {b.message && (
            <div style={{ marginTop:6, fontSize:11, color:'oklch(46% 0.04 265)', background:'oklch(97% 0.005 60)', borderLeft:'2px solid oklch(88% 0.02 265)', padding:'5px 9px', borderRadius:5, fontStyle:'italic' }}>
              Student note: {b.message}
            </div>
          )}
          {b.teacherNotes && (
            <div style={{ marginTop:6, fontSize:11, color:'oklch(32% 0.07 265)', background:'oklch(97% 0.02 265)', borderLeft:'2px solid oklch(60% 0.18 265)', padding:'7px 9px', borderRadius:5 }}>
              <div style={{ fontSize:10, fontWeight:700, color:'oklch(40% 0.14 265)', textTransform:'uppercase', letterSpacing:'0.05em', marginBottom:3 }}>Teacher session note</div>
              {b.teacherNotes}
              {b.teacherNotesAt && <div style={{ fontSize:10, color:'oklch(60% 0.03 265)', marginTop:3 }}>Saved {new Date(b.teacherNotesAt).toLocaleString()}</div>}
            </div>
          )}
          {rd && (
            <div style={{ marginTop:6, background:'oklch(96.5% 0.07 80)', borderRadius:7, padding:'7px 10px', fontSize:11, color:'oklch(34% 0.13 75)', display:'flex', alignItems:'center', gap:8, flexWrap:'wrap' }}>
              <span style={{ fontWeight:700 }}>🔄 Reschedule pending</span>
              <span>→ {rd.toLocaleDateString(undefined, { month:'short', day:'numeric' })} · {ASfmtH(rd.getHours())} {window.adminSchTzAbbrev?.(rd) || ''}</span>
              <button disabled={busy} onClick={async () => { setBusyId(b.id); try { await data.approveReschedule(b.id); } catch(e){ alert(e.message); } finally { setBusyId(null); } }}
                style={{ background:'oklch(32% 0.14 150)', color:'#fff', border:'none', borderRadius:5, padding:'3px 8px', fontSize:10, fontWeight:700, cursor:busy?'wait':'pointer', fontFamily:"'Plus Jakarta Sans', sans-serif" }}>Approve</button>
              <button disabled={busy} onClick={async () => { setBusyId(b.id); try { await data.declineReschedule(b.id); } catch(e){ alert(e.message); } finally { setBusyId(null); } }}
                style={{ background:'#fff', color:'oklch(40% 0.1 25)', border:'1px solid oklch(88% 0.08 25)', borderRadius:5, padding:'3px 8px', fontSize:10, fontWeight:700, cursor:busy?'wait':'pointer', fontFamily:"'Plus Jakarta Sans', sans-serif" }}>Decline</button>
            </div>
          )}
        </div>
        <div style={{ display:'flex', gap:4, flexWrap:'wrap', justifyContent:'flex-end' }}>
          {!cancelled && (
            <React.Fragment>
              {/* Always-visible paid/unpaid toggle. Clicking it flips state in
                  either direction, so a mis-tap is recoverable and the button
                  never becomes a no-op. */}
              <button disabled={busy} onClick={() => act(b.id, { paid: !b.paid })} style={{
                background: b.paid ? 'oklch(91% 0.1 150)' : 'oklch(96% 0.01 265)',
                border: b.paid ? '1px solid oklch(82% 0.08 150)' : '1px solid oklch(86% 0.02 265)',
                borderRadius:6, padding:'4px 8px', fontSize:11, fontWeight:600,
                color: b.paid ? 'oklch(28% 0.13 150)' : 'oklch(28% 0.05 265)',
                cursor:busy?'wait':'pointer', fontFamily:"'Plus Jakarta Sans', sans-serif",
              }}>
                {b.paid ? '✓ Paid · click to undo' : 'Mark paid'}
              </button>
              {b.paid && !b.settled && (
                <button disabled={busy} onClick={() => act(b.id, { settled:true })} style={{ background: b.paymentMethod==='cash' ? 'oklch(44% 0.12 150)' : 'oklch(22% 0.06 265)', border:'none', borderRadius:6, padding:'4px 8px', fontSize:11, fontWeight:700, color:'#fff', cursor:busy?'wait':'pointer', fontFamily:"'Plus Jakarta Sans', sans-serif" }}>
                  {b.paymentMethod === 'cash' ? 'Cash received' : 'Payout sent'}
                </button>
              )}
            </React.Fragment>
          )}
          <button disabled={busy} onClick={() => setDeleteTarget(b)}
            title="Permanently delete this class"
            style={{ background:'#fff', color:'oklch(40% 0.16 25)', border:'1px solid oklch(86% 0.12 25)', borderRadius:6, padding:'4px 8px', fontSize:11, fontWeight:700, cursor:busy?'wait':'pointer', fontFamily:"'Plus Jakarta Sans', sans-serif" }}>
            Delete
          </button>
        </div>
      </div>
    );
  };

  return (
    <div>
      <div style={{ marginBottom:14 }}>
        <div style={{ fontSize:11, fontWeight:700, textTransform:'uppercase', letterSpacing:'0.07em', color:'oklch(55% 0.03 265)', marginBottom:8 }}>
          Upcoming ({upcoming.length})
        </div>
        {upcoming.length === 0
          ? <div style={{ fontSize:12, color:'oklch(64% 0.03 265)', fontStyle:'italic' }}>No upcoming lessons.</div>
          : upcoming.map(b => <Row key={b.id} b={b} />)}
      </div>
      <div>
        <div style={{ fontSize:11, fontWeight:700, textTransform:'uppercase', letterSpacing:'0.07em', color:'oklch(55% 0.03 265)', marginBottom:8 }}>
          Past ({past.length})
        </div>
        {past.length === 0
          ? <div style={{ fontSize:12, color:'oklch(64% 0.03 265)', fontStyle:'italic' }}>No past lessons.</div>
          : past.slice(0, 30).map(b => <Row key={b.id} b={b} />)}
        {past.length > 30 && <div style={{ fontSize:11, color:'oklch(60% 0.03 265)', marginTop:6, textAlign:'center' }}>… {past.length - 30} more</div>}
      </div>
      {deleteTarget && (
        <window.AdminDeleteBookingModal
          booking={deleteTarget}
          onClose={() => setDeleteTarget(null)}
        />
      )}
    </div>
  );
};

// ── Inline "see + change login email" field ───────────────────────────────
// The login email lives in auth.users; editing it routes through
// data.updateUserEmail → /api/admin-update-email (service-role GoTrue admin
// call). The user id never changes, so bookings/assignments/etc. are
// untouched — only the login address moves. Twin of AIEmailEditorRow in the
// instructors card (kept local so each .jsx stays standalone for the
// claude.ai paste workflow).
const ASEmailEditorRow = ({ userId, email, onSaved }) => {
  const data = window.useAdminData();
  const [editing, setEditing] = React.useState(false);
  const [draft, setDraft]     = React.useState(email || '');
  const [busy, setBusy]       = React.useState(false);
  const [msg, setMsg]         = React.useState('');
  const [isErr, setIsErr]     = React.useState(false);
  React.useEffect(() => { if (!editing) setDraft(email || ''); }, [email, editing]);

  const begin  = () => { setDraft(email || ''); setMsg(''); setIsErr(false); setEditing(true); };
  const cancel = () => { setEditing(false); setMsg(''); setIsErr(false); setDraft(email || ''); };
  const save = async () => {
    const next = (draft || '').trim().toLowerCase();
    if (next === (email || '').trim().toLowerCase()) { setEditing(false); return; }
    if (!next.includes('@') || /\s/.test(next)) { setIsErr(true); setMsg('Enter a valid email address.'); return; }
    if (!window.confirm(`Change the login email to "${next}"?\n\nThey'll use this address to log in (magic link) from now on. Lessons, payments and all other data are unaffected.`)) return;
    setBusy(true); setMsg(''); setIsErr(false);
    try {
      const saved = await data.updateUserEmail(userId, next);
      onSaved && onSaved(saved);
      setEditing(false); setIsErr(false); setMsg('Email updated');
      setTimeout(() => setMsg(''), 2400);
    } catch (e) {
      setIsErr(true); setMsg(e.message || 'Could not update email.');
    } finally { setBusy(false); }
  };

  const lbl = { fontSize:10, fontWeight:700, textTransform:'uppercase', letterSpacing:'0.06em', color:'oklch(52% 0.04 265)', marginBottom:5 };
  return (
    <div style={{ padding:'2px 0 10px' }}>
      <div style={lbl}>Login email</div>
      {!editing ? (
        <div style={{ display:'flex', alignItems:'center', gap:10, flexWrap:'wrap' }}>
          <span style={{ fontSize:13.5, fontWeight:600, color: email ? 'oklch(24% 0.05 265)' : 'oklch(64% 0.03 265)', wordBreak:'break-all' }}>
            {email || 'none on file'}
          </span>
          {userId ? (
            <button onClick={begin} style={{ background:'oklch(96% 0.04 265)', color:'oklch(28% 0.13 265)', border:'1px solid oklch(86% 0.08 265)', borderRadius:7, padding:'4px 11px', fontSize:11, fontWeight:700, cursor:'pointer', fontFamily:"'Plus Jakarta Sans', sans-serif" }}>Edit</button>
          ) : (
            <span style={{ fontSize:11, color:'oklch(64% 0.03 265)', fontStyle:'italic' }}>no account — can't edit</span>
          )}
          {msg && <span style={{ fontSize:12, fontWeight:600, color: isErr ? 'oklch(45% 0.16 25)' : 'oklch(34% 0.13 150)' }}>{msg}</span>}
        </div>
      ) : (
        <div style={{ display:'flex', alignItems:'center', gap:8, flexWrap:'wrap' }}>
          <input
            type="email" value={draft} autoFocus disabled={busy}
            onChange={e => setDraft(e.target.value)}
            onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); save(); } if (e.key === 'Escape') cancel(); }}
            placeholder="name@example.com"
            style={{ flex:'1 1 240px', minWidth:200, padding:'7px 10px', borderRadius:7, border:'1px solid oklch(80% 0.04 265)', fontSize:13, fontFamily:"'Plus Jakarta Sans', sans-serif", color:'oklch(22% 0.05 265)', background:'#fff', outline:'none' }}
          />
          <button onClick={save} disabled={busy} style={{ background:'oklch(22% 0.06 265)', color:'#fff', border:'none', borderRadius:7, padding:'7px 14px', fontSize:12, fontWeight:700, cursor:busy?'wait':'pointer', fontFamily:"'Plus Jakarta Sans', sans-serif", opacity:busy?0.6:1 }}>{busy ? 'Saving…' : 'Save'}</button>
          <button onClick={cancel} disabled={busy} style={{ background:'oklch(96% 0.01 265)', color:'oklch(34% 0.05 265)', border:'1px solid oklch(88% 0.02 265)', borderRadius:7, padding:'7px 12px', fontSize:12, fontWeight:600, cursor:'pointer', fontFamily:"'Plus Jakarta Sans', sans-serif" }}>Cancel</button>
          {msg && isErr && <span style={{ fontSize:12, fontWeight:600, color:'oklch(45% 0.16 25)' }}>{msg}</span>}
        </div>
      )}
    </div>
  );
};

// ── Right-side drawer + collapsible "folder" ──────────────────────────────
// AS-prefixed twin of AIDrawer/AISection in the instructors card. Mirrors the
// admin Schedule detail drawer — slide-in from the right, dim backdrop,
// click-outside / × / Esc to close. Defined locally (the .jsx files share one
// global scope) so this card stays standalone for the claude.ai paste workflow.
const ASDrawer = ({ eyebrow, title, subtitle, statusChip, accentHue = 258, onClose, children }) => {
  const [shown, setShown] = React.useState(false);
  const handleClose = React.useCallback(() => { setShown(false); setTimeout(onClose, 200); }, [onClose]);
  React.useEffect(() => {
    const t = setTimeout(() => setShown(true), 10);
    const onKey = (e) => { if (e.key === 'Escape') handleClose(); };
    document.addEventListener('keydown', onKey);
    const prevOverflow = document.body.style.overflow;
    document.body.style.overflow = 'hidden';
    return () => { clearTimeout(t); document.removeEventListener('keydown', onKey); document.body.style.overflow = prevOverflow; };
  }, [handleClose]);
  return (
    <div onClick={handleClose}
      style={{ position:'fixed', inset:0, background:'rgba(15,18,40,0.35)', zIndex:1000, display:'flex', justifyContent:'flex-end' }}>
      <div onClick={e => e.stopPropagation()}
        style={{ width:'min(460px, 96vw)', height:'100%', background:'oklch(99% 0.004 60)',
          boxShadow:'-8px 0 40px rgba(0,0,0,0.18)', transform: shown ? 'translateX(0)' : 'translateX(100%)',
          transition:'transform 0.24s cubic-bezier(0.4,0,0.2,1)', overflowY:'auto',
          fontFamily:"'Plus Jakarta Sans', sans-serif", display:'flex', flexDirection:'column' }}>
        <div style={{ padding:'18px 22px 16px', borderBottom:'1px solid oklch(92% 0.01 265)', position:'sticky', top:0, background:'oklch(99% 0.004 60)', zIndex:2 }}>
          <div style={{ display:'flex', justifyContent:'space-between', alignItems:'flex-start', gap:12 }}>
            <div style={{ minWidth:0 }}>
              {eyebrow && <div style={{ fontSize:11, fontWeight:700, letterSpacing:'0.08em', textTransform:'uppercase', color:`oklch(58% 0.05 ${accentHue})`, marginBottom:6 }}>{eyebrow}</div>}
              <div style={{ fontFamily:"'Cormorant Garamond', serif", fontSize:24, fontWeight:700, color:'oklch(18% 0.05 265)', lineHeight:1.12, wordBreak:'break-word' }}>{title}</div>
              {subtitle && <div style={{ marginTop:6, fontSize:13, color:'oklch(46% 0.04 265)', fontWeight:600 }}>{subtitle}</div>}
              {statusChip && <div style={{ marginTop:10 }}>{statusChip}</div>}
            </div>
            <button onClick={handleClose} aria-label="Close" title="Close (Esc)"
              style={{ flexShrink:0, width:34, height:34, borderRadius:'50%', background:'oklch(95% 0.005 60)', border:'1px solid oklch(90% 0.01 265)', cursor:'pointer', fontSize:18, lineHeight:1, color:'oklch(40% 0.04 265)', display:'flex', alignItems:'center', justifyContent:'center' }}>×</button>
          </div>
        </div>
        <div style={{ display:'flex', flexDirection:'column' }}>{children}</div>
      </div>
    </div>
  );
};

const ASSection = ({ title, hint, count, accent, defaultOpen = false, children }) => {
  const [open, setOpen] = React.useState(defaultOpen);
  return (
    <div style={{ borderBottom:'1px solid oklch(94% 0.006 265)' }}>
      <button type="button" onClick={() => setOpen(o => !o)}
        style={{ width:'100%', display:'flex', alignItems:'center', gap:10, padding:'14px 22px', background: open ? 'oklch(98% 0.005 265)' : '#fff', border:'none', cursor:'pointer', textAlign:'left', fontFamily:"'Plus Jakarta Sans', sans-serif" }}>
        <span style={{ fontSize:13.5, fontWeight:700, color:'oklch(26% 0.05 265)', flexShrink:0 }}>
          {title}
          {count != null && <span style={{ marginLeft:8, fontSize:12, fontWeight:700, color: accent || 'oklch(55% 0.04 265)' }}>{count}</span>}
        </span>
        {hint && !open && <span style={{ flex:1, fontSize:11.5, color:'oklch(60% 0.03 265)', fontWeight:500, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap', textAlign:'right' }}>{hint}</span>}
        <span style={{ marginLeft:'auto', fontSize:12, color:'oklch(54% 0.04 265)', transform: open ? 'rotate(180deg)' : 'none', transition:'transform 0.15s', flexShrink:0 }}>▾</span>
      </button>
      {open && <div style={{ padding:'2px 22px 18px' }}>{children}</div>}
    </div>
  );
};

// ── Student Card ────────────────────────────────────────────────────────
const ASCard = ({ student }) => {
  const data = window.useAdminData();
  const isMobile = window.useIsMobile();
  const [rowHover, setRowHover] = React.useState(false);
  const [recharge, setRecharge] = React.useState(false);
  const [edit,     setEdit]     = React.useState(false);
  const [del,      setDel]      = React.useState(false);
  const [expand,   setExpand]   = React.useState(false);
  // Magic link state: { email } once we've resolved it; null while idle.
  const [magic,    setMagic]    = React.useState(null);
  const [magicBusy, setMagicBusy] = React.useState(false);
  const [showBlock, setShowBlock] = React.useState(false);
  const [loginLinkBusy, setLoginLinkBusy] = React.useState(false);
  // Inline panel state (not window.alert/prompt — see admin-instructors-card.jsx
  // for the rationale).
  const [loginLinkResult, setLoginLinkResult] = React.useState(null);
  const [loginLinkError, setLoginLinkError]   = React.useState('');
  const [loginLinkCopied, setLoginLinkCopied] = React.useState(false);
  // Shown inside the URL panel when clipboard write fails — keeps the URL
  // visible for manual copy instead of discarding it via the error view.
  const [loginLinkCopyHint, setLoginLinkCopyHint] = React.useState('');
  const closeLoginLinkPanel = () => {
    setLoginLinkResult(null); setLoginLinkError(''); setLoginLinkCopied(false); setLoginLinkCopyHint('');
  };

  // Mint a one-time magic-link URL for this student's account and copy
  // to clipboard. Admin opens an incognito window and pastes — lands
  // logged in as the student for real. Different from startMagic, which
  // EMAILS the link to the student. See api/admin-send-magic-link.js
  // (returnLink branch) for guards.
  const handleLoginLink = async () => {
    if (!student.id || loginLinkBusy) return;
    setLoginLinkBusy(true);
    setLoginLinkError('');
    setLoginLinkCopied(false);
    setLoginLinkCopyHint('');
    try {
      const { url, email } = await data.getImpersonateLink(student.id);
      setLoginLinkResult({ url, email });
      try {
        if (navigator.clipboard && navigator.clipboard.writeText) {
          await navigator.clipboard.writeText(url);
          setLoginLinkCopied(true);
        } else {
          setLoginLinkCopyHint('Select the URL above and copy it (Cmd+C).');
        }
      } catch (_) {
        setLoginLinkCopyHint('Select the URL above and copy it (Cmd+C).');
      }
    } catch (e) {
      setLoginLinkError(e?.message || String(e));
    } finally {
      setLoginLinkBusy(false);
    }
  };
  const copyLoginLink = async () => {
    if (!loginLinkResult) return;
    try {
      await navigator.clipboard.writeText(loginLinkResult.url);
      setLoginLinkCopied(true);
      setLoginLinkCopyHint('');
      setTimeout(() => setLoginLinkCopied(false), 1800);
    } catch (e) {
      // Clipboard blocked — keep the URL visible; prompt a manual copy
      // rather than switching to the error panel (which would hide it).
      setLoginLinkCopyHint('Clipboard blocked — select the URL above and copy it manually (Cmd+C).');
    }
  };

  // Cache the student's email so we can render it on the card (Joe asked for
  // the address to be visible inline, not hidden behind the Magic link button).
  // Lazy fetch on mount — getUserEmail hits the admin RPC, which is fast.
  const [studentEmail, setStudentEmail] = React.useState('');
  React.useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        const email = await data.getUserEmail(student.id);
        if (!cancelled && email) setStudentEmail(email);
      } catch (e) { /* leave blank, magic link button still works */ }
    })();
    return () => { cancelled = true; };
  }, [student.id]);

  const startMagic = async () => {
    setMagicBusy(true);
    try {
      const email = studentEmail || await data.getUserEmail(student.id);
      if (!email) { alert("Couldn't find this student's email. They may have been created outside the dashboard."); return; }
      setMagic({ email });
    } catch (e) {
      alert(e.message || 'Could not look up email.');
    } finally {
      setMagicBusy(false);
    }
  };

  const myAssignments = (data.assignments || []).filter(a => a.studentId === student.id && a.active);
  const instructorNames = myAssignments.map(a => a.instructor?.name).filter(Boolean).join(', ');

  const myBookings = (data.bookings || []).filter(b => b.studentId === student.id);
  const today = new Date(); today.setHours(0,0,0,0);
  const upcomingCount = myBookings.filter(b => new Date(b.scheduledAt) >= today && b.status !== 'cancelled').length;
  const completedCount = myBookings.filter(b => b.status === 'completed').length;
  // Past, non-cancelled lessons split by payment state. Both are always shown
  // (even when 0) so the admin can see at a glance how many have been paid.
  const pastBookings = myBookings.filter(b => b.status !== 'cancelled' && new Date(b.scheduledAt) < new Date());
  const paidCount   = pastBookings.filter(b => b.paid).length;
  const unpaidCount = pastBookings.filter(b => !b.paid).length;

  const balanceLow = student.classesBalance <= 1;
  const balanceColor = balanceLow ? 'oklch(40% 0.18 25)' : 'oklch(22% 0.06 265)';

  return (
    <div style={{ borderBottom:'1px solid oklch(95% 0.006 265)', background:'#fff' }}>
      {/* Collapsed row — minimal Google-Sheets style: name · classes-left ·
          instructor. No email (Joe: keep the outside row bare). Click → drawer. */}
      <div
        onClick={() => setExpand(true)}
        onMouseEnter={() => setRowHover(true)}
        onMouseLeave={() => setRowHover(false)}
        style={{
          display:'grid',
          gridTemplateColumns: isMobile ? 'minmax(0,1fr) 22px' : 'minmax(0,2.4fr) 70px minmax(0,1.6fr) 22px',
          alignItems:'center', gap:12, padding:'9px 16px', cursor:'pointer',
          background: rowHover ? 'oklch(98% 0.006 265)' : '#fff', transition:'background 0.12s',
        }}
      >
        {/* Name (+ no-invite dot) */}
        <div style={{ display:'flex', alignItems:'center', gap:10, minWidth:0 }}>
          <div style={{ width:30, height:30, borderRadius:7, background:`oklch(72% 0.12 ${student.hue||258})`, display:'flex', alignItems:'center', justifyContent:'center', fontFamily:"'Plus Jakarta Sans', sans-serif", fontSize:12, fontWeight:700, color:'#fff', flexShrink:0 }}>
            {student.initials}
          </div>
          <span style={{ fontWeight:700, fontSize:14, color:'oklch(18% 0.03 265)', overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>{student.name}</span>
          {/* NULL magic_link_sent_at = no invite email sent yet. */}
          {student.magicLinkSentAt == null && <span title="No invite email sent yet — open the row and use Magic link to send one" style={{ width:7, height:7, borderRadius:'50%', background:'oklch(80% 0.13 80)', flexShrink:0 }} />}
        </div>
        {/* Classes left (hidden on mobile) */}
        {!isMobile && (
          <div style={{ textAlign:'right', whiteSpace:'nowrap' }}>
            <span style={{ fontSize:14, fontWeight:700, color: balanceColor }}>{student.classesBalance}</span>
            <span style={{ fontSize:11, fontWeight:500, color:'oklch(62% 0.03 265)' }}> left</span>
          </div>
        )}
        {/* Instructor(s) (hidden on mobile) */}
        {!isMobile && (
          <div style={{ fontSize:12.5, color:'oklch(46% 0.04 265)', overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }} title={instructorNames || ''}>
            {instructorNames || <span style={{ color:'oklch(70% 0.03 265)', fontStyle:'italic' }}>no instructor</span>}
          </div>
        )}
        {/* Open-drawer chevron */}
        <div style={{ textAlign:'right', color:'oklch(60% 0.04 265)', fontSize:16, userSelect:'none', lineHeight:1 }}>›</div>
      </div>

      {/* Detail drawer — slides in from the right (like the Schedule drawer).
          Primary "+ Add Classes" up top; payments / teachers / settings folded. */}
      {expand && (
        <ASDrawer
          eyebrow="Student"
          title={student.name}
          subtitle={`${student.classesBalance} class${student.classesBalance === 1 ? '' : 'es'} left · ${instructorNames || 'no instructor'}`}
          onClose={() => setExpand(false)}
        >
          {/* Primary action — recharging the class balance is the most-used move */}
          <div style={{ padding:'16px 22px', borderBottom:'1px solid oklch(94% 0.006 265)' }}>
            <button onClick={() => setRecharge(true)}
              style={{ height:42, width:'100%', background:'oklch(72% 0.17 80)', color:'oklch(18% 0.06 265)', border:'none', borderRadius:10, fontSize:13.5, fontWeight:700, cursor:'pointer', fontFamily:"'Plus Jakarta Sans', sans-serif", display:'inline-flex', alignItems:'center', justifyContent:'center' }}>
              + Add Classes
            </button>
          </div>

          {/* Folder: Classes & payments */}
          <ASSection title="Classes & payments" hint={`${student.classesBalance} left · ${upcomingCount} upcoming`}>
            <div style={{ display:'flex', gap:22, flexWrap:'wrap', marginBottom:16 }}>
              {[
                { label:'Classes left', value: `${student.classesBalance} of ${student.classesPurchased}`, color: balanceColor },
                { label:'Upcoming',     value: upcomingCount },
                { label:'Completed',    value: completedCount },
                { label:'Paid',         value: paidCount,   color: paidCount > 0 ? 'oklch(30% 0.13 150)' : undefined },
                { label:'Unpaid past',  value: unpaidCount, color: unpaidCount > 0 ? 'oklch(40% 0.18 25)' : undefined },
              ].map(m => (
                <div key={m.label}>
                  <div style={{ fontSize:10, fontWeight:700, textTransform:'uppercase', letterSpacing:'0.06em', color:'oklch(58% 0.03 265)', marginBottom:3 }}>{m.label}</div>
                  <div style={{ fontSize:15, fontWeight:700, color: m.color || 'oklch(22% 0.06 265)' }}>{m.value}</div>
                </div>
              ))}
            </div>
            {window.AdminStudentPaymentsLog && <window.AdminStudentPaymentsLog student={student} />}
            <ASClassHistory student={student} bookings={data.bookings || []} instructors={data.instructors || []} />
          </ASSection>

          {/* Folder: Teachers & meeting links */}
          <ASSection title="Teachers & meeting links" count={myAssignments.length} hint={instructorNames || 'no teacher'}>
            {myAssignments.length === 0
              ? <div style={{ fontSize:13, color:'oklch(65% 0.03 265)', fontStyle:'italic', padding:'4px 0' }}>No teacher assigned yet.</div>
              : (
                <>
                  <div style={{ display:'flex', flexWrap:'wrap', gap:8, marginBottom:12 }}>
                    {myAssignments.map(a => {
                      const confirmed = !!a.confirmedByTeacherAt;
                      const when = confirmed ? new Date(a.confirmedByTeacherAt) : null;
                      return (
                        <div key={a.id} style={{ display:'flex', alignItems:'center', gap:8, background:'#fff', border:'1px solid oklch(92% 0.01 265)', borderRadius:8, padding:'5px 9px', fontSize:11 }}>
                          <span style={{ color:'oklch(28% 0.05 265)', fontWeight:600 }}>{a.instructor?.name || 'Unknown teacher'}</span>
                          {confirmed ? (
                            <span title={`Confirmed ${when.toLocaleString()}`} style={{ background:'oklch(94% 0.08 150)', color:'oklch(30% 0.13 150)', borderRadius:5, padding:'2px 7px', fontSize:10, fontWeight:700 }}>
                              Confirmed {when.toLocaleDateString(undefined, { month:'short', day:'numeric' })}
                            </span>
                          ) : (
                            <span style={{ background:'oklch(95% 0.1 80)', color:'oklch(40% 0.14 75)', borderRadius:5, padding:'2px 7px', fontSize:10, fontWeight:700 }}>
                              Pending teacher confirm
                            </span>
                          )}
                        </div>
                      );
                    })}
                  </div>
                  <div style={{ fontSize:10, fontWeight:700, textTransform:'uppercase', letterSpacing:'0.06em', color:'oklch(52% 0.04 265)', marginBottom:6 }}>
                    Meeting links · per teacher
                  </div>
                  {myAssignments.map(a => <ASMeetingUrlEditor key={a.id} assignment={a} />)}
                  <div style={{ fontSize:10, color:'oklch(60% 0.03 265)', marginTop:4, fontStyle:'italic' }}>
                    One link per student-teacher pair. Both dashboards' Join button picks it up.
                  </div>
                </>
              )}
          </ASSection>

          {/* Folder: Settings — login email (see + edit), profile, magic link, etc. */}
          <ASSection title="Settings" hint={studentEmail || 'email · edit · magic link…'}>
            <ASEmailEditorRow userId={student.id} email={studentEmail} onSaved={setStudentEmail} />
            <div style={{ padding:'4px 0 2px', fontSize:12, color:'oklch(50% 0.04 265)' }}>
              {student.topics?.[0] && <>{student.topics[0]} · </>}
              {ASfmt$(student.hourlyRate)}/hr
              {student.isOnline
                ? <> · <span style={{ color:'oklch(40% 0.12 265)', fontWeight:600 }}>Online</span>{student.meetingLink && <> · <a href={student.meetingLink} target="_blank" rel="noopener noreferrer" style={{ color:'oklch(28% 0.13 265)', textDecoration:'underline' }}>meeting link</a></>}</>
                : (student.address && <> · {student.address}</>)}
            </div>
            <div style={{ display:'flex', gap:8, flexWrap:'wrap', marginTop:12 }}>
              <button onClick={() => setEdit(true)} style={{ background:'oklch(96% 0.01 265)', color:'oklch(28% 0.05 265)', border:'1px solid oklch(88% 0.02 265)', borderRadius:8, padding:'8px 13px', fontSize:12, fontWeight:600, cursor:'pointer', fontFamily:"'Plus Jakarta Sans', sans-serif" }}>Edit</button>
              <button onClick={() => setShowBlock(true)} title="Block off a specific date/time this student isn't available" style={{ background:'oklch(96% 0.01 265)', color:'oklch(28% 0.05 265)', border:'1px solid oklch(88% 0.02 265)', borderRadius:8, padding:'8px 13px', fontSize:12, fontWeight:600, cursor:'pointer', fontFamily:"'Plus Jakarta Sans', sans-serif" }}>Block</button>
              <button onClick={() => window.open('?admin_view_student=' + student.id, '_blank', 'noopener')} title="Open this student's dashboard in a new tab (read-only)" style={{ background:'oklch(96% 0.01 265)', color:'oklch(28% 0.05 265)', border:'1px solid oklch(88% 0.02 265)', borderRadius:8, padding:'8px 13px', fontSize:12, fontWeight:600, cursor:'pointer', fontFamily:"'Plus Jakarta Sans', sans-serif" }}>View as</button>
              {student.id && (
                <button onClick={handleLoginLink} disabled={loginLinkBusy} title="Mint a one-time magic-link URL and copy to clipboard. Paste in an incognito window to log in as this student for real." style={{ background:'oklch(96% 0.01 265)', color:'oklch(28% 0.05 265)', border:'1px solid oklch(88% 0.02 265)', borderRadius:8, padding:'8px 13px', fontSize:12, fontWeight:600, cursor: loginLinkBusy ? 'wait' : 'pointer', fontFamily:"'Plus Jakarta Sans', sans-serif", opacity: loginLinkBusy ? 0.6 : 1 }}>{loginLinkBusy ? '…' : 'Login link'}</button>
              )}
              <button onClick={startMagic} disabled={magicBusy} title="Email this student a magic-link login" style={{ background:'oklch(96% 0.01 265)', color:'oklch(28% 0.05 265)', border:'1px solid oklch(88% 0.02 265)', borderRadius:8, padding:'8px 13px', fontSize:12, fontWeight:600, cursor:magicBusy?'wait':'pointer', fontFamily:"'Plus Jakarta Sans', sans-serif", opacity:magicBusy?0.6:1 }}>{magicBusy ? '…' : 'Magic link'}</button>
            </div>
            <div style={{ marginTop:14, paddingTop:14, borderTop:'1px solid oklch(95% 0.005 60)' }}>
              <button onClick={() => setDel(true)} style={{ background:'oklch(98% 0.04 25)', color:'oklch(40% 0.18 25)', border:'1px solid oklch(88% 0.05 25)', borderRadius:8, padding:'8px 13px', fontSize:12, fontWeight:600, cursor:'pointer', fontFamily:"'Plus Jakarta Sans', sans-serif" }}>Delete student</button>
            </div>
          </ASSection>
        </ASDrawer>
      )}

      {recharge && <ASRechargeModal  student={student} onClose={() => setRecharge(false)} />}
      {edit     && <ASEditStudent    student={student} onClose={() => setEdit(false)}     />}
      {del      && <ASConfirmDelete  name={student.name} kind="student" onClose={() => setDel(false)} onConfirm={(typedName) => data.deleteStudent(student.id, typedName)} />}
      {magic    && <window.MagicLinkPostCreate name={student.name} email={magic.email} studentId={student.id} mode="resend" onClose={() => setMagic(null)} />}
      {showBlock && <window.BlockTimeModal     studentId={student.id} ownerName={student.name} ownerTimezone={student.timezone || 'America/New_York'} onClose={() => setShowBlock(false)} />}

      {(loginLinkResult || loginLinkError) && (
        <div data-testid="login-link-panel" style={{ position:'fixed', inset:0, background:'rgba(15,23,42,0.45)', zIndex:1100, display:'flex', alignItems:'center', justifyContent:'center', padding:20 }}
             onClick={(e)=>{ if (e.target === e.currentTarget) closeLoginLinkPanel(); }}>
          <div style={{ background:'#fff', borderRadius:14, padding:24, maxWidth:600, width:'100%', boxShadow:'0 20px 60px rgba(15,23,42,0.35)', fontFamily:"'Plus Jakarta Sans', sans-serif" }}>
            {loginLinkError ? (
              <>
                <div style={{ fontSize:16, fontWeight:700, color:'oklch(40% 0.18 25)', marginBottom:8 }}>Could not mint login link</div>
                <div style={{ fontSize:13, color:'oklch(35% 0.04 265)', marginBottom:18, lineHeight:1.5 }}>{loginLinkError}</div>
                <div style={{ display:'flex', justifyContent:'flex-end' }}>
                  <button onClick={closeLoginLinkPanel} style={{ background:'oklch(96% 0.01 265)', border:'1px solid oklch(88% 0.02 265)', borderRadius:8, padding:'9px 16px', fontSize:13, fontWeight:600, color:'oklch(28% 0.05 265)', cursor:'pointer' }}>Close</button>
                </div>
              </>
            ) : (
              <>
                <div style={{ fontSize:16, fontWeight:700, color:'oklch(18% 0.03 265)', marginBottom:6 }}>Login link for {loginLinkResult.email}</div>
                <div style={{ fontSize:13, color:'oklch(45% 0.04 265)', marginBottom:14, lineHeight:1.5 }}>
                  Open an <b>incognito</b> Chrome window (Cmd+Shift+N) and paste this URL — you'll be signed in as this student for real. Same RLS, same realtime, same view. Single-use, expires in 1 hour.
                </div>
                <textarea data-testid="login-link-url" readOnly value={loginLinkResult.url}
                  onFocus={(e)=>e.target.select()}
                  style={{ width:'100%', height:90, padding:10, fontFamily:'ui-monospace, SFMono-Regular, monospace', fontSize:11, border:'1px solid oklch(88% 0.02 265)', borderRadius:8, resize:'none', background:'oklch(97% 0.005 265)', color:'oklch(28% 0.05 265)', boxSizing:'border-box' }} />
                {loginLinkCopyHint && <div style={{ fontSize:12, color:'oklch(45% 0.16 60)', fontWeight:600, marginTop:8 }}>{loginLinkCopyHint}</div>}
                <div style={{ display:'flex', gap:8, marginTop:14, justifyContent:'flex-end', alignItems:'center' }}>
                  {loginLinkCopied && <span style={{ fontSize:12, color:'oklch(45% 0.13 150)', fontWeight:600, marginRight:8 }}>Copied to clipboard</span>}
                  <button onClick={closeLoginLinkPanel} style={{ background:'oklch(96% 0.01 265)', border:'1px solid oklch(88% 0.02 265)', borderRadius:8, padding:'9px 16px', fontSize:13, fontWeight:600, color:'oklch(28% 0.05 265)', cursor:'pointer' }}>Close</button>
                  <button onClick={copyLoginLink} style={{ background:'oklch(22% 0.06 265)', color:'#fff', border:'none', borderRadius:8, padding:'9px 18px', fontSize:13, fontWeight:700, cursor:'pointer' }}>{loginLinkCopied ? 'Copied ✓' : 'Copy URL'}</button>
                </div>
              </>
            )}
          </div>
        </div>
      )}
    </div>
  );
};

// ── Page ────────────────────────────────────────────────────────────────


Object.assign(window, { ASCard, ASClassHistory });
