/* Role switcher — floating bottom-left widget that lets a platform admin
   hot-swap between any user identity for testing.

   v1 was localhost-only, used hardcoded seed accounts (admin@aquaos.io
   etc.) and called /api/auth/login. That broke in production because the
   seed accounts don't exist in Supabase Postgres. v2 (this) calls
   /api/auth/impersonable-users on open to fetch real users, and uses
   /api/auth/assume to switch identity without needing passwords —
   gated server-side to aquaos_admin only.

   "Return to admin" works as long as we stash the original session in
   localStorage before assuming, since the assume endpoint overwrites the
   cookie + returned token. */

/* NOTE: TOKEN_KEY and USER_KEY live in api.jsx — text/babel scripts
   share global scope, so redeclaring them here threw
   `Identifier 'TOKEN_KEY' has already been declared` and killed the
   whole role-switcher widget (it failed silently and the user only
   saw the half-rendered "Session expired" badge). Reuse the globals
   instead. ORIGINAL_KEY is unique to this widget. */
const ORIGINAL_KEY = 'aquaos.original_session';

function DevRoleSwitcher() {
  const [open, setOpen] = useState(false);
  const [busy, setBusy] = useState(null);
  const [users, setUsers] = useState(null);   // null = not yet fetched
  const [error, setError] = useState(null);
  const me = Auth.getUser();

  // Bug feedback ff23422a: "logged in as oli@slate, not seeing the
  // account quick switcher … to allow me to quick switch to an org
  // account, site or operator, for testing". The bottom-left badge was
  // easy to miss, so the topbar avatar now dispatches this event when
  // the curator picks "Switch role for testing" — opens the same panel
  // without us forking the impersonation logic into two surfaces.
  useEffect(() => {
    const onOpen = () => setOpen(true);
    window.addEventListener('aquaos:open-role-switcher', onOpen);
    return () => window.removeEventListener('aquaos:open-role-switcher', onOpen);
  }, []);

  /* Render rules:
       1. Local dev hosts always — original behaviour.
       2. Production: render for aquaos_admin OR an explicitly
          whitelisted operator email OR a user who has an
          "original_session" stored (i.e. they've assumed and need a
          way back). When non-admins log in normally they never see it.

     ALWAYS_VISIBLE_EMAILS exists so a platform operator can reach the
     switcher even if their JWT temporarily lists them as a non-admin
     role (e.g. mid-migration, or when they've explicitly stepped down
     to test something). Adding to this list is safer than relaxing
     the role check globally.

     NOTE: visibility flags computed BEFORE the next useEffect so they
     can guard the effect body rather than gating the hook itself —
     conditionally calling/skipping a hook violates React's rules-of-
     hooks and crashes when impersonation state flips (e.g. hasOriginal
     toggles after `Assume role` → next render had different hook count).
     Per the 2026-05-15 audit (CRIT-7). */
  const ALWAYS_VISIBLE_EMAILS = ['oli@slateinteractives.com'];
  const host = typeof window !== 'undefined' ? window.location.hostname : '';
  const isLocalDev = /^(localhost|127\.0\.0\.1|0\.0\.0\.0|aquaos\.local)$/i.test(host)
    || /\.local$/i.test(host);
  const isPlatformAdmin = me && me.role === 'aquaos_admin';
  const myEmail = (me && me.email) ? String(me.email).toLowerCase() : '';
  const isAlwaysVisible = !!myEmail && ALWAYS_VISIBLE_EMAILS.includes(myEmail);
  const hasOriginal = !!localStorage.getItem(ORIGINAL_KEY);
  /* Only render when there's actually a user to switch FROM. Pre-
     auth screens (login, SSO redirect, invite acceptance) showed a
     "DEV · signed out" chip in the corner with no useful action
     behind it — visual noise on a brand surface. Gating on `me`
     hides the switcher until after login, which is when role
     switching becomes meaningful anyway. */
  const shouldRender = !!me && (isLocalDev || isPlatformAdmin || isAlwaysVisible || hasOriginal);

  /* Lazy fetch the user list when the panel opens — avoids hitting the
     endpoint on every page load. Re-fetched each open so a freshly
     added user shows up without a refresh. */
  /* Helper — every call here goes through authenticatePreferBearer
     on the server, which reads the Authorization header first and
     falls back to the cookie. Attaching the Bearer token from
     localStorage avoids the "Session expired" branch when the
     cookie can't authenticate (Secure-flag mismatch on http://
     local dev, SameSite restrictions, etc.). The cookie is still
     sent via credentials:'include' as a belt-and-braces fallback. */
  function authedFetch(url, opts) {
    const token = (typeof localStorage !== 'undefined' && localStorage.getItem(TOKEN_KEY)) || null;
    const headers = Object.assign({}, (opts && opts.headers) || {});
    if (token && !headers['Authorization']) headers['Authorization'] = 'Bearer ' + token;
    return fetch(url, Object.assign({ credentials: 'include' }, opts || {}, { headers }));
  }

  useEffect(() => {
    if (!shouldRender || !open) return;
    setUsers(null); setError(null);
    authedFetch('/api/auth/impersonable-users')
      .then((r) => r.ok ? r.json() : r.json().then((j) => Promise.reject(j)))
      .then((d) => setUsers(d.users || []))
      .catch((e) => setError(e.error || 'Failed to load users'));
  }, [open, shouldRender]);

  if (!shouldRender) return null;

  async function assumeAs(target) {
    setBusy(target.id);
    try {
      // Stash the original session BEFORE the call so a refresh mid-
      // assume doesn't strand us. Don't overwrite if we're already
      // impersonating — keep the very first admin session as the anchor.
      if (!localStorage.getItem(ORIGINAL_KEY)) {
        localStorage.setItem(ORIGINAL_KEY, JSON.stringify({
          token: localStorage.getItem(TOKEN_KEY),
          user: localStorage.getItem(USER_KEY),
        }));
      }
      const res = await authedFetch('/api/auth/assume', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ user_id: target.id }),
      });
      if (!res.ok) {
        const data = await res.json().catch(() => ({}));
        throw new Error(data.error || `${res.status} ${res.statusText}`);
      }
      const data = await res.json();
      Auth.setSession(data.token, data.user);
      const hash = window.location.hash || '#dashboard';
      window.location.hash = hash;
      window.location.reload();
    } catch (err) {
      // Roll back the stashed original if the assume itself failed —
      // otherwise the user gets stuck in a "you're impersonating"
      // visual state without actually being impersonated.
      try { localStorage.removeItem(ORIGINAL_KEY); } catch (_) {}
      window.alert(`Assume role failed: ${err.message}`);
    } finally {
      setBusy(null);
    }
  }

  async function returnToAdmin() {
    const raw = localStorage.getItem(ORIGINAL_KEY);
    if (!raw) return;
    try {
      const orig = JSON.parse(raw);
      const origUser = orig.user ? JSON.parse(orig.user) : null;
      if (orig.token && origUser) {
        // Restore the cookie by hitting /assume on the original admin's
        // own user_id — that re-issues the cookie cleanly. Fallback to
        // localStorage-only if the call fails (cookie may be stale but
        // the UI will recover on next /me check).
        const r = await fetch('/api/auth/assume', {
          method: 'POST', credentials: 'include',
          headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + orig.token },
          body: JSON.stringify({ user_id: origUser.id }),
        });
        if (r.ok) {
          const data = await r.json();
          Auth.setSession(data.token, data.user);
        } else {
          // Server didn't accept — restore raw localStorage and let /me
          // sort it out. The user will likely need to sign in again.
          localStorage.setItem(TOKEN_KEY, orig.token);
          localStorage.setItem(USER_KEY, orig.user);
        }
      }
    } catch (_) { /* ignore corrupted stash */ }
    localStorage.removeItem(ORIGINAL_KEY);
    window.location.reload();
  }

  /* Closed state — small badge with the current role. */
  if (!open) {
    return (
      <button
        onClick={() => setOpen(true)}
        title="Switch demo role"
        style={{
          position: 'fixed', bottom: 12, left: 12, zIndex: 999,
          padding: '6px 10px',
          background: 'var(--aq-surface-2)',
          border: '1px solid var(--aq-line)',
          borderRadius: 7, color: 'var(--aq-text-dim)',
          fontFamily: 'var(--aq-ff-mono)', fontSize: 10.5,
          letterSpacing: '0.06em', cursor: 'pointer',
          display: 'inline-flex', alignItems: 'center', gap: 6,
          boxShadow: 'var(--aq-shadow-1)',
        }}
      >
        <span style={{
          width: 6, height: 6, borderRadius: 99,
          background: 'var(--aq-success)',
        }} />
        DEV · {me ? me.role : 'signed out'}
      </button>
    );
  }

  /* Open panel — list of accounts grouped roughly by role. */
  return (
    <div style={{
      position: 'fixed', bottom: 12, left: 12, zIndex: 999,
      width: 280,
      background: 'var(--aq-surface)',
      border: '1px solid var(--aq-line)',
      borderRadius: 10,
      boxShadow: 'var(--aq-shadow-2)',
      overflow: 'hidden',
    }}>
      <header style={{
        padding: '10px 12px',
        borderBottom: '1px solid var(--aq-line)',
        display: 'flex', alignItems: 'center', gap: 8,
        background: 'var(--aq-surface-2)',
      }}>
        <div style={{ flex: 1 }}>
          <div style={{
            fontFamily: 'var(--aq-ff-mono)', fontSize: 9.5,
            letterSpacing: '0.08em', textTransform: 'uppercase',
            color: 'var(--aq-text-faint)',
          }}>Dev role switcher</div>
          <div style={{ fontSize: 12, color: 'var(--aq-text)', marginTop: 2 }}>
            Currently: <b>{me ? me.email : '—'}</b>
          </div>
        </div>
        <button
          onClick={() => setOpen(false)}
          aria-label="Close"
          style={{
            width: 22, height: 22, borderRadius: 4,
            background: 'transparent', border: 0,
            color: 'var(--aq-text-faint)', cursor: 'pointer',
            display: 'grid', placeItems: 'center',
          }}
        ><Icon name="close" size={11} /></button>
      </header>

      <div style={{ padding: 4, maxHeight: 360, overflowY: 'auto' }}>
        {users === null && (
          <div style={{ padding: 16, color: 'var(--aq-text-faint)', fontSize: 12 }}>Loading users…</div>
        )}
        {error && (
          <div style={{ padding: 12, color: 'var(--aq-warn)', fontSize: 12 }}>
            {error}
            {error.match(/Platform admin only|403|Forbidden/i) ? '. Sign in as a platform admin to use this.' : ''}
          </div>
        )}
        {users && users.length === 0 && !error && (
          <div style={{ padding: 16, color: 'var(--aq-text-faint)', fontSize: 12 }}>No impersonable users found.</div>
        )}
        {users && users.map((u) => {
          const isMe = me && me.id === u.id;
          const initials = (u.role || '').split('_').map((s) => (s && s[0] ? s[0].toUpperCase() : '')).join('');
          const orgLabel = u.org_name || (u.role === 'aquaos_admin' ? 'platform' : '');
          return (
            <button
              key={u.id}
              onClick={() => !isMe && assumeAs(u)}
              disabled={isMe || busy != null}
              style={{
                width: '100%', textAlign: 'left',
                display: 'flex', alignItems: 'center', gap: 10,
                padding: '8px 10px', borderRadius: 6,
                background: isMe ? 'var(--aq-accent-soft)' : 'transparent',
                border: 0, cursor: isMe ? 'default' : 'pointer',
                color: 'var(--aq-text)', fontFamily: 'inherit',
              }}
              onMouseEnter={(e) => { if (!isMe) e.currentTarget.style.background = 'var(--aq-surface-2)'; }}
              onMouseLeave={(e) => { if (!isMe) e.currentTarget.style.background = 'transparent'; }}
            >
              <span style={{
                width: 28, height: 28, borderRadius: 6,
                background: 'var(--aq-surface-2)',
                color: 'var(--aq-text-dim)',
                display: 'grid', placeItems: 'center',
                fontFamily: 'var(--aq-ff-mono)', fontSize: 10,
                flexShrink: 0,
              }}>{initials}</span>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontSize: 12.5, fontWeight: 500, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', textTransform: 'capitalize' }}>
                  {(u.role || '').replace(/_/g, ' ')}
                </div>
                <div style={{
                  fontSize: 11, color: 'var(--aq-text-faint)',
                  whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
                }}>
                  {u.email}{orgLabel ? ` · ${orgLabel}` : ''}
                </div>
              </div>
              {isMe && (
                <span style={{
                  fontFamily: 'var(--aq-ff-mono)', fontSize: 9,
                  letterSpacing: '0.06em',
                  color: 'var(--aq-accent)',
                }}>YOU</span>
              )}
              {busy === u.id && (
                <span style={{ fontSize: 11, color: 'var(--aq-text-faint)' }}>…</span>
              )}
            </button>
          );
        })}
      </div>

      {hasOriginal && (
        <button
          onClick={returnToAdmin}
          disabled={busy != null}
          style={{
            display: 'block', width: '100%',
            padding: '8px 12px',
            background: 'var(--aq-accent-soft)', color: 'var(--aq-text)',
            border: 0, borderTop: '1px solid var(--aq-line)',
            cursor: busy != null ? 'wait' : 'pointer',
            fontFamily: 'inherit', fontSize: 12, textAlign: 'left',
          }}
        >
          ↩ Return to original admin
        </button>
      )}

      <footer style={{
        padding: '8px 12px', borderTop: '1px solid var(--aq-line)',
        background: 'var(--aq-surface-2)',
        fontFamily: 'var(--aq-ff-mono)', fontSize: 9.5,
        letterSpacing: '0.06em', color: 'var(--aq-text-faint)',
      }}>
        Platform admin · server-issued sessions
      </footer>
    </div>
  );
}

window.DevRoleSwitcher = DevRoleSwitcher;
