/* global React, Icon, Avatar, Badge, Linkify, ModalOverlay, PageHeader, SectionHead, apiGet, apiPost, apiPatch, apiDelete, activeMembers, PresentationOrder */

const { useState, useEffect, useMemo } = React;

const TITLE_MAX = 140;
const BODY_MAX = 1000;

const FIELD_TYPES = [
  { value: 'short_text', label: 'Short text' },
  { value: 'long_text', label: 'Long text' },
  { value: 'single_choice', label: 'Single choice' },
  { value: 'multi_choice', label: 'Multiple choice' },
];
const isChoiceType = (type) => type === 'single_choice' || type === 'multi_choice';
// True when a respondent actually answered a field. Optional fields are simply
// absent from the stored answers (see validateAnswers in server.js).
const hasAnswer = (v) => (Array.isArray(v) ? v.length > 0 : v != null && String(v).trim() !== '');

// --- Form field builder ------------------------------------------------------
// Shared by the composer (a form not yet sent) and the post-send editor, so the two
// can never drift. See tasks/edit-sent-notifications.md.

// The next field id that cannot collide with one already in the schema. Field ids are
// load-bearing once a form has been sent — notification_responses.answers is keyed by
// them — so a new field must never reuse an id whose answers are already stored.
const nextFieldId = (fields) => {
  const highest = fields.reduce((max, f) => {
    const match = /^f(\d+)$/.exec(String(f.id || ''));
    return match ? Math.max(max, Number(match[1])) : max;
  }, 0);
  return `f${highest + 1}`;
};

const blankField = (fields) => (
  { id: nextFieldId(fields), type: 'short_text', label: '', required: true, options: ['', ''] }
);

// A form is sendable when every field has a label and choice fields have >= 2 non-empty
// options. Mirrors the server's validateFormSchema so we never submit a 400.
const isFormValid = (fields) => fields.length > 0 && fields.every((f) => {
  if (!String(f.label || '').trim()) return false;
  if (isChoiceType(f.type)) return f.options.map((o) => o.trim()).filter(Boolean).length >= 2;
  return true;
});

// Strip blank options + trim before sending; the server re-validates.
const buildFormSchema = (fields) => fields.map((f) => {
  const field = { id: f.id, type: f.type, label: f.label.trim(), required: f.required };
  if (isChoiceType(f.type)) field.options = f.options.map((o) => o.trim()).filter(Boolean);
  return field;
});

// The builder always renders at least two option inputs, so a stored choice field with
// fewer (or a text field, which has none) gets padded out to two.
const builderOptions = (field) => {
  const stored = isChoiceType(field.type) ? (field.options || []) : [];
  return stored.length >= 2 ? [...stored] : [...stored, '', ''].slice(0, 2);
};

// Inflate a stored form_schema into builder state. Every field keeps its stored id —
// that is what already-collected answers are keyed by.
const fieldsFromSchema = (schema) => (Array.isArray(schema) ? schema : []).map((f) => ({
  id: f.id,
  type: f.type,
  label: f.label || '',
  required: f.required === true,
  options: builderOptions(f),
}));

// Controlled editor for a form's field list. Owns only the drag cursor; every edit is
// handed back through onChange so the parent stays the single source of truth.
function FormFieldBuilder({ fields, onChange }) {
  const [dragId, setDragId] = useState(null);

  const updateField = (id, patch) =>
    onChange(fields.map((f) => (f.id === id ? { ...f, ...patch } : f)));
  const removeField = (id) => onChange(fields.filter((f) => f.id !== id));
  const setOption = (id, index, value) =>
    onChange(fields.map((f) =>
      f.id === id ? { ...f, options: f.options.map((o, i) => (i === index ? value : o)) } : f));
  const addOption = (id) =>
    onChange(fields.map((f) => (f.id === id ? { ...f, options: [...f.options, ''] } : f)));
  const removeOption = (id, index) =>
    onChange(fields.map((f) =>
      f.id === id ? { ...f, options: f.options.filter((_, i) => i !== index) } : f));

  // Drag-to-reorder: drop `fromId` into `toId`'s slot (insert before it).
  const moveField = (fromId, toId) => {
    if (!fromId || fromId === toId) return;
    const from = fields.findIndex((f) => f.id === fromId);
    const to = fields.findIndex((f) => f.id === toId);
    if (from < 0 || to < 0) return;
    const next = [...fields];
    const [moved] = next.splice(from, 1);
    next.splice(to, 0, moved);
    onChange(next);
  };

  return (
    <div className="field-builder">
      {fields.length === 0 && (
        <div className="field-builder-empty">
          <Icon name="edit" size={16}/>
          <span>No questions yet — add your first field below.</span>
        </div>
      )}

      {fields.map((f, idx) => {
        const choice = isChoiceType(f.type);
        return (
          <div key={f.id}
            className={`field-card${dragId && dragId !== f.id ? ' is-drop' : ''}${dragId === f.id ? ' is-dragging' : ''}`}
            onDragOver={(e) => { if (dragId) e.preventDefault(); }}
            onDrop={(e) => { e.preventDefault(); moveField(dragId, f.id); setDragId(null); }}>
            <div className="field-card-head">
              <span className="field-handle" draggable title="Drag to reorder"
                onDragStart={(e) => { e.dataTransfer.effectAllowed = 'move'; setDragId(f.id); }}
                onDragEnd={() => setDragId(null)}>
                <Icon name="grip" size={14}/>
              </span>
              <span className="field-card-num">Field {idx + 1}</span>
              <button type="button" className="btn-icon" title="Remove field"
                onClick={() => removeField(f.id)}>
                <Icon name="trash" size={13}/>
              </button>
            </div>

            <input className="input field-card-label" value={f.label}
              placeholder="Question — e.g. What's your t-shirt size?"
              onChange={(e) => updateField(f.id, { label: e.target.value })}/>

            <div className="field-card-meta">
              <label className="field-type-select">
                <span className="micro-label">Type</span>
                <select className="input" value={f.type}
                  onChange={(e) => updateField(f.id, { type: e.target.value })}>
                  {FIELD_TYPES.map((t) => <option key={t.value} value={t.value}>{t.label}</option>)}
                </select>
              </label>
              <label className="field-required-toggle">
                <input type="checkbox" checked={f.required}
                  onChange={(e) => updateField(f.id, { required: e.target.checked })}/>
                <span>Required</span>
              </label>
            </div>

            {choice && (
              <div className="ff-options">
                {f.options.map((opt, i) => (
                  <div key={i} className="ff-option">
                    <span className={`ff-bullet ${f.type === 'multi_choice' ? 'ff-bullet--square' : 'ff-bullet--radio'}`}/>
                    <input className="input" placeholder={`Option ${i + 1}`} value={opt}
                      onChange={(e) => setOption(f.id, i, e.target.value)}/>
                    <button type="button" className="btn-icon" title="Remove option"
                      disabled={f.options.length <= 2} onClick={() => removeOption(f.id, i)}>
                      <Icon name="x" size={11}/>
                    </button>
                  </div>
                ))}
                <button type="button" className="linklike ff-add-option" onClick={() => addOption(f.id)}>
                  <Icon name="plus" size={11}/> Add option
                </button>
              </div>
            )}
          </div>
        );
      })}

      <button type="button" className="btn-add-field" onClick={() => onChange([...fields, blankField(fields)])}>
        <Icon name="plus" size={14}/> Add field
      </button>

      {!isFormValid(fields) && fields.length > 0 && (
        <div className="field-builder-hint">
          Each field needs a label, and choice fields need at least two options.
        </div>
      )}
    </div>
  );
}


