// Members — a workspace's own people, managed by its own admin.
//
// Reaches `/v1/members`. Every control here is backed by a real route that
// enforces the same rule server-side; nothing on this screen is decoration.
//
// ── Why the controls are HIDDEN rather than greyed for a non-admin ──────────
// The rail entry itself only appears with `member:manage`, so a Reviewer never
// arrives here. If they do (a bookmarked URL), they get the honest empty state
// rather than a page of buttons that answer 403 — a control that cannot work
// should not be offered (`feedback_no_false_affordance_toggles`).

const MEMBER_ROLES = [
  {
    id: 'admin',
    label: 'Admin',
    blurb: 'Everything, plus inviting people and managing access.',
  },
  {
    id: 'editor',
    label: 'Editor',
    blurb: 'Create and edit courses, upload media, run exports.',
  },
  {
    id: 'reviewer',
    label: 'Reviewer',
    blurb: 'Read-only. Can open, read and preview every course, and change nothing.',
  },
  {
    id: 'language_reviewer',
    label: 'Language Reviewer',
    blurb: 'Localisation only, in the languages you assign below. Nothing else.',
  },
];

const ROLE_LABEL = MEMBER_ROLES.reduce((m, r) => { m[r.id] = r.label; return m; }, {});

// The languages a reviewer can be assigned. Taken from the same table the rest
// of the app uses, so this list cannot drift from the Localisation rail.
function availableLanguages() {
  const names = window.LANG_NAMES || {};
  return Object.keys(names).map((code) => ({ code, name: names[code] }));
}

async function membersApi(path, options) {
  const token = await window.dynamoGetAccessToken();
  const res = await fetch(`${window.DYNAMO_ENV.gatewayBase}/v1${path}`, {
    ...(options || {}),
    headers: {
      'content-type': 'application/json',
      authorization: `Bearer ${token}`,
      ...((options || {}).headers || {}),
    },
  });
  let body = null;
  try { body = await res.json(); } catch (_) { body = null; }
  if (!res.ok) {
    // The gateway's error envelope is flat: { code, message, details? }. Its
    // messages are written for the person reading them, so they are surfaced
    // verbatim rather than replaced with something vaguer.
    const err = new Error((body && body.message) || `Request failed (${res.status})`);
    err.status = res.status;
    err.details = body && body.details;
    throw err;
  }
  return body;
}

// The 'revoked' arm is deliberately KEPT even though the members list no longer
// returns removed people: deleting it would make an unexpected `revoked` render
// as **Active**, which is a lie. A formatter's fallback must never flatter.
function StatusPill({ status }) {
  const label = status === 'invited' ? 'Invited'
    : status === 'revoked' ? 'Removed'
    : 'Active';
  return <span className="pill" style={{
    opacity: status === 'revoked' ? 0.6 : 1,
  }}>{label}</span>;
}

