// people.jsx — lab roster: avatars, PIN sign-in, person filter, roster editor, connection pill
//
// With the lab server, editing requires signing in with a per-person PIN; the server checks
// it and hands back a session token that's kept in this browser. Opened without the server
// (local mode) there's nothing to protect, so picking a name signs in straight away.

const SESSION_KEY = 'workshop_v2_session';

function useSession() {
  const [session, setSessionState] = React.useState(() => {
    try {
      localStorage.removeItem('workshop_v2_me'); // identity used to be pick-a-name, no PIN
      const s = JSON.parse(localStorage.getItem(SESSION_KEY) || 'null');
      return s && s.personId ? s : null;
    } catch (e) { return null; }
  });
  const setSession = (s) => {
    setSessionState(s);
    try { s ? localStorage.setItem(SESSION_KEY, JSON.stringify(s)) : localStorage.removeItem(SESSION_KEY); } catch (e) {}
  };
  return [session, setSession];
}

// POST to the lab server, with the session token when there is one. Resolves { ok, status, body }.
async function apiPost(path, payload, token) {
  const headers = { 'Content-Type': 'application/json' };
  if (token) headers.Authorization = 'Bearer ' + token;
  try {
    const r = await fetch(path, { method: 'POST', headers, body: JSON.stringify(payload || {}) });
    const body = await r.json().catch(() => ({}));
    return { ok: r.ok, status: r.status, body };
  } catch (e) {
    return { ok: false, status: 0, body: { error: 'offline' } };
  }
}

const AUTH_MESSAGES = {
  sign_in_required: 'Sign in to make changes',
  admin_only: 'Only an admin can change the roster',
  last_admin: 'There must always be at least one admin',
  offline: "Can't reach the lab server",
  bad_pin: 'PIN must be 4–8 digits',
  unknown_person: "That person isn't on the roster any more",
  not_visible: "That project is private and you're not a member",
  not_your_hand: 'You can only raise or lower your own hand',
};
const authMessage = (code) => AUTH_MESSAGES[code] || 'That change was refused';

function Avatar({ person, size = 20, online, title }) {
  if (!person) return null;
  return (
    <span className="nb-avatar" title={title || person.name}
      style={{ '--av-color': person.color, width: size, height: size, fontSize: Math.max(8, size * 0.42) }}>
      {personInitials(person.name)}
      {online && <span className="nb-avatar-online"/>}
    </span>
  );
}

function AvatarStack({ people, size = 18, max = 3, onlineIds }) {
  if (!people || people.length === 0) return null;
  const shown = people.slice(0, max);
  const extra = people.length - shown.length;
  return (
    <span className="nb-avatar-stack" title={people.map(p => p.name).join(', ')}>
      {shown.map(p => <Avatar key={p.id} person={p} size={size} online={onlineIds && onlineIds.has(p.id)}/>)}
      {extra > 0 && <span className="nb-avatar nb-avatar-more" style={{ width: size, height: size, fontSize: Math.max(8, size * 0.42) }}>+{extra}</span>}
    </span>
  );
}

// Toggle-chip picker used in the task panel.
function AssigneePicker({ people, value, onChange, meId }) {
  const selected = new Set(value || []);
  const toggle = (id) => onChange(selected.has(id) ? value.filter(x => x !== id) : [...(value || []), id]);
  if (!people || people.length === 0) {
    return <div className="nb-mono" style={{ fontSize: 9, color: 'var(--ink-3)' }}>no people yet — add the lab roster from the people menu in the top bar</div>;
  }
  return (
    <div className="nb-assignee-row">
      {people.map(p => (
        <button key={p.id} type="button"
          className={`nb-assignee-chip ${selected.has(p.id) ? 'is-on' : ''}`}
          style={{ '--av-color': p.color }}
          onClick={() => toggle(p.id)}>
          <Avatar person={p} size={18}/>
          <span>{p.name}{p.id === meId ? ' (me)' : ''}</span>
        </button>
      ))}
    </div>
  );
}