// Post-send editor. Title and body for any notification; for a form, the whole field
// list through the shared builder above. PATCHes /api/notifications/:id and hands the
// updated row back so the caller can refresh its own copy. Deliberately does not touch
// the audience or re-send anything — see tasks/edit-sent-notifications.md.
function EditNotificationModal({ notification, responseCount = 0, onClose, onSaved }) {
  const [title, setTitle] = useState(notification.title || '');
  const [body, setBody] = useState(notification.body || '');
  const [fields, setFields] = useState(() => fieldsFromSchema(notification.formSchema));
  const [saving, setSaving] = useState(false);
  const [error, setError] = useState('');

  const isForm = notification.kind === 'form';
  const canSave = Boolean(title.trim()) && Boolean(body.trim())
    && (!isForm || isFormValid(fields)) && !saving;

  const save = () => {
    if (!canSave) return;
    setSaving(true);
    setError('');
    apiPatch(`/api/notifications/${encodeURIComponent(notification.id)}`, {
      title: title.trim(),
      body: body.trim(),
      formSchema: isForm ? buildFormSchema(fields) : null,
    })
      .then((data) => onSaved(data.notification))
      .catch((err) => setError(err.message || 'Could not save the changes.'))
      .finally(() => setSaving(false));
  };

  return (
    <ModalOverlay onClose={onClose}>
      <div className="modal" onClick={(e) => e.stopPropagation()} style={{ maxWidth: 620 }}>
        <div className="row between" style={{ alignItems: 'flex-start', marginBottom: 18 }}>
          <div style={{ minWidth: 0 }}>
            <div className="page-eyebrow">Already sent</div>
            <h2 className="section-title" style={{ marginTop: 4 }}>
              Edit this {isForm ? 'form' : 'message'}
            </h2>
            <div className="muted" style={{ fontSize: 12.5, marginTop: 6, lineHeight: 1.55 }}>
              Recipients see the update in the bell{isForm ? ' and on the respond page' : ''}.
              Slack DMs that already went out keep the original wording — use
              <strong> Send reminder</strong> if people need to look again.
            </div>
          </div>
          <button type="button" className="btn-icon" onClick={onClose}><Icon name="x" size={13}/></button>
        </div>

        {isForm && responseCount > 0 && (
          <div className="edit-warning">
            <Icon name="help" size={14}/>
            <span>
              <strong>{responseCount}</strong> {responseCount === 1 ? 'person has' : 'people have'} already
              answered. Editing a question keeps their answers attached to it; deleting a field or an
              option hides those answers from the tallies.
            </span>
          </div>
        )}

        <div style={{ display: 'grid', gap: 16 }}>
          <label className="field">
            <div className="field-row">
              <span className="field-label">Title</span>
              <span className={`char-count ${title.length > TITLE_MAX ? 'over' : ''}`}>
                {title.length}/{TITLE_MAX}
              </span>
            </div>
            <input className="input" value={title} maxLength={TITLE_MAX}
              onChange={(e) => setTitle(e.target.value)}/>
          </label>

          <label className="field">
            <div className="field-row">
              <span className="field-label">{isForm ? 'Form intro' : 'Body'}</span>
              <span className={`char-count ${body.length > BODY_MAX ? 'over' : ''}`}>
                {body.length}/{BODY_MAX}
              </span>
            </div>
            <textarea className="input" value={body} rows={4} maxLength={BODY_MAX}
              onChange={(e) => setBody(e.target.value)}
              style={{ resize: 'vertical', fontFamily: 'inherit', lineHeight: 1.5 }}/>
          </label>

          {isForm && (
            <div className="field">
              <div className="field-row">
                <span className="field-label">Form fields</span>
                <span className="compose-section-hint">
                  {fields.length} field{fields.length === 1 ? '' : 's'} · drag to reorder
                </span>
              </div>
              <FormFieldBuilder fields={fields} onChange={setFields}/>
            </div>
          )}
        </div>

        {error && <div className="form-error" style={{ marginTop: 14 }}>{error}</div>}

        <div className="row" style={{ justifyContent: 'flex-end', gap: 8, marginTop: 20 }}>
          <button type="button" className="btn btn-ghost" onClick={onClose}>Cancel</button>
          <button type="button" className="btn btn-primary" onClick={save} disabled={!canSave}>
            <Icon name="check" size={13}/> {saving ? 'Saving…' : 'Save changes'}
          </button>
        </div>
      </div>
    </ModalOverlay>
  );
}

function SlackStatusBadge({ status }) {
  const map = {
    sent: { tone: 'success', label: 'Slack sent' },
    partial: { tone: 'cream', label: 'Slack partial' },
    failed: { tone: 'coral', label: 'Slack failed' },
    skipped: { tone: 'mist', label: 'Slack skipped' },
    pending: { tone: 'cream', label: 'Slack sending…' },
    disabled: { tone: 'mist', label: 'No Slack' },
  };
  const item = map[status] || map.pending;
  return <Badge tone={item.tone}>{item.label}</Badge>;
}

// Per-recipient Slack delivery (notification_recipients.slack_delivery), distinct
// from the notification-level roll-up above.
function SlackDeliveryBadge({ status }) {
  const map = {
    sent: { tone: 'success', label: 'Sent' },
    skipped: { tone: 'mist', label: 'Skipped' },
    failed: { tone: 'coral', label: 'Failed' },
    pending: { tone: 'cream', label: 'Sending…' },
    disabled: { tone: 'mist', label: '—' },
  };
  const item = map[status] || map.pending;
  return <Badge tone={item.tone}>{item.label}</Badge>;
}

const fmtWhen = (iso) =>
  iso ? new Date(iso).toLocaleString(undefined,
    { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }) : '';

const answerText = (value) =>
  Array.isArray(value) ? value.join(', ') : (value == null ? '' : String(value));