// ── The invite / change-access dialog ───────────────────────────────────────
function MemberDialog({ member, busy, error, onCancel, onSubmit }) {
  const editing = !!member;
  const [email, setEmail] = React.useState(editing ? member.email : '');
  const [role, setRole] = React.useState(editing ? member.role : 'editor');
  const [langs, setLangs] = React.useState(
    editing && Array.isArray(member.assignedLangs) ? member.assignedLangs : [],
  );
  const languages = availableLanguages();
  const needsLangs = role === 'language_reviewer';
  // The same rule the route and the database both enforce, stated here so the
  // person filling the form is told BEFORE they submit rather than by a 400.
  const canSubmit = !busy
    && (editing || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email))
    && (!needsLangs || langs.length > 0);

  const toggleLang = (code) => setLangs((prev) =>
    prev.indexOf(code) === -1 ? prev.concat([code]) : prev.filter((c) => c !== code));

  return (
    <>
      <div onClick={busy ? undefined : onCancel}
        style={{ position: 'fixed', inset: 0, zIndex: 60, background: 'rgba(0,0,0,.35)' }} />
      <div role="dialog" aria-modal="true" aria-label={editing ? 'Change access' : 'Invite someone'}
        style={{
          position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%,-50%)',
          width: 'min(520px, calc(100vw - 32px))', maxHeight: 'calc(100vh - 64px)',
          overflowY: 'auto', padding: 20, zIndex: 61,
          background: 'var(--surface)', border: '1px solid var(--border)',
          borderRadius: 'var(--radius-md)', boxShadow: 'var(--shadow-lg)',
        }}>
        <div style={{ fontSize: 15, fontWeight: 650, marginBottom: 4 }}>
          {editing ? `Change ${member.displayName || member.email}` : 'Invite someone'}
        </div>
        <div style={{ fontSize: 12, color: 'var(--text-faint)', marginBottom: 14 }}>
          {editing
            ? 'Their access changes the moment you save. They stay signed in.'
            : 'They will get an email asking them to set a password, then land in this workspace.'}
        </div>

        {!editing && (
          <label style={{ display: 'block', marginBottom: 12 }}>
            <div style={{ fontSize: 12, fontWeight: 600, marginBottom: 4 }}>Email address</div>
            <input className="field" type="email" value={email} autoFocus
              disabled={busy}
              onChange={(e) => setEmail(e.target.value)}
              placeholder="name@company.com" style={{ width: '100%' }} />
          </label>
        )}

        <div style={{ fontSize: 12, fontWeight: 600, marginBottom: 6 }}>Access level</div>
        <div style={{ display: 'grid', gap: 6, marginBottom: needsLangs ? 12 : 16 }}>
          {MEMBER_ROLES.map((r) => (
            <label key={r.id} style={{
              display: 'flex', gap: 8, alignItems: 'flex-start', padding: '8px 10px',
              border: '1px solid var(--border)', borderRadius: 'var(--radius-sm)',
              cursor: busy ? 'default' : 'pointer',
              background: role === r.id ? 'var(--surface-2)' : 'transparent',
            }}>
              <input type="radio" name="member-role" value={r.id} checked={role === r.id}
                disabled={busy}
                onChange={() => setRole(r.id)} style={{ marginTop: 2 }} />
              <span>
                <span style={{ display: 'block', fontSize: 13, fontWeight: 600 }}>{r.label}</span>
                <span style={{ display: 'block', fontSize: 11.5, color: 'var(--text-faint)' }}>
                  {r.blurb}
                </span>
              </span>
            </label>
          ))}
        </div>

        {needsLangs && (
          <div style={{ marginBottom: 16 }}>
            <div style={{ fontSize: 12, fontWeight: 600, marginBottom: 6 }}>
              Languages they may edit
            </div>
            <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
              {languages.map((l) => {
                const on = langs.indexOf(l.code) !== -1;
                return (
                  <button key={l.code} type="button" disabled={busy}
                    className={`btn sm${on ? ' primary' : ''}`}
                    onClick={() => toggleLang(l.code)}>
                    {on && <I.Check size={12} />}{l.name}
                  </button>
                );
              })}
            </div>
            {langs.length === 0 && (
              // Stated as a requirement, not an error, because nothing has gone
              // wrong yet. The route and a database CHECK both refuse this too —
              // three layers, because a Language Reviewer with no languages can
              // open the workspace and change nothing, which looks like a bug in
              // the product rather than a mistake in the form.
              <div style={{ fontSize: 11.5, color: 'var(--text-faint)', marginTop: 6 }}>
                Pick at least one — a Language Reviewer with no languages could not
                edit anything.
              </div>
            )}
          </div>
        )}

        {error && (
          <div style={{
            fontSize: 12, padding: '8px 10px', marginBottom: 12,
            border: '1px solid var(--error)', borderRadius: 'var(--radius-sm)',
            color: 'var(--error-text)', background: 'var(--error-bg)',
          }}>{error}</div>
        )}

        <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
          <button className="btn ghost" onClick={onCancel} disabled={busy}>Cancel</button>
          <button className="btn primary" disabled={!canSubmit}
            onClick={() => onSubmit({ email, role, assignedLangs: langs })}>
            {busy ? 'Working…' : editing ? 'Save access' : 'Send invitation'}
          </button>
        </div>
      </div>
    </>
  );
}