function ConnectionPill({ connection, presence }) {
  if (connection === 'local') {
    return <span className="nb-conn nb-conn-local" title="opened without the lab server — data lives only in this browser">○ local</span>;
  }
  if (connection === 'live') {
    const n = presence ? presence.total : 0;
    return <span className="nb-conn nb-conn-live" title="connected to the lab server — changes are shared live">● live{n > 1 ? ` · ${n} open` : ''}</span>;
  }
  if (connection === 'offline') {
    return <span className="nb-conn nb-conn-offline" title="lost the lab server — edits are kept and will be sent when it's back">⚠ offline</span>;
  }
  return <span className="nb-conn">… connecting</span>;
}

// Topbar: sign in / out, change PIN, filter by person, manage roster.
function PeopleMenu({ data, meId, isAdmin, canManage, hasPin, localMode, onSignIn, onSignOut, onChangePin,
                      personFilter, setPersonFilter, onlineIds, onManage }) {
  const [open, setOpen] = React.useState(false);
  const ref = React.useRef(null);
  const people = data.people || [];
  const me = personById(data, meId);

  React.useEffect(() => {
    if (!open) return;
    const onDown = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener('mousedown', onDown);
    return () => document.removeEventListener('mousedown', onDown);
  }, [open]);

  const filterLabel = personFilter === 'all' ? null
    : personFilter === 'me' ? 'mine'
    : personFilter === 'unassigned' ? 'unassigned'
    : personById(data, personFilter)?.name || null;

  const close = (fn) => () => { setOpen(false); fn(); };

  return (
    <div ref={ref} style={{ position: 'relative', display: 'inline-flex' }}>
      <button className={`nb-topbar-btn nb-people-btn ${!me ? 'needs-me' : ''} ${filterLabel ? 'is-filtered' : ''}`}
        onClick={() => setOpen(o => !o)} title={me ? 'signed in · filter by person' : 'sign in to edit · filter by person'}>
        {me ? <Avatar person={me} size={16}/> : <span style={{ fontSize: 11 }}>🔒</span>}
        <span className="nb-people-btn-label">{me ? me.name : 'sign in'}</span>
        {filterLabel && <span className="nb-people-btn-filter">· {filterLabel}</span>}
      </button>

      {open && (
        <div className="sync-menu nb-people-menu">
          {me ? (
            <>
              <div className="sync-menu-status">
                <Avatar person={me} size={26}/>
                <div>
                  <div className="sync-menu-filename">{me.name}</div>
                  <div className="sync-menu-substatus">signed in{isAdmin ? ' · admin' : ''}</div>
                </div>
              </div>
              <div className="sync-menu-section">
                {!localMode && (
                  <button className="sync-menu-item" onClick={close(onChangePin)}>
                    <span className="sync-menu-glyph">⚿</span><span>Change my PIN…</span>
                  </button>
                )}
                <button className="sync-menu-item subtle" onClick={close(onSignOut)}>
                  <span className="sync-menu-glyph">⏻</span><span>Sign out</span>
                </button>
              </div>
            </>
          ) : (
            <>
              <div className="sync-menu-heading">sign in as</div>
              <div className="sync-menu-section">
                {people.map(p => (
                  <button key={p.id} className="sync-menu-item" onClick={close(() => onSignIn(p.id))}>
                    <Avatar person={p} size={18} online={onlineIds.has(p.id)}/>
                    <span>{p.name}</span>
                    <span className="sync-menu-hint">{localMode ? '' : hasPin.has(p.id) ? 'PIN' : 'set up PIN'}</span>
                  </button>
                ))}
                {people.length === 0 && <div className="nb-people-empty">no one on the roster yet</div>}
              </div>
            </>
          )}

          <div className="sync-menu-divider"/>
          <div className="sync-menu-heading">show tasks for</div>
          <div className="nb-people-filter-grid">
            {[
              { id: 'all', label: 'everyone' },
              { id: 'me', label: 'me', disabled: !meId },
              { id: 'unassigned', label: 'unassigned' },
              ...people.map(p => ({ id: p.id, label: p.name, person: p })),
            ].map(f => (
              <button key={f.id} disabled={f.disabled}
                className={`nb-filter-chip ${personFilter === f.id ? 'is-on' : ''}`}
                onClick={() => setPersonFilter(f.id)}>
                {f.person && <Avatar person={f.person} size={14}/>}
                {f.label}
              </button>
            ))}
          </div>

          <div className="sync-menu-divider"/>
          <div className="sync-menu-section">
            <button className="sync-menu-item" onClick={close(onManage)}>
              <span className="sync-menu-glyph">✎</span>
              <span>{canManage ? 'Manage people…' : 'View roster…'}</span>
              <span className="sync-menu-hint">{canManage ? `${people.length} on roster` : 'admins can edit'}</span>
            </button>
          </div>
        </div>
      )}
    </div>
  );
}