// Activity view: read receipts for any notification + collected answers for forms.
// Master–detail over the admin notification history (passed in) backed by
// GET /api/notifications/:id/activity. Read-only — see tasks/notification-activity-view.md.
function NotificationActivity({ notifications, onChanged }) {
  const [selectedId, setSelectedId] = useState(null);
  const [detail, setDetail] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [reloadKey, setReloadKey] = useState(0);
  // Reminder ("bump") controls for the selected notification.
  const [bumpOpen, setBumpOpen] = useState(false);
  const [bumpAudience, setBumpAudience] = useState('pending'); // 'pending' | 'all'
  const [bumping, setBumping] = useState(false);
  const [bumpFeedback, setBumpFeedback] = useState(null);
  // True while the post-send editor is open for the selected notification.
  const [editing, setEditing] = useState(false);

  // Default to the most recent notification once history arrives.
  useEffect(() => {
    if (!selectedId && notifications.length) setSelectedId(notifications[0].id);
  }, [notifications, selectedId]);

  // Reset the reminder UI when switching notifications.
  useEffect(() => {
    setBumpOpen(false);
    setBumpFeedback(null);
    setBumpAudience('pending');
    setEditing(false);
  }, [selectedId]);

  useEffect(() => {
    if (!selectedId) { setDetail(null); return; }
    let cancelled = false;
    setLoading(true);
    setError(null);
    apiGet(`/api/notifications/${encodeURIComponent(selectedId)}/activity`)
      .then((data) => { if (!cancelled) setDetail(data); })
      .catch((err) => { if (!cancelled) setError(err.message || 'Could not load activity.'); })
      .finally(() => { if (!cancelled) setLoading(false); });
    return () => { cancelled = true; };
  }, [selectedId, reloadKey]);

  const notification = detail?.notification;
  const recipients = detail?.recipients || [];
  const isForm = notification?.kind === 'form';
  const fields = notification?.formSchema || [];

  const seenCount = recipients.filter((r) => r.readAt).length;
  const responders = recipients.filter((r) => r.answers != null);
  // Who still needs a nudge: forms → not responded; messages → not seen.
  const pendingCount = recipients.filter((r) => (isForm ? r.answers == null : !r.readAt)).length;
  const pendingLabel = isForm ? "haven't responded" : "haven't seen it";

  const sendReminder = () => {
    setBumping(true);
    setBumpFeedback(null);
    apiPost(`/api/notifications/${encodeURIComponent(selectedId)}/bump`, { audience: bumpAudience })
      .then((data) => {
        setBumpFeedback({
          tone: 'success',
          text: data.sent === 0
            ? 'Nobody to remind — everyone is caught up.'
            : `Reminder sending to ${data.sent} ${data.sent === 1 ? 'person' : 'people'} via Slack.`,
        });
        setBumpOpen(false);
        setReloadKey((k) => k + 1);
      })
      .catch((err) => setBumpFeedback({ tone: 'error', text: err.message || 'Could not send the reminder.' }))
      .finally(() => setBumping(false));
  };

  if (notifications.length === 0) {
    return (
      <div className="card" style={{ padding: 24 }}>
        <div className="muted">No notifications sent yet — once you send one, its read receipts and responses show up here.</div>
      </div>
    );
  }

  return (
    <div className="activity-grid">
      {/* --- Notification list ----------------------------------------- */}
      <div className="card activity-list" style={{ padding: 6 }}>
        {notifications.map((n) => (
          <button
            key={n.id}
            type="button"
            className={`activity-pick${n.id === selectedId ? ' active' : ''}`}
            onClick={() => setSelectedId(n.id)}
            style={{
              display: 'block', width: '100%', textAlign: 'left', border: 'none',
              background: n.id === selectedId ? 'var(--cream-100)' : 'transparent',
              borderRadius: 8, padding: '10px 12px', cursor: 'pointer',
            }}
          >
            <div className="row" style={{ gap: 6, alignItems: 'center' }}>
              <span className="wrap-anywhere" style={{ fontWeight: 600, color: 'var(--steel-900)', fontSize: 13 }}>{n.title}</span>
              {n.kind === 'form' && <Badge tone="steel">Form</Badge>}
            </div>
            <div className="muted" style={{ fontSize: 11.5, marginTop: 3 }}>
              {n.readCount}/{n.recipientCount} seen · {fmtWhen(n.createdAt)}
            </div>
          </button>
        ))}
      </div>

      {/* --- Detail ----------------------------------------------------- */}
      <div style={{ display: 'grid', gap: 20 }}>
        {loading && <div className="card" style={{ padding: 24 }}><div className="muted">Loading…</div></div>}
        {error && <div className="card" style={{ padding: 24 }}><div className="form-error">{error}</div></div>}

        {!loading && !error && notification && (
          <>
            <div className="card" style={{ display: 'grid', gap: 14 }}>
              <div className="row between" style={{ gap: 8, alignItems: 'flex-start' }}>
                <div className="row" style={{ gap: 8, alignItems: 'center', minWidth: 0 }}>
                  <span className="wrap-anywhere" style={{ fontWeight: 600, fontSize: 16, color: 'var(--steel-900)' }}>{notification.title}</span>
                  <Badge tone={isForm ? 'coral-solid' : 'steel'}>{isForm ? 'Form' : 'Message'}</Badge>
                </div>
                <div className="row" style={{ gap: 6, flex: '0 0 auto' }}>
                  <button type="button" className="btn btn-ghost btn-sm" onClick={() => setEditing(true)}>
                    <Icon name="edit" size={13}/> Edit
                  </button>
                  <button type="button" className="btn btn-ghost btn-sm" onClick={() => setBumpOpen((o) => !o)}>
                    <Icon name="bell" size={13}/> Send reminder
                  </button>
                </div>
              </div>
              {notification.body && (
                <div className="muted wrap-anywhere" style={{ fontSize: 12.5, lineHeight: 1.55, whiteSpace: 'pre-wrap' }}>
                  <Linkify text={notification.body}/>
                </div>
              )}
              <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
                <Badge tone="cream">{recipients.length} recipient{recipients.length === 1 ? '' : 's'}</Badge>
                <Badge tone="cream">{seenCount}/{recipients.length} seen</Badge>
                {isForm && <Badge tone="cream">{responders.length}/{recipients.length} responded</Badge>}
                <Badge tone="mist">Sent {fmtWhen(notification.createdAt)}</Badge>
              </div>

              {/* --- Reminder ("bump") panel ---------------------------- */}
              {bumpOpen && (
                <div style={{ display: 'grid', gap: 10, padding: 14, background: 'var(--cream-100)', borderRadius: 10 }}>
                  <span className="field-label">Resend a Slack reminder to</span>
                  <label className="row" style={{ gap: 8, margin: 0, cursor: 'pointer', fontSize: 13 }}>
                    <input type="radio" name="bump-audience" checked={bumpAudience === 'pending'}
                      onChange={() => setBumpAudience('pending')}/>
                    <span>Only those who {pendingLabel} <strong>({pendingCount})</strong></span>
                  </label>
                  <label className="row" style={{ gap: 8, margin: 0, cursor: 'pointer', fontSize: 13 }}>
                    <input type="radio" name="bump-audience" checked={bumpAudience === 'all'}
                      onChange={() => setBumpAudience('all')}/>
                    <span>Everyone <strong>({recipients.length})</strong></span>
                  </label>
                  <div className="muted" style={{ fontSize: 12, lineHeight: 1.5 }}>
                    Recipients get the same notification again, clearly marked as a reminder. The in-app
                    bell entry is unchanged — this only re-sends the Slack DM.
                  </div>
                  <div className="row" style={{ justifyContent: 'flex-end', gap: 8 }}>
                    <button type="button" className="btn btn-ghost btn-sm" onClick={() => setBumpOpen(false)}>Cancel</button>
                    <button type="button" className="btn btn-primary btn-sm" onClick={sendReminder}
                      disabled={bumping || (bumpAudience === 'pending' && pendingCount === 0)}>
                      <Icon name="bell" size={13}/> {bumping ? 'Sending…' : 'Send reminder'}
                    </button>
                  </div>
                </div>
              )}

              {bumpFeedback && (
                <div className={bumpFeedback.tone === 'error' ? 'form-error' : ''}
                  style={{ fontSize: 12.5, color: bumpFeedback.tone === 'success' ? 'var(--coral-700)' : undefined }}>
                  {bumpFeedback.text}
                </div>
              )}
            </div>

            {/* --- Read receipts ---------------------------------------- */}
            <div>
              <SectionHead title="Read receipts" meta={`${seenCount} of ${recipients.length} seen`}/>
              <div className="card" style={{ padding: 0, overflow: 'hidden' }}>
                <div className="table-scroll">
                <table className="tbl">
                  <thead>
                    <tr>
                      <th>Recipient</th>
                      <th style={{ width: 170 }}>Seen</th>
                      {isForm && <th style={{ width: 120 }}>Response</th>}
                      <th style={{ width: 90 }}>Slack</th>
                    </tr>
                  </thead>
                  <tbody>
                    {recipients.map((r) => (
                      <tr key={r.email}>
                        <td>
                          <div style={{ fontWeight: 600, color: 'var(--steel-900)', fontSize: 12.5 }}>
                            {r.name || r.email}
                          </div>
                          {r.name && <div className="muted" style={{ fontSize: 11 }}>{r.email}</div>}
                        </td>
                        <td style={{ fontSize: 12.5 }}>
                          {r.readAt
                            ? <span style={{ color: 'var(--success)' }}><Icon name="check" size={12}/> {fmtWhen(r.readAt)}</span>
                            : <span className="muted">Not yet</span>}
                        </td>
                        {isForm && (
                          <td>
                            {r.answers != null
                              ? <Badge tone="success">Responded</Badge>
                              : <span className="muted" style={{ fontSize: 12.5 }}>—</span>}
                          </td>
                        )}
                        <td><SlackDeliveryBadge status={r.slackDelivery}/></td>
                      </tr>
                    ))}
                  </tbody>
                </table>
                </div>
              </div>
            </div>

            {/* --- Form responses --------------------------------------- */}
            {isForm && (
              <div>
                <SectionHead title="Responses" meta={`${responders.length} submitted`}/>
                {responders.length === 0 ? (
                  <div className="card" style={{ padding: 20 }}>
                    <div className="muted">No responses yet.</div>
                  </div>
                ) : (
                  <div style={{ display: 'grid', gap: 12 }}>
                    {/* Choice-field tallies across all responses. */}
                    {fields.some((f) => isChoiceType(f.type)) && (
                      <div className="card" style={{ display: 'grid', gap: 14 }}>
                        <div style={{ fontWeight: 600, color: 'var(--steel-900)', fontSize: 12.5 }}>Tallies</div>
                        {fields.filter((f) => isChoiceType(f.type)).map((f) => {
                          // Counts rarely sum to the submission count: an optional field can
                          // be skipped, and a multi-choice field takes several picks from one
                          // person. Both the per-field "answered" meta and the bar denominator
                          // are that field's respondents, so the numbers reconcile on sight.
                          const answered = responders.filter((r) => hasAnswer(r.answers[f.id]));
                          const counts = (f.options || []).map((opt) => ({
                            opt,
                            n: answered.reduce((acc, r) => {
                              const v = r.answers[f.id];
                              return acc + ((Array.isArray(v) ? v.includes(opt) : v === opt) ? 1 : 0);
                            }, 0),
                          }));
                          // Editing a sent form can drop an option people had already
                          // picked. Their answer is still stored, so show it as a retired
                          // row instead of letting the bars quietly stop adding up to the
                          // "answered" count claimed above them.
                          const retired = [];
                          answered.forEach((r) => {
                            const v = r.answers[f.id];
                            (Array.isArray(v) ? v : [v]).forEach((pick) => {
                              if (pick == null || (f.options || []).includes(pick)) return;
                              const seen = retired.find((x) => x.opt === pick);
                              if (seen) seen.n += 1;
                              else retired.push({ opt: pick, n: 1, retired: true });
                            });
                          });
                          const denom = Math.max(1, answered.length);
                          return (
                            <div key={f.id} style={{ display: 'grid', gap: 6 }}>
                              <div className="muted wrap-anywhere" style={{ fontSize: 12 }}>
                                {f.label}
                                <span style={{ fontSize: 11 }}>
                                  {' · '}{answered.length} of {responders.length} answered
                                  {f.type === 'multi_choice' && ' · multiple picks allowed'}
                                </span>
                              </div>
                              {counts.concat(retired).map((c) => (
                                <div key={c.opt} className="row" style={{ gap: 8, alignItems: 'flex-start' }}>
                                  <span className="wrap-anywhere" style={{ fontSize: 12, flex: '0 0 130px' }}>
                                    {c.opt}
                                    {c.retired && <span className="muted" style={{ fontSize: 10.5 }}> · removed</span>}
                                  </span>
                                  <span style={{ flex: 1, height: 8, marginTop: 5, background: 'var(--mist-100)', borderRadius: 999, overflow: 'hidden' }}>
                                    <span style={{
                                      display: 'block', height: '100%', width: `${(c.n / denom) * 100}%`,
                                      background: c.retired ? 'var(--steel-400)' : 'var(--coral-500)',
                                    }}/>
                                  </span>
                                  <span style={{ fontSize: 12, flex: '0 0 20px', textAlign: 'right' }}>{c.n}</span>
                                </div>
                              ))}
                            </div>
                          );
                        })}
                      </div>
                    )}

                    {/* One card per respondent. */}
                    {responders.map((r) => (
                      <div key={r.email} className="card" style={{ display: 'grid', gap: 10 }}>
                        <div className="row between" style={{ alignItems: 'baseline' }}>
                          <span style={{ fontWeight: 600, color: 'var(--steel-900)', fontSize: 13 }}>{r.name || r.email}</span>
                          <span className="muted" style={{ fontSize: 11 }}>{fmtWhen(r.respondedAt)}</span>
                        </div>
                        <div style={{ display: 'grid', gap: 8 }}>
                          {fields.map((f) => (
                            <div key={f.id}>
                              <div className="muted wrap-anywhere" style={{ fontSize: 11.5 }}>{f.label}</div>
                              <div className="wrap-anywhere" style={{ fontSize: 13, color: 'var(--steel-900)', whiteSpace: 'pre-wrap' }}>
                                {answerText(r.answers[f.id]) || <span className="muted">—</span>}
                              </div>
                            </div>
                          ))}
                        </div>
                      </div>
                    ))}
                  </div>
                )}
              </div>
            )}
          </>
        )}

        {editing && notification && (
          <EditNotificationModal
            notification={notification}
            responseCount={responders.length}
            onClose={() => setEditing(false)}
            onSaved={() => {
              setEditing(false);
              setReloadKey((k) => k + 1);  // re-fetch this notification's detail
              if (onChanged) onChanged();  // and the list beside it, whose title may have changed
            }}/>
        )}
      </div>
    </div>
  );
}