function SurfaceMembers() {
  const [members, setMembers] = React.useState(null);
  const [loadError, setLoadError] = React.useState(null);
  const [dialog, setDialog] = React.useState(null); // null | {member?}
  const [busy, setBusy] = React.useState(false);
  const [dialogError, setDialogError] = React.useState(null);
  const [notice, setNotice] = React.useState(null);
  // A SYNCHRONOUS latch, not a `disabled` prop. Two clicks land in the same
  // React tick before any re-render, and each one would send an email.
  const inFlight = React.useRef(false);

  const canManage = typeof window.dynamoCan === 'function' && window.dynamoCan('member:manage');

  const load = React.useCallback(async () => {
    try {
      const j = await membersApi('/members');
      setMembers((j && j.members) || []);
      setLoadError(null);
    } catch (e) {
      setLoadError(e.message);
    }
  }, []);

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

  const run = async (fn, okMessage) => {
    if (inFlight.current) return;
    inFlight.current = true;
    setBusy(true);
    setDialogError(null);
    try {
      await fn();
      setDialog(null);
      if (okMessage) setNotice(okMessage);
      await load();
    } catch (e) {
      setDialogError(e.message);
    } finally {
      inFlight.current = false;
      setBusy(false);
    }
  };

  const invite = (form) => run(async () => {
    await membersApi('/members', { method: 'POST', body: JSON.stringify(form) });
  }, `Invitation sent to ${form.email}.`);

  const changeAccess = (member, form) => run(async () => {
    await membersApi(`/members/${member.id}`, {
      method: 'PATCH',
      body: JSON.stringify({ role: form.role, assignedLangs: form.assignedLangs }),
    });
  }, `${member.displayName || member.email} is now ${ROLE_LABEL[form.role]}.`);

  const revoke = (member) => {
    const who = member.displayName || member.email;
    if (!window.confirm(
      `Remove ${who} from this workspace?\n\n`
      + 'They keep their sign-in but lose access to every course here, immediately. '
      + 'They will disappear from this list. Anything they authored stays, and you '
      + 'can invite them back at any time.')) return;
    return run(async () => {
      await membersApi(`/members/${member.id}`, { method: 'DELETE' });
    }, `${who} no longer has access.`);
  };

  const resend = (member) => run(async () => {
    await membersApi(`/members/${member.id}/resend`, { method: 'POST' });
  }, `Invitation re-sent to ${member.email}.`);

  if (!canManage) {
    return (
      <div style={{ padding: 24, maxWidth: 560 }}>
        <div style={{ fontSize: 15, fontWeight: 650, marginBottom: 6 }}>Members</div>
        <div style={{ fontSize: 13, color: 'var(--text-faint)' }}>
          Only an admin can see and change who has access to this workspace. Ask an
          admin in your workspace if you need somebody added.
        </div>
      </div>
    );
  }

  return (
    <div style={{ padding: 24 }}>
      <div style={{
        display: 'flex', alignItems: 'center', justifyContent: 'space-between',
        gap: 12, marginBottom: 4,
      }}>
        <div style={{ fontSize: 15, fontWeight: 650 }}>Members</div>
        <button className="btn primary sm" onClick={() => { setDialogError(null); setDialog({}); }}>
          <I.Plus size={12} />Invite someone
        </button>
      </div>
      <div style={{ fontSize: 12, color: 'var(--text-faint)', marginBottom: 16 }}>
        Everyone who can sign in to this workspace. Changes take effect immediately.
      </div>

      {notice && (
        <div style={{
          fontSize: 12, padding: '8px 10px', marginBottom: 12,
          border: '1px solid var(--border)', borderRadius: 'var(--radius-sm)',
          background: 'var(--surface-2)',
        }}>{notice}</div>
      )}

      {loadError && (
        <div style={{
          fontSize: 12, padding: '8px 10px', marginBottom: 12,
          border: '1px solid var(--error)', borderRadius: 'var(--radius-sm)',
          color: 'var(--error-text)', background: 'var(--error-bg)',
        }}>{loadError}</div>
      )}

      {members === null && !loadError && (
        <div style={{ fontSize: 12, color: 'var(--text-faint)' }}>Loading…</div>
      )}

      {members && members.length > 0 && (
        <div style={{ overflowX: 'auto' }}>
          <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
            <thead>
              <tr style={{ textAlign: 'left', color: 'var(--text-faint)', fontSize: 11 }}>
                <th style={{ padding: '6px 8px', fontWeight: 600 }}>Person</th>
                <th style={{ padding: '6px 8px', fontWeight: 600 }}>Access</th>
                <th style={{ padding: '6px 8px', fontWeight: 600 }}>Languages</th>
                <th style={{ padding: '6px 8px', fontWeight: 600 }}>Status</th>
                <th style={{ padding: '6px 8px', fontWeight: 600 }} />
              </tr>
            </thead>
            <tbody>
              {members.map((m) => (
                <tr key={m.id} style={{ borderTop: '1px solid var(--border)' }}>
                  <td data-label="Person" style={{ padding: '8px' }}>
                    <div style={{ fontWeight: 600 }}>{m.displayName || m.email}</div>
                    {m.displayName && (
                      <div style={{ fontSize: 11.5, color: 'var(--text-faint)' }}>{m.email}</div>
                    )}
                  </td>
                  <td data-label="Access" style={{ padding: '8px' }}>
                    {ROLE_LABEL[m.role] || m.role}
                  </td>
                  <td data-label="Languages" style={{ padding: '8px' }}>
                    {m.role === 'language_reviewer'
                      ? (m.assignedLangs || []).map((c) =>
                          (window.LANG_NAMES && window.LANG_NAMES[c]) || c).join(', ') || '—'
                      : '—'}
                  </td>
                  <td data-label="Status" style={{ padding: '8px' }}>
                    <StatusPill status={m.status} />
                  </td>
                  <td data-label="" style={{ padding: '8px', textAlign: 'right', whiteSpace: 'nowrap' }}>
                    {m.status === 'invited' && (
                      <button className="btn sm ghost" disabled={busy}
                        onClick={() => resend(m)}>Resend</button>
                    )}
                    {/* No `revoked` branch. The server no longer lists removed
                        people (Omar, 2026-08-20), so every row here is a
                        current member. The button that used to sit here said
                        "Invite again" and opened a BLANK invite form — it never
                        re-invited the person it sat beside. Bringing somebody
                        back is the ordinary Invite button, which upserts. */}
                    <button className="btn sm ghost" disabled={busy}
                      onClick={() => { setDialogError(null); setDialog({ member: m }); }}>
                      Change
                    </button>
                    <button className="btn sm danger" disabled={busy}
                      onClick={() => revoke(m)}>Remove</button>
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      )}

      {dialog && (
        <MemberDialog
          member={dialog.member}
          busy={busy}
          error={dialogError}
          onCancel={() => { if (!busy) { setDialog(null); setDialogError(null); } }}
          onSubmit={(form) => (dialog.member ? changeAccess(dialog.member, form) : invite(form))}
        />
      )}
    </div>
  );
}

Object.assign(window, { SurfaceMembers });