// Topbar ✋: raise/lower your hand with an optional note; lists everyone whose hand is up.
function HandButton({ data, meId, isAdmin, onRaise, onLower, onSignIn }) {
  const [open, setOpen] = React.useState(false);
  const [text, setText] = React.useState('');
  const [askIds, setAskIds] = React.useState([]);   // [] = anyone
  const ref = React.useRef(null);
  const hands = (data.hands || [])
    .filter(h => personById(data, h.personId))
    .sort((a, b) => a.raisedAt - b.raisedAt);
  const mine = meId ? hands.find(h => h.personId === meId) : null;
  const askingMe = meId ? hands.filter(h => h.personId !== meId && (h.askIds || []).includes(meId)) : [];
  const others = (data.people || []).filter(p => p.id !== meId);
  const toggleAsk = (id) => setAskIds(a => a.includes(id) ? a.filter(x => x !== id) : [...a, id]);

  React.useEffect(() => {
    if (!open) return;
    setText(mine ? mine.text || '' : '');
    setAskIds(mine ? (mine.askIds || []).filter(id => personById(data, id)) : []);
    const onDown = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener('mousedown', onDown);
    return () => document.removeEventListener('mousedown', onDown);
  }, [open]);

  const raise = () => { onRaise(text, askIds); setOpen(false); };

  return (
    <div ref={ref} style={{ position: 'relative', display: 'inline-flex' }}>
      <button className={`nb-topbar-btn nb-hand-btn ${mine ? 'is-up' : ''} ${hands.length ? 'has-hands' : ''} ${askingMe.length ? 'asks-me' : ''}`}
        onClick={() => setOpen(o => !o)}
        title={askingMe.length ? `${askingMe.map(h => personById(data, h.personId).name).join(', ')} asked for your help`
          : mine ? 'your hand is up' : hands.length ? `${hands.length} hand${hands.length === 1 ? '' : 's'} up` : 'raise your hand'}>
        ✋{hands.length > 0 && <span className="nb-hand-count">{hands.length}</span>}
      </button>

      {open && (
        <div className="sync-menu nb-hand-menu">
          {meId ? (
            <div className="nb-hand-form">
              <div className="sync-menu-heading">{mine ? 'your hand is up' : 'raise your hand'}</div>
              <textarea className="nb-textarea nb-hand-text" rows={2} autoFocus
                placeholder="what do you need? (optional)"
                value={text} onChange={e => setText(e.target.value)}
                onKeyDown={e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); raise(); } }}/>
              <div className="nb-hand-ask-label">help from</div>
              <div className="nb-people-filter-grid nb-hand-ask">
                <button className={`nb-filter-chip ${askIds.length === 0 ? 'is-on' : ''}`} onClick={() => setAskIds([])}>anyone</button>
                {others.map(p => (
                  <button key={p.id} className={`nb-filter-chip ${askIds.includes(p.id) ? 'is-on' : ''}`} onClick={() => toggleAsk(p.id)}>
                    <Avatar person={p} size={14}/>{p.name}
                  </button>
                ))}
              </div>
              <div style={{ display: 'flex', gap: 6, justifyContent: 'flex-end' }}>
                {mine && <button className="nb-act" onClick={() => { onLower(meId); setOpen(false); }}>Lower hand</button>}
                <button className="nb-act primary" onClick={raise}>{mine ? 'Update' : '✋ Raise hand'}</button>
              </div>
            </div>
          ) : (
            <div className="sync-menu-section">
              <button className="sync-menu-item" onClick={() => { setOpen(false); onSignIn(); }}>
                <span className="sync-menu-glyph">🔒</span><span>Sign in to raise your hand</span>
              </button>
            </div>
          )}

          <div className="sync-menu-divider"/>
          <div className="sync-menu-heading">hands up</div>
          {hands.length === 0 && <div className="nb-people-empty">nobody right now</div>}
          {hands.map(h => {
            const p = personById(data, h.personId);
            return (
              <div key={h.id} className="nb-hand-row">
                <Avatar person={p} size={22}/>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div className="nb-hand-name">{p.name} <span className="nb-mono nb-hand-when">{fmtRelative(h.raisedAt)}</span></div>
                  {h.text && <div className="nb-hand-note">{h.text}</div>}
                  {handAsks(h, data).length > 0 && (
                    <div className={`nb-hand-asks ${(h.askIds || []).includes(meId) ? 'is-me' : ''}`}>
                      → {handAsks(h, data).map(p => p.id === meId ? 'you' : p.name).join(', ')}
                    </div>
                  )}
                </div>
                {(isAdmin || h.personId === meId) && (
                  <button className="nb-blocker-remove" title="lower hand" onClick={() => onLower(h.personId)}>×</button>
                )}
              </div>
            );
          })}
        </div>
      )}
    </div>
  );
}