// Admin composer + delivery history for ad-hoc notifications. Sends an in-app
// bell entry to the chosen audience and (optionally) a Slack DM to each recipient.
// Audience is either Everyone (audienceType 'all') or an explicit set of member
// ids (audienceType 'members') built via the recipient picker below. Quick-group
// chips (active members, classes, project teams) additively toggle member ids into
// that set — see tasks/notification-system.md.
const NOTIFY_VIEWS = ['compose', 'activity', 'segments', 'smart-goals'];

// The concise, single-spot tracking view: this week's roster (submitted/missing +
// expandable answers) and overall per-member participation, with a per-cycle strip.
function StandupTracking({ tracking, latest }) {
  const [openEmail, setOpenEmail] = useState(null);
  if (!tracking) return null;
  const { cycles, members } = tracking;

  const roster = latest ? latest.roster : [];
  const fields = latest ? latest.fields : [];
  const submitted = roster.filter((r) => r.answers != null);
  const missing = roster.filter((r) => r.answers == null);

  return (
    <div style={{ display: 'grid', gap: 24 }}>
      {/* Per-cycle completion strip -------------------------------------- */}
      {cycles.length > 0 && (
        <div>
          <SectionHead title="Cycles" meta={`${cycles.length} sent`}/>
          <div className="card" style={{ display: 'flex', gap: 8, flexWrap: 'wrap', padding: 14 }}>
            {cycles.map((c) => {
              const pct = c.recipientCount ? Math.round((c.respondedCount / c.recipientCount) * 100) : 0;
              return (
                <div key={c.cycle} className="standup-cycle-chip" title={`${c.respondedCount}/${c.recipientCount} submitted`}>
                  <span className="standup-cycle-label">{c.cycle}</span>
                  <span className="standup-cycle-pct">{pct}%</span>
                </div>
              );
            })}
          </div>
        </div>
      )}

      {/* This week's roster ---------------------------------------------- */}
      {latest && (
        <div>
          <SectionHead title="This week's submissions"
            meta={`${submitted.length} submitted · ${missing.length} missing`}/>
          <div className="card standup-roster">
            {roster.map((r) => {
              const done = r.answers != null;
              const open = openEmail === r.email;
              return (
                <div key={r.email} className={`standup-row ${done ? 'done' : 'missing'}`}>
                  <button type="button" className="standup-row-head"
                    onClick={() => done && setOpenEmail(open ? null : r.email)}
                    style={{ cursor: done ? 'pointer' : 'default' }}>
                    <span className="standup-status">
                      {done
                        ? <Icon name="check" size={13}/>
                        : <span className="standup-dot"/>}
                    </span>
                    <span className="standup-name">{r.name}</span>
                    {done
                      ? <span className="muted" style={{ fontSize: 11.5 }}>{fmtWhen(r.respondedAt)}</span>
                      : <Badge tone="cream">Missing</Badge>}
                  </button>
                  {open && done && (
                    <div className="standup-answers">
                      {fields.map((f) => (
                        <div key={f.id} style={{ marginBottom: 8 }}>
                          <div className="muted" style={{ fontSize: 11.5 }}>{f.label}</div>
                          <div style={{ fontSize: 13, color: 'var(--steel-900)', whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' }}>
                            {answerText(r.answers[f.id]) || <span className="muted">—</span>}
                          </div>
                        </div>
                      ))}
                    </div>
                  )}
                </div>
              );
            })}
          </div>
        </div>
      )}

      {/* Overall participation ------------------------------------------- */}
      <div>
        <SectionHead title="Overall participation" meta={`${members.length} member${members.length === 1 ? '' : 's'}`}/>
        <div className="card" style={{ padding: 0, overflow: 'hidden' }}>
          <div className="table-scroll">
          <table className="tbl">
            <thead>
              <tr>
                <th>Member</th>
                <th style={{ width: 110 }}>This week</th>
                <th style={{ width: 110 }}>Cycles</th>
                <th style={{ width: 130 }}>Last submitted</th>
              </tr>
            </thead>
            <tbody>
              {members.length === 0 ? (
                <tr><td colSpan={4} className="muted" style={{ textAlign: 'center', padding: 20 }}>
                  No cycles sent yet.
                </td></tr>
              ) : members.map((m) => (
                <tr key={m.email}>
                  <td>
                    <div style={{ fontWeight: 600, color: 'var(--steel-900)', fontSize: 12.5 }}>{m.name}</div>
                    <div className="muted" style={{ fontSize: 11 }}>{m.email}</div>
                  </td>
                  <td>
                    {m.respondedThisWeek
                      ? <Badge tone="success">Submitted</Badge>
                      : <Badge tone="cream">Missing</Badge>}
                  </td>
                  <td style={{ fontSize: 12.5 }}>{m.respondedCount}/{m.recipientCount}</td>
                  <td style={{ fontSize: 12.5 }}>{m.lastRespondedCycle || <span className="muted">Never</span>}</td>
                </tr>
              ))}
            </tbody>
          </table>
          </div>
        </div>
      </div>
    </div>
  );
}
// Collapsible editor for the canned form + schedule. Saves via POST /api/standup.
// Field types are fixed to long_text for v1 (the three SMART questions); editing
// labels/required + the reminder day covers the stated need without rebuilding the
// full composer field-builder here.
const REMINDER_DAYS = [
  { value: '0 18 * * 0', label: 'Sunday 6pm' },
  { value: '0 18 * * 1', label: 'Monday 6pm' },
  { value: '0 9 * * 1', label: 'Monday 9am' },
  { value: '0 18 * * 5', label: 'Friday 6pm' },
];

function StandupSettings({ series, onSaved }) {
  const [open, setOpen] = useState(false);
  const [title, setTitle] = useState(series.title || 'SMART goals');
  const [intro, setIntro] = useState(series.intro || '');
  const [fields, setFields] = useState(() => (series.formSchema || []).map((f) => ({ ...f })));
  const [sendSlack, setSendSlack] = useState(series.sendSlack !== false);
  const [reminderCron, setReminderCron] = useState(series.reminderCron || '0 18 * * 0');
  const [saving, setSaving] = useState(false);
  const [feedback, setFeedback] = useState(null);

  const setLabel = (id, label) => setFields((prev) => prev.map((f) => (f.id === id ? { ...f, label } : f)));
  const setRequired = (id, required) => setFields((prev) => prev.map((f) => (f.id === id ? { ...f, required } : f)));

  const valid = title.trim() && fields.length > 0 && fields.every((f) => f.label.trim());

  // If the saved cron isn't one of the presets (e.g. a custom prior save), surface
  // it as an extra option so the select reflects reality instead of showing blank.
  const reminderOptions = REMINDER_DAYS.some((d) => d.value === reminderCron)
    ? REMINDER_DAYS
    : [...REMINDER_DAYS, { value: reminderCron, label: `Custom (${reminderCron})` }];

  const save = () => {
    setSaving(true);
    setFeedback(null);
    apiPost('/api/standup', {
      title: title.trim(),
      intro: intro.trim(),
      sendSlack,
      reminderCron,
      formSchema: fields.map((f) => ({ id: f.id, type: 'long_text', label: f.label.trim(), required: f.required === true })),
    })
      .then(() => { setFeedback({ tone: 'success', text: 'Saved.' }); onSaved && onSaved(); })
      .catch((err) => setFeedback({ tone: 'error', text: err.message || 'Could not save.' }))
      .finally(() => setSaving(false));
  };

  return (
    <div>
      <div className="row between" style={{ alignItems: 'center', marginBottom: 8 }}>
        <SectionHead title="Form settings"/>
        <button type="button" className="btn btn-ghost btn-sm" onClick={() => setOpen((o) => !o)}>
          <Icon name={open ? 'chevron-up' : 'chevron-down'} size={13}/> {open ? 'Hide' : 'Edit'}
        </button>
      </div>
      {open && (
        <div className="card" style={{ display: 'grid', gap: 16 }}>
          <label className="field">
            <span className="field-label">Title</span>
            <input className="input" value={title} onChange={(e) => setTitle(e.target.value)}/>
          </label>
          <label className="field">
            <span className="field-label">Intro</span>
            <textarea className="input" rows={2} value={intro} onChange={(e) => setIntro(e.target.value)}
              style={{ resize: 'vertical', fontFamily: 'inherit', lineHeight: 1.5 }}/>
          </label>

          <div style={{ display: 'grid', gap: 10 }}>
            <span className="field-label">Questions</span>
            {fields.map((f, i) => (
              <div key={f.id} className="row" style={{ gap: 8, alignItems: 'center' }}>
                <span className="muted" style={{ fontSize: 12, minWidth: 16 }}>{i + 1}</span>
                <input className="input" value={f.label} onChange={(e) => setLabel(f.id, e.target.value)}
                  style={{ flex: 1 }}/>
                <label className="row" style={{ gap: 6, margin: 0, fontSize: 12, cursor: 'pointer' }}>
                  <input type="checkbox" checked={f.required === true}
                    onChange={(e) => setRequired(f.id, e.target.checked)}/>
                  Required
                </label>
              </div>
            ))}
            <div className="muted" style={{ fontSize: 11.5 }}>
              All questions are long-text answers. (Add/remove questions in a future update.)
            </div>
          </div>

          <label className="field">
            <span className="field-label">Weekly reminder</span>
            <select className="input" value={reminderCron} onChange={(e) => setReminderCron(e.target.value)}>
              {reminderOptions.map((d) => <option key={d.value} value={d.value}>{d.label}</option>)}
            </select>
          </label>

          <label className="row" style={{ gap: 8, fontSize: 12.5, cursor: 'pointer', margin: 0 }}>
            <input type="checkbox" checked={sendSlack} onChange={(e) => setSendSlack(e.target.checked)}/>
            <span>Also send a Slack DM when the cycle goes out</span>
          </label>

          {feedback && (
            <div className={feedback.tone === 'error' ? 'form-error' : ''}
              style={{ fontSize: 12.5, color: feedback.tone === 'success' ? 'var(--coral-700)' : undefined }}>
              {feedback.text}
            </div>
          )}

          <div className="row" style={{ justifyContent: 'flex-end' }}>
            <button type="button" className="btn btn-primary btn-sm" onClick={save} disabled={!valid || saving}>
              {saving ? 'Saving…' : 'Save settings'}
            </button>
          </div>
        </div>
      )}
    </div>
  );
}

// SMART goals — the single tracking spot for the weekly standup. Loads status +
// config from GET /api/standup and the longitudinal data from GET
// /api/standup/tracking. See tasks/smart-goals-standup.md.
// The Responses reader: read all answers for a chosen week, or one member's answers
// across every week. Loads GET /api/standup/responses once and slices it client-side.
function StandupResponses() {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);
  const [mode, setMode] = useState('week');   // 'week' | 'member'
  const [cycleSel, setCycleSel] = useState('');
  const [memberSel, setMemberSel] = useState('');

  useEffect(() => {
    let cancelled = false;
    apiGet('/api/standup/responses')
      .then((d) => {
        if (cancelled) return;
        setData(d);
        if (d.cycles.length) setCycleSel(d.cycles[0].notificationId);
        if (d.members.length) setMemberSel(d.members[0].email);
      })
      .catch((err) => { if (!cancelled) setError(err.message || 'Could not load responses.'); });
    return () => { cancelled = true; };
  }, []);

  if (error) return <div className="card" style={{ padding: 24 }}><div className="form-error">{error}</div></div>;
  if (!data) return <div className="card" style={{ padding: 24 }}><div className="muted">Loading…</div></div>;

  const { cycles, members } = data;
  if (cycles.length === 0) {
    return <div className="card" style={{ padding: 24 }}><div className="muted">No SMART goals have been sent yet.</div></div>;
  }

  const selectedCycle = cycles.find((c) => c.notificationId === cycleSel) || cycles[0];

  // Shared answer card (one respondent's answers under a heading + sub-label).
  const answerCard = (key, heading, sub, fields, answers) => (
    <div key={key} className="card" style={{ display: 'grid', gap: 10 }}>
      <div className="row between" style={{ alignItems: 'baseline' }}>
        <span style={{ fontWeight: 600, color: 'var(--steel-900)', fontSize: 13 }}>{heading}</span>
        <span className="muted" style={{ fontSize: 11 }}>{sub}</span>
      </div>
      <div style={{ display: 'grid', gap: 8 }}>
        {fields.map((f) => (
          <div key={f.id}>
            <div className="muted" style={{ fontSize: 11.5 }}>{f.label}</div>
            <div style={{ fontSize: 13, color: 'var(--steel-900)', whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' }}>
              {answerText(answers[f.id]) || <span className="muted">—</span>}
            </div>
          </div>
        ))}
      </div>
    </div>
  );

  return (
    <div style={{ display: 'grid', gap: 20 }}>
      <div className="segment" role="tablist">
        <button type="button" className={mode === 'week' ? 'active' : ''}
          onClick={() => setMode('week')} role="tab" aria-selected={mode === 'week'}>
          <Icon name="calendar" size={14}/> By week
        </button>
        <button type="button" className={mode === 'member' ? 'active' : ''}
          onClick={() => setMode('member')} role="tab" aria-selected={mode === 'member'}>
          <Icon name="users" size={14}/> By member
        </button>
      </div>

      {mode === 'week' && (
        <div style={{ display: 'grid', gap: 14 }}>
          <label className="field" style={{ maxWidth: 240 }}>
            <span className="field-label">Week</span>
            <select className="input" value={selectedCycle.notificationId}
              onChange={(e) => setCycleSel(e.target.value)}>
              {cycles.map((c) => <option key={c.notificationId} value={c.notificationId}>{c.cycle}</option>)}
            </select>
          </label>
          <SectionHead title={`${selectedCycle.cycle} responses`}
            meta={`${selectedCycle.responders.length} of ${selectedCycle.responders.length + selectedCycle.missing.length} submitted`}/>
          {selectedCycle.missing.length > 0 && (
            <div className="muted" style={{ fontSize: 12 }}>
              Missing: {selectedCycle.missing.map((m) => m.name).join(', ')}
            </div>
          )}
          {selectedCycle.responders.length === 0 ? (
            <div className="card" style={{ padding: 20 }}><div className="muted">No responses for this week yet.</div></div>
          ) : (
            selectedCycle.responders.map((r) =>
              answerCard(r.email, r.name, fmtWhen(r.respondedAt), selectedCycle.fields, r.answers))
          )}
        </div>
      )}

      {mode === 'member' && (
        <div style={{ display: 'grid', gap: 14 }}>
          {members.length === 0 ? (
            <div className="card" style={{ padding: 20 }}><div className="muted">No one has responded yet.</div></div>
          ) : (
            <>
              <label className="field" style={{ maxWidth: 280 }}>
                <span className="field-label">Member</span>
                <select className="input" value={memberSel} onChange={(e) => setMemberSel(e.target.value)}>
                  {members.map((m) => <option key={m.email} value={m.email}>{m.name} ({m.respondedCount})</option>)}
                </select>
              </label>
              {(() => {
                const mine = cycles
                  .map((c) => ({ c, resp: c.responders.find((r) => r.email === memberSel) }))
                  .filter((x) => x.resp);
                if (mine.length === 0) {
                  return <div className="card" style={{ padding: 20 }}><div className="muted">This member hasn't responded to any cycle yet.</div></div>;
                }
                return mine.map(({ c, resp }) =>
                  answerCard(c.notificationId, c.cycle, fmtWhen(resp.respondedAt), c.fields, resp.answers));
              })()}
            </>
          )}
        </div>
      )}
    </div>
  );
}

function SmartGoals() {
  const [data, setData] = useState(null);        // GET /api/standup
  const [tracking, setTracking] = useState(null); // GET /api/standup/tracking
  const [sending, setSending] = useState(false);
  const [feedback, setFeedback] = useState(null);
  const [reloadKey, setReloadKey] = useState(0);
  const [subView, setSubView] = useState('track'); // 'track' | 'responses'

  useEffect(() => {
    let cancelled = false;
    Promise.all([apiGet('/api/standup'), apiGet('/api/standup/tracking')])
      .then(([status, track]) => { if (!cancelled) { setData(status); setTracking(track); } })
      .catch((err) => { if (!cancelled) setFeedback({ tone: 'error', text: err.message || 'Could not load SMART goals.' }); });
    return () => { cancelled = true; };
  }, [reloadKey]);

  const send = () => {
    setSending(true);
    setFeedback(null);
    apiPost('/api/standup/send', {})
      .then((res) => {
        setFeedback(res.alreadySent
          ? { tone: 'success', text: `This week (${res.cycle}) was already sent.` }
          : { tone: 'success', text: `Sent to ${res.recipientCount} member${res.recipientCount === 1 ? '' : 's'}.` });
        setReloadKey((k) => k + 1);
      })
      .catch((err) => setFeedback({ tone: 'error', text: err.message || 'Could not send.' }))
      .finally(() => setSending(false));
  };

  if (!data) {
    return <div className="card" style={{ padding: 24 }}><div className="muted">Loading…</div></div>;
  }

  const { currentCycle, status } = data;

  return (
    <div style={{ display: 'grid', gap: 24 }}>
      <div className="segment" role="tablist">
        <button type="button" className={subView === 'track' ? 'active' : ''}
          onClick={() => setSubView('track')} role="tab" aria-selected={subView === 'track'}>
          <Icon name="check-square" size={14}/> Track
        </button>
        <button type="button" className={subView === 'responses' ? 'active' : ''}
          onClick={() => setSubView('responses')} role="tab" aria-selected={subView === 'responses'}>
          <Icon name="edit" size={14}/> Responses
        </button>
      </div>

      {subView === 'track' && (
        <>
          {/* This week ------------------------------------------------------- */}
          <div className="card" style={{ display: 'grid', gap: 14 }}>
            <div className="row between" style={{ alignItems: 'center' }}>
              <div className="row" style={{ gap: 10, alignItems: 'center' }}>
                <span style={{ fontWeight: 600, fontSize: 16, color: 'var(--steel-900)' }}>This week</span>
                <Badge tone="steel">{currentCycle}</Badge>
                {status.sent
                  ? <Badge tone="success">{status.respondedCount}/{status.recipientCount} submitted</Badge>
                  : <Badge tone="cream">Not sent yet</Badge>}
              </div>
              {!status.sent && (
                <button type="button" className="btn btn-primary" onClick={send} disabled={sending}>
                  <Icon name="check-square" size={14}/> {sending ? 'Sending…' : "Send this week's SMART goals"}
                </button>
              )}
            </div>
            {feedback && (
              <div className={feedback.tone === 'error' ? 'form-error' : ''}
                style={{ fontSize: 12.5, color: feedback.tone === 'success' ? 'var(--coral-700)' : undefined }}>
                {feedback.text}
              </div>
            )}
          </div>

          {/* Tracking (Task 9) + form settings (Task 10) render here. */}
          <StandupTracking tracking={tracking} latest={tracking ? tracking.latest : null}/>
          <StandupSettings series={data.series} onSaved={() => setReloadKey((k) => k + 1)}/>
        </>
      )}

      {subView === 'responses' && <StandupResponses/>}
    </div>
  );
}

function Notifications({ members = [], memberClasses = [], projects = [], currentUser, viewMode = 'member', routeParam = null }) {
  // The active tab is mirrored into the hash (#/notifications/<view>) so it's
  // deep-linkable and survives a refresh. routeParam (from the router) is the source
  // of truth; selectView writes the hash, which flows back through routeParam.
  const [view, setView] = useState(() => (NOTIFY_VIEWS.includes(routeParam) ? routeParam : 'compose'));

  useEffect(() => {
    setView(NOTIFY_VIEWS.includes(routeParam) ? routeParam : 'compose');
  }, [routeParam]);

  const selectView = (next) => {
    setView(next);
    const desired = `#/notifications${next === 'compose' ? '' : `/${next}`}`;
    if (window.location.hash !== desired) window.location.hash = desired;
  };
  const [title, setTitle] = useState('');
  const [body, setBody] = useState('');
  const [mode, setMode] = useState('all'); // 'all' | 'specific'
  const [selectedMembers, setSelectedMembers] = useState([]);
  const [search, setSearch] = useState('');
  const [sendSlack, setSendSlack] = useState(true);
  // requestResponse drives the Message↔Form switch — true sends a kind='form'
  // notification with the fields below; false sends a one-way message.
  const [requestResponse, setRequestResponse] = useState(false);
  const [formFields, setFormFields] = useState([]);
  const [sending, setSending] = useState(false);
  const [feedback, setFeedback] = useState(null);
  const [history, setHistory] = useState([]);
  // The sent notification currently open in the post-send editor (null when closed).
  const [editing, setEditing] = useState(null);

  // Field editing lives in FormFieldBuilder; this component only holds the list.
  // It is sent as `formSchema` when Form mode is active (tasks/response-request-notifications.md).
  const formValid = useMemo(() => isFormValid(formFields), [formFields]);

  const loadHistory = () => {
    apiGet('/api/notifications')
      .then((data) => setHistory(data.notifications || []))
      .catch(() => {});
  };

  useEffect(() => {
    loadHistory();
  }, []);

  // Only members with an email can be DM'd or tracked, so they are the universe
  // the picker operates over.
  const addressableMembers = useMemo(
    () => members.filter((m) => m.email).slice().sort((a, b) => a.name.localeCompare(b.name)),
    [members]
  );
  const addressableIds = useMemo(
    () => new Set(addressableMembers.map((m) => m.id)),
    [addressableMembers]
  );
  const selectedSet = useMemo(() => new Set(selectedMembers), [selectedMembers]);

  // Quick-group chips, derived from the roster. Each resolves to the addressable
  // member ids it covers; empty groups are dropped so chips never lie.
  const groups = useMemo(() => {
    const keepAddressable = (ids) => ids.filter((id) => addressableIds.has(id));
    const out = [];
    const active = keepAddressable(activeMembers(members).map((m) => m.id));
    if (active.length) out.push({ key: 'active', kind: 'status', label: 'Active members', ids: active });
    memberClasses.forEach((c) => {
      const ids = keepAddressable(members.filter((m) => (m.classIds || []).includes(c.id)).map((m) => m.id));
      if (ids.length) out.push({ key: `class:${c.id}`, kind: 'class', label: c.name, ids });
    });
    projects.forEach((p) => {
      const ids = keepAddressable((p.memberships || []).map((ms) => ms.memberId));
      if (ids.length) out.push({ key: `team:${p.id}`, kind: 'team', label: p.title || p.name, ids });
    });
    return out;
  }, [members, memberClasses, projects, addressableIds]);

  const groupsByKind = (kind) => groups.filter((g) => g.kind === kind);

  const groupState = (group) => {
    const hits = group.ids.reduce((n, id) => n + (selectedSet.has(id) ? 1 : 0), 0);
    if (hits === 0) return 'none';
    if (hits === group.ids.length) return 'all';
    return 'partial';
  };

  const toggleGroup = (group) => {
    setSelectedMembers((prev) => {
      const next = new Set(prev);
      const allIn = group.ids.every((id) => next.has(id));
      group.ids.forEach((id) => (allIn ? next.delete(id) : next.add(id)));
      return Array.from(next);
    });
  };

  const toggleMember = (id) => {
    setSelectedMembers((prev) =>
      prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]
    );
  };

  const selectAll = () => setSelectedMembers(addressableMembers.map((m) => m.id));
  const clearAll = () => setSelectedMembers([]);

  const filtered = useMemo(() => {
    const q = search.trim().toLowerCase();
    if (!q) return addressableMembers;
    return addressableMembers.filter(
      (m) => m.name.toLowerCase().includes(q) || (m.email || '').toLowerCase().includes(q)
    );
  }, [search, addressableMembers]);

  const filteredAllSelected = filtered.length > 0 && filtered.every((m) => selectedSet.has(m.id));
  const toggleFiltered = () => {
    setSelectedMembers((prev) => {
      const next = new Set(prev);
      const allIn = filtered.every((m) => next.has(m.id));
      filtered.forEach((m) => (allIn ? next.delete(m.id) : next.add(m.id)));
      return Array.from(next);
    });
  };

  const reset = () => {
    setTitle('');
    setBody('');
    setMode('all');
    setSelectedMembers([]);
    setSearch('');
    setSendSlack(true);
    setRequestResponse(false);
    setFormFields([]);
  };

  const audienceType = mode === 'all' ? 'all' : 'members';
  const audienceValue = mode === 'all' ? null : selectedMembers;

  // Everything still missing before this can send. One list drives both the disabled
  // state and the hint under the send bar, so a blocked button always says why —
  // in Form mode the extra preconditions are easy to miss.
  const missing = [];
  if (!title.trim()) missing.push('a title');
  if (!body.trim()) missing.push(requestResponse ? 'a form intro' : 'a message');
  if (mode !== 'all' && selectedMembers.length === 0) missing.push('at least one recipient');
  if (requestResponse && !formValid) {
    missing.push(formFields.length === 0
      ? 'at least one form field'
      : 'a label on every field (and two options on each choice field)');
  }
  const missingText = missing.length < 2
    ? missing[0]
    : `${missing.slice(0, -1).join(', ')} and ${missing[missing.length - 1]}`;

  const canSend = missing.length === 0 && !sending;

  const count = selectedMembers.length;
  const sendLabel = sending
    ? 'Sending…'
    : mode === 'all'
      ? 'Send to everyone'
      : `Send to ${count} ${count === 1 ? 'person' : 'people'}`;

  const send = () => {
    if (!canSend) return;
    setSending(true);
    setFeedback(null);
    apiPost('/api/notifications', {
      title: title.trim(),
      body: body.trim(),
      audienceType,
      audienceValue,
      sendSlack,
      kind: requestResponse ? 'form' : 'message',
      formSchema: requestResponse ? buildFormSchema(formFields) : null,
    })
      .then((data) => {
        const recipients = data.notification?.recipientCount ?? 0;
        setFeedback({
          tone: 'success',
          text: `Sent to ${recipients} recipient${recipients === 1 ? '' : 's'}.${sendSlack ? ' Slack DMs are sending in the background.' : ''}`,
        });
        reset();
        loadHistory();
      })
      .catch((error) => {
        setFeedback({ tone: 'error', text: error.message || 'Failed to send notification.' });
      })
      .finally(() => setSending(false));
  };

  const remove = (id) => {
    apiDelete(`/api/notifications/${id}`)
      .then(() => loadHistory())
      .catch(() => {});
  };

  const audienceLabel = (n) => {
    if (n.audienceType === 'all') return 'Everyone';
    if (n.audienceType === 'class') return 'Class';
    if (n.audienceType === 'members') return 'Selected members';
    return n.audienceType;
  };

  if (!currentUser?.isAdmin) {
    return null;
  }

  const Chip = ({ group }) => {
    const state = groupState(group);
    return (
      <button
        type="button"
        className={`chip ${state === 'all' ? 'active' : state === 'partial' ? 'partial' : ''}`}
        onClick={() => toggleGroup(group)}
        aria-pressed={state === 'all'}
      >
        {state === 'all' && <span className="chip-tick"><Icon name="check" size={12}/></span>}
        {group.label}
        <span className="chip-count">{group.ids.length}</span>
      </button>
    );
  };

  const classChips = groupsByKind('class');
  const teamChips = groupsByKind('team');
  const statusChips = groupsByKind('status');

  // Right-rail preview of who this send reaches.
  const selectedNames = addressableMembers.filter((m) => selectedSet.has(m.id));

  return (
    <>
      <PageHeader
        eyebrow="Operations · Notify"
        title={view === 'compose' ? 'Send a notification'
          : view === 'activity' ? 'Notification activity'
          : view === 'smart-goals' ? 'SMART goals'
          : 'News segments'}
        sub={view === 'compose'
          ? 'Posts an in-app bell notification to the chosen audience and, when enabled, a Slack DM to each recipient.'
          : view === 'activity'
            ? 'See who has seen each notification and review the answers to your response forms.'
            : view === 'smart-goals'
              ? 'The weekly check-in: send this week’s form and track who’s keeping up.'
              : 'Set the order in which presenter teams (2–3 people) give the news segment, and remind whoever’s up next.'}
        actions={
          <div className="row" style={{ gap: 12, alignItems: 'center' }}>
            <div className="segment" role="tablist">
              <button type="button" className={view === 'compose' ? 'active' : ''}
                onClick={() => selectView('compose')} role="tab" aria-selected={view === 'compose'}>
                <Icon name="edit" size={14}/> Compose
              </button>
              <button type="button" className={view === 'activity' ? 'active' : ''}
                onClick={() => selectView('activity')} role="tab" aria-selected={view === 'activity'}>
                <Icon name="users" size={14}/> Activity
              </button>
              <button type="button" className={view === 'segments' ? 'active' : ''}
                onClick={() => selectView('segments')} role="tab" aria-selected={view === 'segments'}>
                <Icon name="mic" size={14}/> Segments
              </button>
              <button type="button" className={view === 'smart-goals' ? 'active' : ''}
                onClick={() => selectView('smart-goals')} role="tab" aria-selected={view === 'smart-goals'}>
                <Icon name="check-square" size={14}/> SMART goals
              </button>
            </div>
            <Badge tone="solid">Admin only</Badge>
          </div>
        }
      />

      {view === 'segments' && (
        <PresentationOrder embedded members={members} currentUser={currentUser} viewMode={viewMode}/>
      )}

      {view === 'activity' && <NotificationActivity notifications={history} onChanged={loadHistory}/>}

      {view === 'smart-goals' && <SmartGoals/>}

      {view === 'compose' && (
      <>
      <div className="compose-grid">
        <div>
          <SectionHead title="Compose"/>
          <div className="card compose-card">
            {/* --- What to send: Message vs Form ----------------------------- */}
            <div className="compose-section compose-kind">
              <div className="segment segment-lg" role="tablist">
                <button type="button" className={!requestResponse ? 'active' : ''}
                  onClick={() => setRequestResponse(false)} role="tab" aria-selected={!requestResponse}>
                  <Icon name="bell" size={14}/> Message
                </button>
                <button type="button" className={requestResponse ? 'active' : ''}
                  onClick={() => setRequestResponse(true)} role="tab" aria-selected={requestResponse}>
                  <Icon name="edit" size={14}/> Form
                </button>
              </div>
              <p className="compose-kind-desc">
                {requestResponse
                  ? 'Recipients get a “Respond” link to a form you design below.'
                  : 'A one-way announcement delivered to the bell and, optionally, Slack.'}
              </p>
            </div>

            {/* --- Message --------------------------------------------------- */}
            <div className="compose-section">
              <div className="compose-section-head">
                <span className="field-label">{requestResponse ? 'Form intro' : 'Message'}</span>
                {requestResponse && <span className="compose-section-hint">Shown above the form</span>}
              </div>

              <label className="field">
                <div className="field-row">
                  <span className="field-label">Title</span>
                  <span className={`char-count ${title.length > TITLE_MAX ? 'over' : ''}`}>
                    {title.length}/{TITLE_MAX}
                  </span>
                </div>
                <input className="input" value={title} onChange={(e) => setTitle(e.target.value)}
                  placeholder="Meeting moved to Thursday" maxLength={TITLE_MAX}/>
              </label>

              <label className="field">
                <div className="field-row">
                  <span className="field-label">Body</span>
                  <span className={`char-count ${body.length > BODY_MAX ? 'over' : ''}`}>
                    {body.length}/{BODY_MAX}
                  </span>
                </div>
                <textarea className="input" value={body} onChange={(e) => setBody(e.target.value)} rows={4}
                  maxLength={BODY_MAX}
                  placeholder="This week's GBM is moved to Thursday 6pm in Gates G01."
                  style={{ resize: 'vertical', fontFamily: 'inherit', lineHeight: 1.5 }}/>
              </label>
            </div>

            {/* --- Form fields (Form mode only) ------------------------------ */}
            {requestResponse && (
              <div className="compose-section">
                <div className="compose-section-head">
                  <span className="field-label">Form fields</span>
                  <span className="compose-section-hint">
                    {formFields.length} field{formFields.length === 1 ? '' : 's'} · drag to reorder
                  </span>
                </div>

                <FormFieldBuilder fields={formFields} onChange={setFormFields}/>
              </div>
            )}

            {/* --- Recipients ------------------------------------------------ */}
            <div className="compose-section">
              <div className="compose-section-head">
                <span className="field-label">Recipients</span>
                <span className="compose-section-hint">
                  {mode === 'all'
                    ? 'Everyone with an account'
                    : `${count} of ${addressableMembers.length} selected`}
                </span>
              </div>

              <div className="segment" role="tablist">
                <button type="button" className={mode === 'all' ? 'active' : ''}
                  onClick={() => setMode('all')}>
                  <Icon name="users" size={14}/> Everyone
                </button>
                <button type="button" className={mode === 'specific' ? 'active' : ''}
                  onClick={() => setMode('specific')}>
                  <Icon name="check-square" size={14}/> Specific people
                </button>
              </div>

              {mode === 'specific' && (
                <>
                  {(statusChips.length > 0 || classChips.length > 0 || teamChips.length > 0) && (
                    <div className="chip-block">
                      {statusChips.length > 0 && (
                        <div className="chip-row">
                          <span className="chip-row-label">Quick</span>
                          {statusChips.map((g) => <Chip key={g.key} group={g}/>)}
                        </div>
                      )}
                      {classChips.length > 0 && (
                        <div className="chip-row">
                          <span className="chip-row-label">Class</span>
                          {classChips.map((g) => <Chip key={g.key} group={g}/>)}
                        </div>
                      )}
                      {teamChips.length > 0 && (
                        <div className="chip-row">
                          <span className="chip-row-label">Teams</span>
                          {teamChips.map((g) => <Chip key={g.key} group={g}/>)}
                        </div>
                      )}
                    </div>
                  )}

                  <div className="recipient-box">
                    <div className="recipient-toolbar">
                      <label className="recipient-search">
                        <Icon name="search" size={14}/>
                        <input className="input" value={search} onChange={(e) => setSearch(e.target.value)}
                          placeholder="Search members by name or email…"/>
                      </label>
                      <button type="button" className="btn btn-ghost btn-sm" onClick={selectAll}
                        disabled={addressableMembers.length === 0}>
                        Select all
                      </button>
                    </div>

                    <div className="recipient-list">
                      {addressableMembers.length === 0 ? (
                        <div className="recipient-empty">No members have an email on file.</div>
                      ) : filtered.length === 0 ? (
                        <div className="recipient-empty">No members match “{search}”.</div>
                      ) : filtered.map((m) => {
                        const selected = selectedSet.has(m.id);
                        return (
                          <label key={m.id} className={`recipient-item ${selected ? 'selected' : ''}`}>
                            <input type="checkbox" checked={selected} onChange={() => toggleMember(m.id)}/>
                            <Avatar initials={m.initials || m.name.slice(0, 2).toUpperCase()} size={28}/>
                            <div className="r-body">
                              <div className="r-name">{m.name}</div>
                              <div className="r-email">{m.email}</div>
                            </div>
                          </label>
                        );
                      })}
                    </div>

                    {addressableMembers.length > 0 && (
                      <div className="recipient-footer">
                        <label className="row" style={{ gap: 8, cursor: 'pointer', margin: 0 }}>
                          <input type="checkbox" checked={filteredAllSelected} onChange={toggleFiltered}/>
                          <span>{search ? 'Select all matches' : 'Select all'}</span>
                        </label>
                        <button type="button" className="linklike" onClick={clearAll} disabled={count === 0}>
                          Clear {count > 0 ? `(${count})` : ''}
                        </button>
                      </div>
                    )}
                  </div>
                </>
              )}
            </div>

            {/* --- Delivery + send ------------------------------------------- */}
            <div className="compose-section">
              <label className="row" style={{ gap: 8, fontSize: 12.5, cursor: 'pointer', margin: 0 }}>
                <input type="checkbox" checked={sendSlack} onChange={(e) => setSendSlack(e.target.checked)}/>
                <span>Also send a Slack DM to each recipient</span>
              </label>

              {feedback && (
                <div className={feedback.tone === 'error' ? 'form-error' : ''}
                  style={{ fontSize: 12.5, color: feedback.tone === 'success' ? 'var(--coral-700)' : undefined }}>
                  {feedback.text}
                </div>
              )}

              <div className="send-bar">
                <span className="send-summary">
                  {mode === 'all'
                    ? <>Sending to <strong>everyone</strong></>
                    : count === 0
                      ? 'No recipients selected yet'
                      : <>Sending to <strong>{count}</strong> {count === 1 ? 'person' : 'people'}</>}
                </span>
                <button className="btn btn-primary" type="button" onClick={send} disabled={!canSend}>
                  <Icon name={requestResponse ? 'edit' : 'bell'} size={14}/> {sendLabel}
                </button>
              </div>

              {!sending && missing.length > 0 && (
                <div className="send-blocked">Add {missingText} to send.</div>
              )}
            </div>
          </div>
        </div>

        <div style={{ display: 'grid', gap: 20 }}>
          <div>
            <SectionHead title="This send"/>
            <div className="card" style={{ display: 'grid', gap: 12 }}>
              <div className="row between">
                <span className="muted" style={{ fontSize: 12.5 }}>Type</span>
                <Badge tone={requestResponse ? 'coral-solid' : 'steel'}>
                  {requestResponse ? `Form · ${formFields.length} field${formFields.length === 1 ? '' : 's'}` : 'Message'}
                </Badge>
              </div>
              <div className="row between">
                <span className="muted" style={{ fontSize: 12.5 }}>Audience</span>
                <Badge tone={mode === 'all' ? 'coral-solid' : 'steel'}>
                  {mode === 'all' ? 'Everyone' : `${count} selected`}
                </Badge>
              </div>
              <div className="row between">
                <span className="muted" style={{ fontSize: 12.5 }}>Slack DM</span>
                <Badge tone={sendSlack ? 'success' : 'mist'}>{sendSlack ? 'On' : 'Off'}</Badge>
              </div>
              {mode === 'specific' && selectedNames.length > 0 && (
                <div style={{ display: 'flex', flexWrap: 'wrap', gap: 5, paddingTop: 2 }}>
                  {selectedNames.slice(0, 8).map((m) => (
                    <Badge key={m.id} tone="cream">{m.name}</Badge>
                  ))}
                  {selectedNames.length > 8 && (
                    <Badge tone="mist">+{selectedNames.length - 8} more</Badge>
                  )}
                </div>
              )}
            </div>
          </div>

          <div>
            <SectionHead title="Notes"/>
            <div className="card" style={{ display: 'grid', gap: 14 }}>
              <div>
                <div style={{ fontWeight: 600, color: 'var(--steel-900)', marginBottom: 4 }}>Slack DMs need matching emails</div>
                <div className="muted" style={{ fontSize: 12.5, lineHeight: 1.55 }}>
                  A DM only lands if the recipient's Slack email matches their Cornell email. Unmatched
                  recipients are recorded as <span className="mono">skipped</span> — they still see the in-app bell.
                </div>
              </div>
              <div>
                <div style={{ fontWeight: 600, color: 'var(--steel-900)', marginBottom: 4 }}>Bot scopes</div>
                <div className="muted" style={{ fontSize: 12.5, lineHeight: 1.55 }}>
                  DM delivery requires the <span className="mono">users:read.email</span> and{' '}
                  <span className="mono">im:write</span> bot scopes in addition to <span className="mono">chat:write</span>.
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>

      <SectionHead title="Sent notifications" meta={`${history.length} total`}/>
      <div className="card" style={{ padding: 0, overflow: 'hidden' }}>
        <div className="table-scroll">
        <table className="tbl">
          <thead>
            <tr>
              <th>Notification</th>
              <th>Audience</th>
              <th style={{ width: 80 }}>Recipients</th>
              <th style={{ width: 70 }}>Read</th>
              <th style={{ width: 130 }}>Slack</th>
              <th style={{ width: 78 }}></th>
            </tr>
          </thead>
          <tbody>
            {history.length === 0 ? (
              <tr><td colSpan={6} className="muted" style={{ textAlign: 'center', padding: 20 }}>No notifications sent yet.</td></tr>
            ) : history.map((n) => (
              <tr key={n.id}>
                <td>
                  <div className="row" style={{ gap: 6, alignItems: 'center' }}>
                    <span style={{ fontWeight: 600, color: 'var(--steel-900)' }}>{n.title}</span>
                    {n.kind === 'form' && <Badge tone="steel">Form</Badge>}
                  </div>
                  <div className="muted wrap-anywhere" style={{ fontSize: 11.5, marginTop: 3, lineHeight: 1.5, maxWidth: 360 }}>
                    {n.body}
                  </div>
                </td>
                <td style={{ fontSize: 12.5 }}>{audienceLabel(n)}</td>
                <td style={{ fontSize: 12.5 }}>{n.recipientCount}</td>
                <td style={{ fontSize: 12.5 }}>{n.readCount}/{n.recipientCount}</td>
                <td><SlackStatusBadge status={n.slackStatus}/></td>
                <td>
                  <div className="row" style={{ gap: 2 }}>
                    <button className="btn-icon" title="Edit" type="button" onClick={() => setEditing(n)}>
                      <Icon name="edit" size={13}/>
                    </button>
                    <button className="btn-icon" title="Delete" type="button" onClick={() => remove(n.id)}>
                      <Icon name="x" size={14}/>
                    </button>
                  </div>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
        </div>
      </div>

      {editing && (
        <EditNotificationModal
          notification={editing}
          responseCount={editing.responseCount || 0}
          onClose={() => setEditing(null)}
          onSaved={() => { setEditing(null); loadHistory(); }}/>
      )}
      </>
      )}
    </>
  );
}

window.Notifications = Notifications;