// PIN entry box: digits only, masked, 4–8 long.
function PinInput({ value, onChange, autoFocus, placeholder, onEnter }) {
  return (
    <input type="password" inputMode="numeric" autoComplete="off" maxLength={8}
      className="nb-input nb-pin-input" placeholder={placeholder || '••••'} autoFocus={autoFocus}
      value={value} onChange={e => onChange(e.target.value.replace(/\D/g, '').slice(0, 8))}
      onKeyDown={e => { if (e.key === 'Enter' && onEnter) onEnter(); }}/>
  );
}

// mode 'signin': pick a person (if not given) → enter PIN, or create one if they have none.
// mode 'change': signed-in person changes their PIN.
function SignInDialog({ data, hasPin, mode, personId: initialPersonId, token, onDone, onClose }) {
  const [personId, setPersonId] = React.useState(initialPersonId || null);
  const [pin, setPin] = React.useState('');
  const [pin2, setPin2] = React.useState('');
  const [currentPin, setCurrentPin] = React.useState('');
  const [error, setError] = React.useState(null);
  const [busy, setBusy] = React.useState(false);
  const [forceCreate, setForceCreate] = React.useState(false);
  const person = personById(data, personId);
  const creating = mode === 'signin' && person && (!hasPin.has(person.id) || forceCreate);

  React.useEffect(() => {
    const onKey = (e) => { if (e.key === 'Escape') onClose(); };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [onClose]);

  const fail = (body) => {
    setBusy(false);
    if (body.error === 'wrong_pin') setError(`Wrong PIN — ${body.attemptsLeft} tr${body.attemptsLeft === 1 ? 'y' : 'ies'} left before a short lockout`);
    else if (body.error === 'locked') setError(`Too many wrong PINs. Try again in ${body.retryIn}s.`);
    else if (body.error === 'no_pin') { setForceCreate(true); setError('No PIN set yet — create one below.'); }
    else setError(authMessage(body.error));
  };

  const submit = async () => {
    if (busy) return;
    setError(null);
    if (mode === 'change' || creating) {
      if (pin.length < 4) return setError('PIN must be 4–8 digits');
      if (pin !== pin2) return setError("The two PINs don't match");
    } else if (pin.length < 4) return setError('Enter your PIN');
    setBusy(true);
    let r;
    if (mode === 'change') r = await apiPost('api/pin', { personId, pin, currentPin }, token);
    else if (creating) r = await apiPost('api/pin', { personId, pin });
    else r = await apiPost('api/login', { personId, pin });
    if (!r.ok) return fail(r.body);
    onDone({ token: r.body.token, personId: r.body.personId });
  };

  const people = data.people || [];

  return (
    <div className="nb-help-overlay" onClick={(e) => { if (e.target.classList.contains('nb-help-overlay')) onClose(); }}>
      <div className="nb-help-modal nb-signin-modal">
        <div className="nb-help-header">
          <div style={{ fontFamily: 'var(--hand)', fontSize: 28, color: 'var(--ink)' }}>
            {mode === 'change' ? 'Change PIN' : creating ? 'Create your PIN' : 'Sign in'}
          </div>
          <button className="nb-help-close" onClick={onClose} title="close (Esc)">✕</button>
        </div>

        {!person ? (
          <div className="nb-signin-people">
            {people.map(p => (
              <button key={p.id} className="nb-signin-person" onClick={() => { setPersonId(p.id); setError(null); }}>
                <Avatar person={p} size={34}/>
                <span>{p.name}</span>
                {!hasPin.has(p.id) && <span className="nb-mono nb-signin-hint">set up PIN</span>}
              </button>
            ))}
            {people.length === 0 && <div className="nb-people-empty">No one on the roster yet — add people first.</div>}
          </div>
        ) : (
          <>
            <div className="nb-signin-who">
              <Avatar person={person} size={40}/>
              <div style={{ flex: 1 }}>
                <div style={{ fontFamily: 'var(--hand)', fontSize: 24 }}>{person.name}</div>
                {creating && <div className="nb-mono" style={{ fontSize: 9, color: 'var(--ink-3)' }}>first time — choose a 4–8 digit PIN you'll use to sign in</div>}
              </div>
              {mode === 'signin' && !initialPersonId && (
                <button className="nb-act" onClick={() => { setPersonId(null); setPin(''); setPin2(''); setError(null); setForceCreate(false); }}>not me</button>
              )}
            </div>

            {mode === 'change' && (
              <div className="nb-field">
                <div className="nb-field-label">Current PIN</div>
                <PinInput value={currentPin} onChange={setCurrentPin} autoFocus/>
              </div>
            )}
            <div className="nb-field">
              <div className="nb-field-label">{mode === 'change' ? 'New PIN' : creating ? 'New PIN' : 'PIN'}</div>
              <PinInput value={pin} onChange={setPin} autoFocus={mode !== 'change'}
                onEnter={mode === 'change' || creating ? undefined : submit}/>
            </div>
            {(mode === 'change' || creating) && (
              <div className="nb-field">
                <div className="nb-field-label">Repeat PIN</div>
                <PinInput value={pin2} onChange={setPin2} onEnter={submit}/>
              </div>
            )}

            {error && <div className="nb-signin-error">{error}</div>}

            <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 14 }}>
              <button className="nb-act" onClick={onClose}>Cancel</button>
              <button className="nb-act primary" onClick={submit} disabled={busy}>
                {busy ? '…' : mode === 'change' ? 'Change PIN' : creating ? 'Create PIN & sign in' : 'Sign in'}
              </button>
            </div>
          </>
        )}
      </div>
    </div>
  );
}

// Modal roster editor. canManage = signed-in admin, or first-run setup (no PINs yet).
function PeopleManager({ data, meId, canManage, hasPin, setup, localMode, onSavePerson, onRemovePerson, onResetPin, onClose }) {
  const [newName, setNewName] = React.useState('');
  const people = data.people || [];
  const adminCount = people.filter(p => p.admin).length;
  const openCount = (id) => data.tasks.filter(t => !t.archived && t.status !== 'done' && (t.assignees || []).includes(id)).length;

  React.useEffect(() => {
    const onKey = (e) => { if (e.key === 'Escape') onClose(); };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [onClose]);

  const add = () => {
    const name = newName.trim();
    if (!name) return;
    const used = new Set(people.map(p => p.color));
    const color = PERSON_PALETTE.find(c => !used.has(c)) || PERSON_PALETTE[people.length % PERSON_PALETTE.length];
    onSavePerson({ id: 'person_' + Math.random().toString(36).slice(2, 11), name, color, createdAt: Date.now() });
    setNewName('');
  };

  const remove = (p) => {
    const n = openCount(p.id);
    const msg = n > 0
      ? `Remove ${p.name}? They'll be unassigned from ${n} open task${n === 1 ? '' : 's'}.`
      : `Remove ${p.name} from the roster?`;
    if (window.confirm(msg)) onRemovePerson(p.id);
  };

  const resetPin = (p) => {
    if (window.confirm(`Clear ${p.name}'s PIN? They'll be signed out everywhere and asked to create a new PIN next time.`)) onResetPin(p.id);
  };

  return (
    <div className="nb-help-overlay" onClick={(e) => { if (e.target.classList.contains('nb-help-overlay')) onClose(); }}>
      <div className="nb-help-modal" style={{ maxWidth: 640 }}>
        <div className="nb-help-header">
          <div style={{ fontFamily: 'var(--hand)', fontSize: 28, color: 'var(--ink)' }}>Lab roster</div>
          <button className="nb-help-close" onClick={onClose} title="close (Esc)">✕</button>
        </div>

        {setup && !localMode && (
          <div className="nb-conflict-note">
            Setup: add everyone, then each person signs in from the top bar and creates their PIN.
            The first person to create a PIN becomes admin, and from then on only admins can change this roster.
          </div>
        )}
        {!canManage && (
          <div className="nb-mono" style={{ fontSize: 9, color: 'var(--ink-3)', marginBottom: 10 }}>
            read-only — ask an admin to add people or reset a PIN
          </div>
        )}

        <fieldset disabled={!canManage} className="nb-fieldset">
          <div className="nb-roster">
            {people.map(p => (
              <div key={p.id} className="nb-roster-row">
                <Avatar person={p} size={28}/>
                <input className="nb-input" value={p.name}
                  onChange={e => onSavePerson({ ...p, name: e.target.value })}/>
                <button className="nb-blocker-remove" onClick={() => remove(p)} title="remove from roster">×</button>
                <div className="nb-roster-meta">
                  <div className="nb-roster-swatches">
                    {PERSON_PALETTE.map(c => (
                      <button key={c} className={`nb-swatch ${p.color === c ? 'is-on' : ''}`}
                        style={{ background: c }} onClick={() => onSavePerson({ ...p, color: c })} title={c}/>
                    ))}
                  </div>
                  <span style={{ flex: 1 }}/>
                  {!localMode && (
                    <>
                      <button className={`nb-admin-toggle ${p.admin ? 'is-on' : ''}`}
                        disabled={p.admin && adminCount <= 1 && !setup}
                        title={p.admin ? (adminCount <= 1 ? 'the last admin can’t be removed' : 'remove admin') : 'make admin'}
                        onClick={() => onSavePerson({ ...p, admin: !p.admin })}>
                        {p.admin ? '★ admin' : '☆ admin'}
                      </button>
                      {hasPin.has(p.id)
                        ? <button className="nb-act nb-roster-pin" disabled={setup} onClick={() => resetPin(p)} title="clear this person's PIN">reset PIN</button>
                        : <span className="nb-mono nb-roster-pinstate" title="will create a PIN at first sign-in">no PIN yet</span>}
                    </>
                  )}
                  <span className="nb-mono nb-roster-count">{openCount(p.id)} open{p.id === meId ? ' · me' : ''}</span>
                </div>
              </div>
            ))}
            {people.length === 0 && (
              <div style={{ fontFamily: 'var(--hand)', fontSize: 18, color: 'var(--ink-3)', padding: '8px 0' }}>
                Add everyone who works on the board.
              </div>
            )}
          </div>

          <div style={{ display: 'flex', gap: 8, marginTop: 16 }}>
            <input className="nb-input" placeholder="name…" value={newName} autoFocus={canManage}
              onChange={e => setNewName(e.target.value)}
              onKeyDown={e => { if (e.key === 'Enter') add(); }}/>
            <button className="nb-act primary" onClick={add} disabled={!newName.trim()}>Add</button>
          </div>
        </fieldset>
      </div>
    </div>
  );
}

Object.assign(window, {
  useSession, apiPost, authMessage, Avatar, AvatarStack, AssigneePicker, ConnectionPill,
  PeopleMenu, SignInDialog, PeopleManager, HandButton,
});
