/* global React, ReactDOM, supabase, Button, LoadingSpinner, Icon */
const {
  createContext: createContextApi,
  useContext: useContextApi,
  useEffect: useEffectApi,
  useMemo: useMemoApi,
  useRef: useRefApi,
  useState: useStateApi,
} = React;

const AuthContext = createContextApi(null);
const ACCESS_TOKEN_KEY = 'mcp_tester_access_token';
const AUTH_NEXT_PATH_KEY = 'mcp_tester_auth_next_path';
const AUTH_NEXT_PATH_FLOW_KEY = 'mcp_tester_auth_next_path_flow';
const AUTH_NEXT_PATH_TTL_MS = 15 * 60 * 1000;
const AUTH_CALLBACK_PATH = '/auth/callback';
// Legacy pathname produced by an outdated Supabase Site URL. Recognized so
// users mid-OAuth can be rescued instead of stranded. Code-level rescue;
// canonical fix is updating the Supabase dashboard Site URL.
const LEGACY_AUTH_CALLBACK_PATH = '/workspace/workspace';
const AUTH_CALLBACK_FLOW_PARAM = 'auth_flow';
const AUTH_PKCE_RETRY_PARAM = 'pkce_retry';
const AUTH_PKCE_RETRY_NEXT_PATH_KEY = 'mcp_tester_auth_pkce_retry_next_path';
const INVITATION_TOKEN_RE = /^[A-Za-z0-9_-]{43}$/;
const SAFE_RETURN_PATH_RE = /^\/[A-Za-z0-9_\-/?&=.%+]*$/;
const ACTIVE_SUBSCRIPTION_STATUSES = new Set(['active', 'trialing']);
const TERMINAL_ONBOARDING_STEPS = new Set(['done', 'dashboard']);
const SUPABASE_AUTH_TOKEN_HASH_KEYS = ['access_token', 'refresh_token'];
const STALE_ORG_SELECTION_KEYS = [
  'mcp_tester_selected_organization_id',
  'mcp_tester_selected_org_id',
  'selectedOrganizationId',
  'selectedOrgId',
];
const PLAN_RESOURCE_LABELS = {
  mcpServers: 'MCP servers or CLIs',
  toolMonitors: 'tool monitors',
  scheduledWorkflows: 'long-running workflows',
};
const TENANT_ACTION_ROLES = {
  editor: ['editor', 'admin', 'owner'],
  admin: ['admin', 'owner'],
};

function getBillingEntitlements(auth) {
  return auth?.me?.billingEntitlements || null;
}

function getPlanLimitState(auth, resource, increment = 1) {
  const entitlements = getBillingEntitlements(auth);
  if (!entitlements?.limits || !entitlements?.usage) {
    return { known: false, allowed: true, usage: null, limit: null, remaining: null, message: '' };
  }
  const usage = entitlements.usage[resource] ?? 0;
  const limit = entitlements.limits[resource] ?? null;
  if (limit == null) {
    return { known: true, allowed: true, usage, limit, remaining: Infinity, message: '' };
  }
  const remaining = Math.max(0, limit - usage);
  const allowed = usage + increment <= limit;
  return {
    known: true,
    allowed,
    usage,
    limit,
    remaining,
    message: allowed ? '' : `${PLAN_RESOURCE_LABELS[resource] || resource} limit reached (${usage}/${limit}).`,
  };
}

function workflowMinIntervalSeconds(auth) {
  return getBillingEntitlements(auth)?.limits?.workflowMinIntervalSeconds || 300;
}

function cronIntervalSeconds(cron) {
  const parts = String(cron || '').trim().split(/\s+/);
  if (parts.length !== 5) return null;
  const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
  if (dayOfMonth !== '*' || month !== '*' || dayOfWeek !== '*') return null;
  const every = minute.match(/^\*\/(\d+)$/);
  if (every && hour === '*') return Number(every[1]) * 60;
  if (/^\d+$/.test(minute) && hour === '*') return 3600;
  if (/^\d+$/.test(minute) && /^\d+$/.test(hour)) return 86400;
  return null;
}

function cronAllowedByPlan(auth, cron) {
  const interval = cronIntervalSeconds(cron);
  return interval == null || interval >= workflowMinIntervalSeconds(auth);
}

function canUseTenantAction(auth, requiredRole = 'editor') {
  const roles = TENANT_ACTION_ROLES[requiredRole];
  return Array.isArray(roles) && roles.includes(auth?.me?.role);
}

function tenantActionDisabledReason(auth, requiredRole = 'editor') {
  if (canUseTenantAction(auth, requiredRole)) return '';
  if (auth?.me?.role === 'viewer') return 'Viewers have read-only access.';
  if (requiredRole === 'admin') return 'Only admins and owners can perform this action.';
  return 'Your role cannot perform this action.';
}

function getAuthRedirectUrl(nextPath, flowId = '') {
  const url = new URL(AUTH_CALLBACK_PATH, window.location.origin);
  const normalizedNextPath = nextPath ? normalizeAuthNextPath(nextPath) : '';
  if (normalizedNextPath && normalizedNextPath !== '/dashboard') {
    url.searchParams.set('next', normalizedNextPath);
    if (flowId) {
      url.searchParams.set(AUTH_CALLBACK_FLOW_PARAM, flowId);
    }
  }
  return url.toString();
}

function getAuthRedirectUrlWithPkceRetry(nextPath, flowId = '') {
  const url = new URL(getAuthRedirectUrl(nextPath, flowId));
  url.searchParams.set(AUTH_PKCE_RETRY_PARAM, '1');
  return url.toString();
}

function normalizeAuthNextPath(path) {
  const value = String(path || '').trim();
  if (!value) return '/dashboard';
  const normalized = value.startsWith('/') ? value : `/${value}`;
  if (normalized.startsWith('//')) return '/dashboard';
  if (normalized === AUTH_CALLBACK_PATH || normalized.startsWith(`${AUTH_CALLBACK_PATH}?`)) return '/dashboard';
  if (normalized === LEGACY_AUTH_CALLBACK_PATH || normalized.startsWith(`${LEGACY_AUTH_CALLBACK_PATH}?`)) return '/dashboard';
  return normalized;
}

function getStorageItem(storage, key) {
  try {
    return storage.getItem(key);
  } catch {
    return null;
  }
}

function setStorageItem(storage, key, value) {
  try {
    storage.setItem(key, value);
  } catch {
    // Storage can be unavailable in private browsing; callback query next remains the primary handoff.
  }
}

function removeStorageItem(storage, key) {
  try {
    storage.removeItem(key);
  } catch {
    // Ignore storage cleanup failures.
  }
}

function createAuthNextPathFlowId() {
  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
}

function parseStoredAuthNextPath(value) {
  if (!value) return null;
  try {
    const parsed = JSON.parse(value);
    if (!parsed || typeof parsed !== 'object') return null;
    if (typeof parsed.nextPath !== 'string') return null;
    if (typeof parsed.flowId !== 'string') return null;
    if (typeof parsed.expiresAt !== 'number') return null;
    return parsed;
  } catch {
    return null;
  }
}

function getAuthCallbackFlowId() {
  if (!isAuthCallbackUrl()) return '';
  return new URLSearchParams(window.location.search).get(AUTH_CALLBACK_FLOW_PARAM) || '';
}

// Paths that are safe to recover from localStorage when the auth callback
// arrives in a fresh tab/browser whose sessionStorage is empty but whose
// `auth_flow=` query param matches our stored flow id. This covers email-link
// flows that necessarily cross tabs (the user opens the email in a different
// tab/browser than where they kicked off the request):
//   - /invite/<token>/accept  — invite acceptance via OAuth or email link.
//   - /reset-password         — Supabase recovery email link, where some
//                                Supabase configurations don't preserve
//                                arbitrary query params on `redirect_to`,
//                                stripping `next=/reset-password` and leaving
//                                only `auth_flow=` for us to match against.
// Any path we return here flows through normalizeAuthNextPath, so it is
// already restricted to in-app routes.
function isCrossTabRecoverableNextPath(path) {
  if (!path) return false;
  if (isInviteAcceptPath(path)) return true;
  if (validateAuthNextTarget(path)) return true;
  if (path === '/reset-password' || path.startsWith('/reset-password?')) return true;
  return false;
}

function readLocalAuthNextPath() {
  const stored = getStorageItem(window.localStorage, AUTH_NEXT_PATH_KEY);
  const payload = parseStoredAuthNextPath(stored);
  if (!payload || payload.expiresAt <= Date.now()) {
    if (stored) removeStorageItem(window.localStorage, AUTH_NEXT_PATH_KEY);
    return null;
  }
  const activeFlowId = getStorageItem(window.sessionStorage, AUTH_NEXT_PATH_FLOW_KEY);
  if (activeFlowId && payload.flowId === activeFlowId) return payload.nextPath;
  const callbackFlowId = getAuthCallbackFlowId();
  if (callbackFlowId && payload.flowId === callbackFlowId && isCrossTabRecoverableNextPath(payload.nextPath)) {
    return payload.nextPath;
  }
  return null;
}

function readAuthNextPath() {
  return getStorageItem(window.sessionStorage, AUTH_NEXT_PATH_KEY)
    || readLocalAuthNextPath();
}

function clearAuthNextPath() {
  const activeFlowId = getStorageItem(window.sessionStorage, AUTH_NEXT_PATH_FLOW_KEY);
  const callbackFlowId = getAuthCallbackFlowId();
  removeStorageItem(window.sessionStorage, AUTH_NEXT_PATH_KEY);
  removeStorageItem(window.sessionStorage, AUTH_NEXT_PATH_FLOW_KEY);
  const stored = getStorageItem(window.localStorage, AUTH_NEXT_PATH_KEY);
  const payload = parseStoredAuthNextPath(stored);
  if (
    !payload
    || payload.expiresAt <= Date.now()
    || (activeFlowId && payload.flowId === activeFlowId)
    || (callbackFlowId && payload.flowId === callbackFlowId)
  ) {
    removeStorageItem(window.localStorage, AUTH_NEXT_PATH_KEY);
  }
}

function setAuthNextPath(path) {
  const nextPath = normalizeAuthNextPath(path);
  const flowId = createAuthNextPathFlowId();
  setStorageItem(window.sessionStorage, AUTH_NEXT_PATH_KEY, nextPath);
  setStorageItem(window.sessionStorage, AUTH_NEXT_PATH_FLOW_KEY, flowId);
  setStorageItem(window.localStorage, AUTH_NEXT_PATH_KEY, JSON.stringify({
    nextPath,
    flowId,
    expiresAt: Date.now() + AUTH_NEXT_PATH_TTL_MS,
  }));
  return flowId;
}

function consumeAuthNextPath() {
  const nextPath = readAuthNextPath();
  clearAuthNextPath();
  return nextPath ? normalizeAuthNextPath(nextPath) : '/dashboard';
}

function routePathFromBrowserLocation() {
  if (typeof getBrowserRoutePath === 'function') {
    return getBrowserRoutePath();
  }
  const pathname = window.location.pathname || '/';
  if (pathname && pathname !== '/' && pathname !== AUTH_CALLBACK_PATH) {
    return `${pathname}${window.location.search || ''}`;
  }
  if (window.location.hash?.startsWith('#/')) {
    return window.location.hash.slice(1);
  }
  return '/dashboard';
}

function pushBrowserPath(path) {
  const nextPath = normalizeAuthNextPath(path);
  if (typeof pushBrowserRoute === 'function') {
    pushBrowserRoute(nextPath);
    return nextPath;
  }
  window.history.pushState(null, document.title, nextPath);
  window.dispatchEvent(new Event('armature:routechange'));
  return nextPath;
}

function navigateToPath(path) {
  pushBrowserPath(path);
}

function validateBrowserReturnPath(value) {
  const path = String(value || '').trim();
  if (!path) return null;
  if (!SAFE_RETURN_PATH_RE.test(path)) return null;
  if (path.startsWith('//')) return null;
  if (path.startsWith('\\\\') || path.includes('\\')) return null;
  if (path.includes(':')) return null;
  if (path.includes('#')) return null;
  if (path === AUTH_CALLBACK_PATH || path.startsWith(`${AUTH_CALLBACK_PATH}?`)) return null;
  if (path === LEGACY_AUTH_CALLBACK_PATH || path.startsWith(`${LEGACY_AUTH_CALLBACK_PATH}?`)) return null;
  return path;
}

function validateAuthNextTarget(value) {
  const path = String(value || '').trim();
  if (!path) return null;
  if (!path.startsWith('/api/mcp/oauth/authorize?')) return null;
  if (path.startsWith('//') || path.includes('\\') || path.includes(':') || path.includes('#')) return null;
  try {
    const url = new URL(path, window.location.origin);
    if (url.origin !== window.location.origin) return null;
    if (url.pathname !== '/api/mcp/oauth/authorize') return null;
    return `${url.pathname}${url.search}`;
  } catch {
    return null;
  }
}

function currentBrowserReturnPath() {
  return validateBrowserReturnPath(routePathFromBrowserLocation()) || '/dashboard';
}

function navigateToBrowserPath(path) {
  const nextPath = validateBrowserReturnPath(path) || '/dashboard';
  pushBrowserPath(nextPath);
}

function emitPaymentRequired(details = {}) {
  window.dispatchEvent(new CustomEvent('armature:payment-required', {
    detail: {
      subscriptionStatus: details?.subscriptionStatus,
      currentStep: details?.currentStep,
      billingPath: validateBrowserReturnPath(details?.billingPath) || '/settings/billing',
      returnTo: validateBrowserReturnPath(details?.returnTo) || currentBrowserReturnPath(),
    },
  }));
}

function emitTenantActionDenied(auth, requiredRole = 'editor') {
  window.dispatchEvent(new CustomEvent('armature:tenant-action-denied', {
    detail: {
      requiredRole,
      message: tenantActionDisabledReason(auth, requiredRole),
    },
  }));
}

function extractInvitationToken(value) {
  const raw = String(value || '').trim();
  if (INVITATION_TOKEN_RE.test(raw)) return raw;

  const hashMatch = raw.match(/#\/invite\/([A-Za-z0-9_-]{43})(?:[/?#].*)?$/);
  if (hashMatch) return hashMatch[1];

  const pathMatch = raw.match(/\/invite\/([A-Za-z0-9_-]{43})(?:[/?#].*)?$/);
  if (pathMatch) return pathMatch[1];

  try {
    const url = new URL(raw);
    const urlHashMatch = url.hash.match(/^#\/invite\/([A-Za-z0-9_-]{43})(?:[/?#].*)?$/);
    if (urlHashMatch) return urlHashMatch[1];
    const urlPathMatch = url.pathname.match(/\/invite\/([A-Za-z0-9_-]{43})$/);
    if (urlPathMatch) return urlPathMatch[1];
  } catch (error) {
    // Not a URL; fall through to null.
  }
  return null;
}

function isLegacyAuthCallbackUrl() {
  return window.location.pathname === LEGACY_AUTH_CALLBACK_PATH;
}

function isAuthCallbackUrl() {
  return window.location.pathname === AUTH_CALLBACK_PATH || isLegacyAuthCallbackUrl();
}

function getSupabaseAuthHashParams() {
  if (!window.location.hash) return new URLSearchParams();
  const hash = window.location.hash.replace(/^#/, '');
  if (!hash || hash.startsWith('/')) return new URLSearchParams();
  return new URLSearchParams(hash);
}

function hasSupabaseAuthTokenHash() {
  const params = getSupabaseAuthHashParams();
  return SUPABASE_AUTH_TOKEN_HASH_KEYS.some((key) => params.has(key));
}

function getAuthCallbackError() {
  const searchParams = new URLSearchParams(window.location.search);
  const hashParams = getSupabaseAuthHashParams();
  const errorDescription = searchParams.get('error_description')
    || hashParams.get('error_description')
    || searchParams.get('error')
    || hashParams.get('error');
  if (!errorDescription) return null;
  return errorDescription;
}

function finishAuthCallbackUrl(callbackNextPath) {
  const nextPath = callbackNextPath ? normalizeAuthNextPath(callbackNextPath) : consumeAuthNextPath();
  if (callbackNextPath) clearAuthNextPath();
  clearPkceRetryMarker();
  if (validateAuthNextTarget(nextPath)) {
    window.location.assign(nextPath);
    return;
  }
  window.history.replaceState(null, document.title, nextPath);
  window.dispatchEvent(new Event('armature:routechange'));
}

function getAuthCallbackHashRoute() {
  if (!window.location.hash || !window.location.hash.startsWith('#/')) return null;
  return normalizeAuthNextPath(window.location.hash.slice(1));
}

function isInviteAcceptPath(path) {
  return /^\/invite\/[A-Za-z0-9_-]{43}\/accept(?:[?#].*)?$/.test(String(path || ''));
}

// True if the current bootstrap is heading to an invite-accept route. This
// must be callable before completeAuthCallback rewrites /auth/callback to the
// final route, because Supabase may emit onAuthStateChange during PKCE exchange.
function isInviteAcceptBootstrapTarget() {
  if (isInviteAcceptPath(window.location.pathname)) return true;
  if (isAuthCallbackUrl()) {
    const params = new URLSearchParams(window.location.search);
    const nextParam = params.get('next');
    if (nextParam && isInviteAcceptPath(normalizeAuthNextPath(nextParam))) return true;
    const hashRoute = getAuthCallbackHashRoute();
    if (hashRoute && isInviteAcceptPath(hashRoute)) return true;
  }
  const sessionNextPath = getStorageItem(window.sessionStorage, AUTH_NEXT_PATH_KEY);
  if (sessionNextPath && isInviteAcceptPath(sessionNextPath)) return true;
  return false;
}

const SUPABASE_OTP_TYPES = new Set(['signup', 'email_change', 'recovery', 'invite', 'magiclink', 'email']);
let activeAuthCallbackCompletion = null;
let activePkceRetryNextPath = null;

async function completeAuthCallback(client) {
  const hasCallbackPayload = isAuthCallbackUrl() || hasSupabaseAuthTokenHash();
  if (!hasCallbackPayload) return null;
  const key = `${window.location.pathname}${window.location.search}${window.location.hash}`;
  if (activeAuthCallbackCompletion?.key === key) {
    return activeAuthCallbackCompletion.promise;
  }
  const promise = completeAuthCallbackOnce(client);
  activeAuthCallbackCompletion = { key, promise };
  try {
    return await promise;
  } finally {
    if (activeAuthCallbackCompletion?.promise === promise) {
      activeAuthCallbackCompletion = null;
    }
  }
}

function pkceCodeVerifierStorageKey(client) {
  const storageKey = client?.auth?.storageKey;
  return storageKey ? `${storageKey}-code-verifier` : null;
}

function hasPkceCodeVerifier(client) {
  const key = pkceCodeVerifierStorageKey(client);
  // Older or mocked Supabase clients may not expose storageKey. In that case
  // don't preflight-block the exchange; let Supabase return the authoritative
  // error and recover from it below if needed.
  if (!key) return true;
  return Boolean(getStorageItem(window.localStorage, key));
}

function isMissingPkceCodeVerifierError(error) {
  const code = String(error?.code || error?.error || error?.name || '').toLowerCase();
  const status = Number(error?.status || 0);
  const message = String(error?.message || error || '').toLowerCase();
  const hasVerifierSignal = /code[_\s-]?verifier|pkce/.test(`${code} ${message}`);
  if (!hasVerifierSignal) return false;
  if (/code[_\s-]?verifier|pkce/.test(code)) return true;
  if (status === 400) return true;
  return /(auth(?:orization)?[_\s-]?code|non[_\s-]?empty|missing|required|empty)/.test(message);
}

function invitePreviewPathFromAcceptPath(path) {
  const match = String(path || '').match(/^(\/invite\/[A-Za-z0-9_-]{43})\/accept(?:[?#].*)?$/);
  return match ? match[1] : null;
}

function resolveInviteAcceptRecoveryPath(callbackNextPath) {
  const explicitPath = callbackNextPath ? normalizeAuthNextPath(callbackNextPath) : null;
  if (explicitPath && isInviteAcceptPath(explicitPath)) return explicitPath;
  const hashRoute = getAuthCallbackHashRoute();
  if (hashRoute && isInviteAcceptPath(hashRoute)) return hashRoute;
  const storedPath = readAuthNextPath();
  return storedPath && isInviteAcceptPath(storedPath) ? normalizeAuthNextPath(storedPath) : null;
}

function isPkceRetryRecoverablePath(path) {
  return isInviteAcceptPath(path) || Boolean(validateAuthNextTarget(path));
}

function resolvePkceRetryRecoveryPath(callbackNextPath) {
  const explicitPath = callbackNextPath ? normalizeAuthNextPath(callbackNextPath) : null;
  if (explicitPath && isPkceRetryRecoverablePath(explicitPath)) return explicitPath;
  const hashRoute = getAuthCallbackHashRoute();
  if (hashRoute && isPkceRetryRecoverablePath(hashRoute)) return hashRoute;
  const storedPath = readAuthNextPath();
  return storedPath && isPkceRetryRecoverablePath(storedPath) ? normalizeAuthNextPath(storedPath) : null;
}

function hasPkceRetryMarker(callbackNextPath) {
  if (!callbackNextPath) return false;
  const params = new URLSearchParams(window.location.search);
  if (params.get(AUTH_PKCE_RETRY_PARAM) === '1') return true;
  if (activePkceRetryNextPath === callbackNextPath) return true;
  return getStorageItem(window.sessionStorage, AUTH_PKCE_RETRY_NEXT_PATH_KEY) === callbackNextPath;
}

function markPkceRetry(callbackNextPath) {
  activePkceRetryNextPath = callbackNextPath;
  setStorageItem(window.sessionStorage, AUTH_PKCE_RETRY_NEXT_PATH_KEY, callbackNextPath);
}

function clearPkceRetryMarker() {
  activePkceRetryNextPath = null;
  removeStorageItem(window.sessionStorage, AUTH_PKCE_RETRY_NEXT_PATH_KEY);
}

async function retryOAuthAfterMissingPkceVerifier(client, callbackNextPath) {
  if (!callbackNextPath || !isPkceRetryRecoverablePath(callbackNextPath)) return false;
  if (hasPkceRetryMarker(callbackNextPath)) {
    const invitePreviewPath = invitePreviewPathFromAcceptPath(callbackNextPath);
    finishAuthCallbackUrl(invitePreviewPath || `/signin?next=${encodeURIComponent(callbackNextPath)}`);
    return true;
  }
  markPkceRetry(callbackNextPath);
  const flowId = setAuthNextPath(callbackNextPath);
  const result = await client.auth.signInWithOAuth({
    provider: 'google',
    options: {
      redirectTo: getAuthRedirectUrlWithPkceRetry(callbackNextPath, flowId),
    },
  });
  if (result.error) throw result.error;
  return true;
}

async function recoverMissingPkceCodeVerifier(client, callbackNextPath) {
  const existingSessionResult = await client.auth.getSession?.();
  const existingSession = existingSessionResult?.data?.session || null;
  if (existingSession) {
    finishAuthCallbackUrl(callbackNextPath);
    return { recovered: true, session: existingSession };
  }
  if (await retryOAuthAfterMissingPkceVerifier(client, callbackNextPath)) {
    return { recovered: true, session: null };
  }
  return { recovered: false, session: null };
}

async function completeAuthCallbackOnce(client) {
  const hasTokenHash = hasSupabaseAuthTokenHash();
  if (!isAuthCallbackUrl() && !hasTokenHash) return null;

  const callbackError = getAuthCallbackError();
  if (callbackError) throw new Error(callbackError);

  const isLegacy = isLegacyAuthCallbackUrl();
  // Same-tab intent for the legacy URL means sessionStorage carries an
  // unconsumed next-path AND its flow id matches the localStorage payload's
  // flow id — the same invariant readLocalAuthNextPath enforces. Cross-tab or
  // mismatched-flow entries are not honored by the legacy rescue.
  const sessionFlowId = getStorageItem(window.sessionStorage, AUTH_NEXT_PATH_FLOW_KEY);
  const sessionNextPath = getStorageItem(window.sessionStorage, AUTH_NEXT_PATH_KEY);
  const localPayload = parseStoredAuthNextPath(getStorageItem(window.localStorage, AUTH_NEXT_PATH_KEY));
  const hasSameTabIntent = Boolean(
    sessionNextPath
    && sessionFlowId
    && localPayload
    && localPayload.flowId === sessionFlowId
    && localPayload.expiresAt > Date.now(),
  );

  const searchParams = new URLSearchParams(window.location.search);
  const callbackNextPath = searchParams.get('next') ? normalizeAuthNextPath(searchParams.get('next')) : null;
  const inviteAcceptRecoveryPath = resolveInviteAcceptRecoveryPath(callbackNextPath);
  const code = searchParams.get('code');
  const otpTokenHash = searchParams.get('token_hash');
  const otpType = searchParams.get('type');
  const hasCallbackPayload = Boolean(code || (otpTokenHash && otpType && SUPABASE_OTP_TYPES.has(otpType)) || hasTokenHash);

  if (isAuthCallbackUrl() && !hasCallbackPayload) {
    // Canonical /auth/callback path: existing rescue rules — invite-accept
    // intent or an existing session may carry the user through.
    // Legacy /workspace/workspace path: stricter — require same-tab intent
    // or an existing session, else fail closed to /signin.
    if (!isLegacy && inviteAcceptRecoveryPath) {
      finishAuthCallbackUrl(inviteAcceptRecoveryPath);
      return null;
    }
    const existingSessionResult = await client.auth.getSession?.();
    const existingSession = existingSessionResult?.data?.session || null;
    if (existingSession) {
      if (isLegacy && !hasSameTabIntent) {
        // Authenticated user, but legacy URL has no fresh intent. Clear any
        // stale storage (parity with the PKCE/OTP !sameTab branches below)
        // and send to /dashboard rather than consuming a possibly-stale path.
        clearAuthNextPath();
        window.history.replaceState(null, document.title, '/dashboard');
        window.dispatchEvent(new Event('armature:routechange'));
        return existingSession;
      }
      finishAuthCallbackUrl(callbackNextPath);
      return existingSession;
    }
    if (isLegacy) {
      window.history.replaceState(null, document.title, '/signin');
      window.dispatchEvent(new Event('armature:routechange'));
      throw new Error('Authentication link is missing or has expired. Start again from this page.');
    }
    const fallbackPath = getAuthCallbackHashRoute() || (callbackNextPath?.startsWith('/onboarding') ? '/signup' : '/signin');
    finishAuthCallbackUrl(fallbackPath);
    throw new Error('Authentication link is missing or has expired. Start again from this page.');
  }

  // Email confirmation links open in a fresh tab with no sessionStorage, so
  // the legacy URL must still honor an explicit ?next=/invite/<token>/accept.
  // Restricted to invite-accept paths because the legacy URL is the rescue
  // lane — an arbitrary ?next= must NOT be honored from a path we treat as
  // possibly attacker-controlled.
  const legacyInviteRescuePath = isLegacy && callbackNextPath && isInviteAcceptPath(callbackNextPath)
    ? callbackNextPath
    : null;

  if (code) {
    if (!hasPkceCodeVerifier(client)) {
      const recovery = await recoverMissingPkceCodeVerifier(client, resolvePkceRetryRecoveryPath(callbackNextPath));
      if (recovery.recovered) return recovery.session;
    }
    const result = await client.auth.exchangeCodeForSession(code);
    if (result.error) {
      if (isMissingPkceCodeVerifierError(result.error)) {
        const recovery = await recoverMissingPkceCodeVerifier(client, resolvePkceRetryRecoveryPath(callbackNextPath));
        if (recovery.recovered) return recovery.session;
      }
      throw result.error;
    }
    if (isLegacy && !hasSameTabIntent) {
      if (legacyInviteRescuePath) {
        finishAuthCallbackUrl(legacyInviteRescuePath);
        return result.data.session || null;
      }
      // Successful PKCE handshake from the legacy URL with no fresh same-tab
      // intent and no invite-accept ?next= — authenticate the user but route
      // to /dashboard rather than consuming whatever stale next-path may sit
      // in localStorage.
      clearAuthNextPath();
      window.history.replaceState(null, document.title, '/dashboard');
      window.dispatchEvent(new Event('armature:routechange'));
      return result.data.session || null;
    }
    finishAuthCallbackUrl(callbackNextPath);
    return result.data.session || null;
  }

  if (otpTokenHash && otpType && SUPABASE_OTP_TYPES.has(otpType)) {
    const result = await client.auth.verifyOtp({ type: otpType, token_hash: otpTokenHash });
    if (result.error) throw result.error;
    if (isLegacy && !hasSameTabIntent) {
      if (legacyInviteRescuePath) {
        finishAuthCallbackUrl(legacyInviteRescuePath);
        return result.data.session || null;
      }
      clearAuthNextPath();
      window.history.replaceState(null, document.title, '/dashboard');
      window.dispatchEvent(new Event('armature:routechange'));
      return result.data.session || null;
    }
    finishAuthCallbackUrl(callbackNextPath);
    return result.data.session || null;
  }

  // Refuse OAuth implicit-flow tokens from window.location.hash. The SPA
  // uses PKCE (flowType: 'pkce'), so `code` above is the only legitimate
  // path. Accepting a raw access/refresh token from the URL hash would let
  // any attacker who can drop a victim onto
  // app.armature.tech/auth/callback#access_token=… log them into an
  // attacker-controlled Supabase session and harvest whatever the victim
  // types into the dashboard. Drop the values from the URL and surface
  // an error so the user notices.
  const hashParams = getSupabaseAuthHashParams();
  if (hashParams.get('access_token') || hashParams.get('refresh_token')) {
    if (isAuthCallbackUrl()) finishAuthCallbackUrl(callbackNextPath);
    throw new Error('Authentication response contained an unexpected hash payload. Restart the sign-in flow.');
  }

  return null;
}

async function parseApiResponse(response) {
  const text = await response.text();
  let data = null;
  if (text) {
    try {
      data = JSON.parse(text);
    } catch (parseError) {
      if (response.ok) throw parseError;
      data = null;
    }
  }
  if (!response.ok) {
    /** @type {Error & { status?: number, code?: string, details?: unknown }} */
    const error = new Error(data?.error?.message || `Request failed with ${response.status}`);
    error.status = response.status;
    error.code = data?.error?.code || data?.errorCode || 'request_failed';
    error.details = data?.error?.details || data?.details;
    const details = /** @type {Record<string, any>} */ (
      error.details && typeof error.details === 'object' ? error.details : {}
    );
    if (error.code === 'payment_required') {
      emitPaymentRequired(details);
    }
    if (error.code === 'plan_limit_exceeded') {
      window.dispatchEvent(new CustomEvent('armature:plan-limit-exceeded', {
        detail: error.details || {},
      }));
    }
    if (error.code === 'onboarding_incomplete' && details?.redirectPath) {
      const redirectPath = validateBrowserReturnPath(details.redirectPath);
      if (redirectPath) {
        window.dispatchEvent(new CustomEvent('armature:onboarding-incomplete', {
          detail: { ...details, redirectPath },
        }));
        navigateToBrowserPath(redirectPath);
      }
    }
    throw error;
  }
  return data;
}

function requirePaidAction(auth, navigate, options = {}) {
  const requiredRole = options?.requiredRole;
  if (requiredRole && !canUseTenantAction(auth, requiredRole)) {
    emitTenantActionDenied(auth, requiredRole);
    return false;
  }

  const status = auth?.me?.organization?.subscriptionStatus;
  const step = auth?.me?.profile?.onboardingState?.currentStep;
  const onboardingComplete = TERMINAL_ONBOARDING_STEPS.has(step);
  const returnTo = currentBrowserReturnPath();

  if (status === 'skipped' && !onboardingComplete) {
    navigate?.('/onboarding/role');
    return false;
  }

  if (!ACTIVE_SUBSCRIPTION_STATUSES.has(status)) {
    const details = {
      subscriptionStatus: status,
      currentStep: step,
      billingPath: '/settings/billing',
      returnTo,
    };
    if (auth?.me?.role === 'owner') {
      navigate?.(`/settings/billing?source=paywall&return_to=${encodeURIComponent(returnTo)}`);
    } else {
      emitPaymentRequired(details);
    }
    return false;
  }

  if (step === 'workspace' || step === 'plan') {
    navigate?.(`/onboarding/${step}`);
    return false;
  }

  return true;
}

function getStoredAccessToken() {
  try {
    return window.localStorage.getItem(ACCESS_TOKEN_KEY);
  } catch {
    return null;
  }
}

function getStoredDevAccessToken() {
  const token = getStoredAccessToken();
  return token?.startsWith('dev:') ? token : null;
}

function hasStoredSupabaseSession() {
  try {
    for (let i = 0; i < window.localStorage.length; i += 1) {
      const key = window.localStorage.key(i);
      if (key?.startsWith('sb-') && key.endsWith('-auth-token')) return true;
    }
  } catch {
    return false;
  }
  return false;
}

function hasInFlightOAuthFlow() {
  // Three signals indicate an OAuth or callback flow is in progress:
  // (1) The URL is any callback URL (canonical /auth/callback or legacy
  //     /workspace/workspace). Even without a payload, the SPA is mid-flow:
  //     completeAuthCallback may still rewrite the URL via the no-payload
  //     invite-rescue path (?next=/invite/<token>/accept). Treating any
  //     callback URL as in-flight keeps the bootstrap from flashing the
  //     legacy AuthScreen at the user.
  // (2) The URL has a PKCE code, OTP token_hash, or Supabase implicit-flow
  //     hash tokens. This catches non-callback-URL cases (e.g. token hash
  //     fragments arriving on a non-callback path).
  // (3) sessionStorage carries a same-tab flow id we wrote via
  //     setAuthNextPath right before redirecting to the IdP. This survives
  //     a refresh during the redirect window. Note: sessionStorage is
  //     per-tab, so the email-confirmation-in-new-tab case does NOT have it
  //     — that case is covered by signal (1).
  if (isAuthCallbackUrl()) return true;
  const params = new URLSearchParams(window.location.search);
  if (params.get('code') || params.get('token_hash')) return true;
  if (hasSupabaseAuthTokenHash()) return true;
  if (getStorageItem(window.sessionStorage, AUTH_NEXT_PATH_FLOW_KEY)) return true;
  return false;
}

function hasStoredAuthSession() {
  return Boolean(getStoredAccessToken() || hasStoredSupabaseSession() || hasInFlightOAuthFlow());
}

function authStatusFromMeError(error) {
  return error.code === 'access_not_configured'
    ? 'unconfigured'
    : error.status === 401 || error.code === 'unauthenticated'
      ? 'unauthenticated'
      : 'error';
}

function clearStaleOrganizationSelection() {
  for (const key of STALE_ORG_SELECTION_KEYS) {
    window.localStorage.removeItem(key);
    window.sessionStorage.removeItem(key);
  }
}

function isUnauthenticatedError(error) {
  return error?.status === 401 || error?.code === 'unauthenticated';
}

function clearSupabaseBrowserSession() {
  try {
    const keys = [];
    for (let i = 0; i < window.localStorage.length; i += 1) {
      const key = window.localStorage.key(i);
      if (key?.startsWith('sb-') && key.endsWith('-auth-token')) keys.push(key);
    }
    keys.forEach((key) => window.localStorage.removeItem(key));
  } catch {
    // Ignore storage cleanup failures. The explicit dev token still takes precedence.
  }
  if (window.__mcpTesterAuth) window.__mcpTesterAuth.session = null;
}

function clearBrowserAuthSession() {
  window.localStorage.removeItem(ACCESS_TOKEN_KEY);
  if (window.__mcpTesterAuth) {
    window.__mcpTesterAuth.session = null;
    // Also clear Supabase's persisted sb-... auth-token so a reload cannot
    // resurrect the same invalid browser session after a 401.
    clearSupabaseBrowserSession();
  }
}

function signedOutAuthState(current) {
  if (
    !current.loading
    && current.status === 'unauthenticated'
    && !current.hasStoredSession
    && current.session === null
    && current.me === null
    && current.error === null
  ) {
    return current;
  }
  return {
    ...current,
    loading: false,
    status: 'unauthenticated',
    hasStoredSession: false,
    session: null,
    me: null,
    error: null,
  };
}

async function apiFetch(path, options = {}) {
  const headers = new Headers(options.headers || {});
  headers.set('Accept', 'application/json');
  if (options.body && !headers.has('Content-Type')) {
    headers.set('Content-Type', 'application/json');
  }
  const token = getStoredDevAccessToken() || (window.__mcpTesterAuth?.getAccessToken
    ? await window.__mcpTesterAuth.getAccessToken()
    : getStoredAccessToken());
  if (token) {
    headers.set('Authorization', `Bearer ${token}`);
  }
  const response = await fetch(path, { ...options, headers });
  return parseApiResponse(response);
}

async function fetchOAuthProviders() {
  const data = await apiFetch('/api/auth-profiles/oauth/providers');
  return data.providers || [];
}

async function startOAuthConnect({ mcpServerId, providerSlug, requestedScopes, authConfig }) {
  const body = {
    mcpServerId,
    authType: 'oauth_authorization_code',
  };
  if (providerSlug) body.providerSlug = providerSlug;
  if (Array.isArray(requestedScopes) && requestedScopes.length > 0) {
    body.requestedScopes = requestedScopes;
  }
  if (authConfig && typeof authConfig === 'object') {
    body.authConfig = authConfig;
  }
  return apiFetch('/api/auth-profiles/oauth/start', {
    method: 'POST',
    body: JSON.stringify(body),
  });
}

async function startOAuthReauthorize({ profileId, requestedScopes, authConfig = null }) {
  const body = {};
  if (Array.isArray(requestedScopes) && requestedScopes.length > 0) {
    body.requestedScopes = requestedScopes;
  }
  if (authConfig && typeof authConfig === 'object') {
    body.authConfig = authConfig;
  }
  return apiFetch(`/api/auth-profiles/${profileId}/oauth/reauthorize`, {
    method: 'POST',
    body: JSON.stringify(body),
  });
}

function useApiResource(path, deps = []) {
  const auth = useContextApi(AuthContext);
  const [state, setState] = useStateApi({ loading: true, data: null, error: null });

  const reload = React.useCallback(async () => {
    if (!path || !auth?.me) {
      setState({ loading: false, data: null, error: null });
      return null;
    }
    setState((current) => ({ ...current, loading: true, error: null }));
    try {
      const data = await apiFetch(path);
      setState({ loading: false, data, error: null });
      return data;
    } catch (error) {
      setState({ loading: false, data: null, error });
      return null;
    }
  }, [path, auth?.me?.profile?.id]);

  useEffectApi(() => {
    reload();
  }, [reload, ...deps]);

  return { ...state, reload };
}

function setBrowserAuthState(client, session) {
  window.__mcpTesterAuth = {
    client,
    session,
    getAccessToken: async () => {
      const devToken = getStoredDevAccessToken();
      if (devToken) return devToken;
      if (window.__mcpTesterAuth.session?.access_token) {
        return window.__mcpTesterAuth.session.access_token;
      }
      if (client) {
        const result = await client.auth.getSession();
        window.__mcpTesterAuth.session = result.data.session;
        if (result.data.session?.access_token) return result.data.session.access_token;
      }
      return getStoredAccessToken();
    },
  };
}

function ApiAuthProvider({ children }) {
  const [state, setState] = useStateApi({
    loading: true,
    status: 'bootstrapping',
    // Presentation hint only. It is intentionally read once at mount; /api/me
    // validation, 401 handling, and sign-out settle the real auth state.
    hasStoredSession: hasStoredAuthSession(),
    me: null,
    error: null,
    config: null,
    client: null,
    session: null,
  });

  // Generation counter for refreshMe results. Bumped whenever auth state is
  // settled by an authoritative source other than /api/me — currently
  // applyMembershipResponse (post-accept hydration) and signOut. refreshMe
  // captures the generation at start; if it changes during the await, the
  // result is stale and must NOT commit. Without this guard, a refreshMe
  // that started before invite accept can return 403 access_not_configured
  // AFTER applyMembershipResponse already set status='authenticated', and
  // overwrite it back to 'unconfigured' — re-stranding the invitee.
  const authGenerationRef = useRefApi(0);

  // Pass `{ flush: true }` when the caller is about to navigate based on the
  // refreshed auth state — flushSync forces React to commit before the
  // subsequent navigate() runs route-guard checks against `me`. Default
  // (batched) is correct everywhere else.
  //
  // Pass `{ retryOnAccessNotConfigured: N }` (where N is a max retry count) to
  // retry on a 403 access_not_configured response. Use this RIGHT AFTER an
  // operation that just created a membership server-side (e.g. invite accept,
  // workspace creation) — the new row is committed but a transient
  // visibility/cache effect on Vercel's serverless layer can briefly hide it
  // from the immediately-following /api/me read. Retries use 250 ms backoff
  // with a 2 s per-attempt timeout (the retry loop never exceeds N*(2s+250ms)
  // even if a fetch stalls — browsers don't time out idle fetches by default).
  const refreshMe = React.useCallback(async (options = {}) => {
    const generationAtStart = authGenerationRef.current;
    const isStale = () => authGenerationRef.current !== generationAtStart;
    const commit = (updater) => {
      if (isStale()) return;
      if (options.flush && ReactDOM?.flushSync) {
        ReactDOM.flushSync(() => setState(updater));
      } else {
        setState(updater);
      }
    };
    const maxRetries = Math.max(0, Math.min(Number(options.retryOnAccessNotConfigured) || 0, 5));
    const useRetryTimeout = maxRetries > 0;
    async function fetchMeOnce() {
      if (!useRetryTimeout) return apiFetch('/api/me');
      // Race the fetch against a per-attempt timeout. A stalled fetch counts
      // as a retry attempt so the loop's worst-case latency stays bounded
      // even if the network drops mid-request.
      let timer;
      const timeoutPromise = new Promise((_, reject) => {
        timer = setTimeout(() => {
          const error = new Error('Request to /api/me timed out');
          /** @type {any} */(error).code = 'request_timeout';
          /** @type {any} */(error).status = 0;
          reject(error);
        }, 2000);
      });
      try {
        return await Promise.race([apiFetch('/api/me'), timeoutPromise]);
      } finally {
        clearTimeout(timer);
      }
    }
    let attempt = 0;
    /** @type {unknown} */
    let lastError = null;
    while (true) {
      try {
        const me = await fetchMeOnce();
        if (isStale()) return null;
        window.identifyPostHogUser?.(me);
        window.identifyIntercomUser?.(me);
        commit((current) => ({ ...current, loading: false, status: 'authenticated', me, error: null }));
        return me;
      } catch (error) {
        if (isStale()) return null;
        lastError = error;
        const code = /** @type {any} */(error)?.code;
        const retryable = code === 'access_not_configured' || code === 'request_timeout';
        if (attempt < maxRetries && retryable) {
          attempt += 1;
          await new Promise((resolve) => setTimeout(resolve, 250));
          continue;
        }
        break;
      }
    }
    if (isStale()) return null;
    const error = /** @type {any} */(lastError);
    window.resetPostHogUser?.();
    window.resetIntercomUser?.();
    const unauthenticated = isUnauthenticatedError(error);
    if (unauthenticated) clearBrowserAuthSession();
    const status = authStatusFromMeError(error);
    commit((current) => ({
      ...current,
      loading: false,
      status,
      hasStoredSession: unauthenticated ? false : current.hasStoredSession,
      me: null,
      session: unauthenticated ? null : current.session,
      error,
    }));
    return null;
  }, []);

  const clearAuthError = React.useCallback(() => {
    setState((current) => (current.error ? { ...current, error: null } : current));
  }, []);

  useEffectApi(() => {
    let unsubscribed = false;
    async function init() {
      try {
        clearStaleOrganizationSelection();
        const config = await fetch('/api/public-config').then(parseApiResponse);
        if (unsubscribed) return;
        window.configurePostHogAnalytics?.(config);
        let client = null;
        let session = null;
        const hasDevAccessToken = Boolean(config.devAuthEnabled && getStoredDevAccessToken());
        if (hasDevAccessToken) {
          clearSupabaseBrowserSession();
        }
        if (config.authConfigured && window.supabase && !hasDevAccessToken) {
          client = supabase.createClient(config.supabaseUrl, config.supabaseAnonKey, {
            auth: {
              detectSessionInUrl: false,
              flowType: 'pkce',
            },
          });
          client.auth.onAuthStateChange((_event, nextSession) => {
            window.__mcpTesterAuth.session = nextSession;
            if (nextSession) {
              setState((current) => ({
                ...current,
                loading: !current.me,
                status: current.me || current.status === 'unconfigured' ? current.status : 'bootstrapping',
                // Keep a warm-bootstrap presentation flag once observed; /api/me
                // or sign-out is responsible for lowering it after validation.
                hasStoredSession: true,
                session: nextSession,
                error: null,
              }));
              // Skip the auth-state-change refreshMe for invite acceptance.
              // During OAuth this can fire before completeAuthCallback rewrites
              // /auth/callback?next=/invite/<token>/accept to the final route.
              if (isInviteAcceptBootstrapTarget()) {
                setState((current) => ({
                  ...current,
                  loading: false,
                  status: current.me ? 'authenticated' : 'unconfigured',
                  error: current.me ? null : { code: 'access_not_configured', message: 'Awaiting invite acceptance' },
                }));
              } else {
                refreshMe();
              }
            } else {
              window.resetPostHogUser?.();
              setState(signedOutAuthState);
            }
          });
          setBrowserAuthState(client, session);
          setState((current) => ({ ...current, config, client, session }));
          const callbackSession = await completeAuthCallback(client);
          const sessionResult = await client.auth.getSession();
          session = callbackSession || sessionResult.data.session;
        }
        setBrowserAuthState(client, session);
        setState((current) => ({
          ...current,
          config,
          client,
          session,
          hasStoredSession: Boolean(session) || current.hasStoredSession,
        }));
        if (session || getStoredAccessToken()) {
          // Skip the bootstrap /api/me when the post-callback URL is an
          // invite-accept route. The user is signed in but has no membership
          // yet, so /api/me is GUARANTEED to return 403 access_not_configured.
          // Calling it just produces a noisy console error and risks racing
          // with InvitePage's accept hydration. Commit the unconfigured
          // state directly — InvitePage runs accept and calls
          // applyMembershipResponse to lift state to authenticated.
          if (isInviteAcceptBootstrapTarget()) {
            setState((current) => ({
              ...current,
              loading: false,
              status: 'unconfigured',
              error: { code: 'access_not_configured', message: 'Awaiting invite acceptance' },
            }));
          } else {
            await refreshMe();
          }
        } else {
          setState((current) => ({ ...current, loading: false, status: 'unauthenticated', hasStoredSession: false }));
        }
      } catch (error) {
        setState((current) => ({ ...current, loading: false, status: 'error', error }));
      }
    }
    init();
    return () => {
      unsubscribed = true;
    };
  }, [refreshMe]);

  // Hydrate auth.me directly from a server response that has the same shape
  // as /api/me (e.g. POST /api/invitations/<token>/accept). Lets the caller
  // skip a follow-up GET /api/me — useful immediately after an INSERT that
  // sometimes returns 403 from /api/me on the very next request because the
  // pooled connection used by /api/me hasn't picked up the new row yet.
  //
  // Bumps the auth generation so any in-flight refreshMe results that arrive
  // after this call (e.g. the pre-accept refreshMe from init() or
  // onAuthStateChange) are treated as stale and discarded. Without that, a
  // 403 access_not_configured from a refreshMe that started before accept
  // could overwrite this authenticated state and re-strand the invitee.
  const applyMembershipResponse = React.useCallback((me, options = {}) => {
    if (!me || !me.profile) return;
    authGenerationRef.current += 1;
    const commit = (updater) => {
      if (options.flush && ReactDOM?.flushSync) {
        ReactDOM.flushSync(() => setState(updater));
      } else {
        setState(updater);
      }
    };
    window.identifyPostHogUser?.(me);
    window.identifyIntercomUser?.(me);
    commit((current) => ({ ...current, loading: false, status: 'authenticated', me, error: null }));
  }, []);

  const auth = useMemoApi(() => ({
    ...state,
    refreshMe,
    applyMembershipResponse,
    signInWithPassword: async (email, password, options = {}) => {
      clearAuthError();
      if (!state.client) throw new Error('Supabase browser auth is not configured');
      const result = await state.client.auth.signInWithPassword({ email, password });
      if (result.error) throw result.error;
      window.__mcpTesterAuth.session = result.data.session;
      setState((current) => ({ ...current, session: result.data.session, error: null }));
      // Pass skipMeRefresh: true when the next navigation target is an
      // invite-accept route. The invitee has no membership yet so /api/me
      // will return 403 access_not_configured — that's expected, but
      // awaiting it here would either stall the SigninPage submit (timeout)
      // or commit unconfigured state that races with the invite-accept
      // hydration. Let InvitePage hydrate from the accept response instead.
      if (options.skipMeRefresh) {
        return { data: result.data, me: null };
      }
      const me = await refreshMe({ flush: options.flush === true });
      return { data: result.data, me };
    },
    signInWithGoogle: async (options = {}) => {
      clearAuthError();
      if (!state.client) throw new Error('Supabase browser auth is not configured');
      clearPkceRetryMarker();
      const flowId = options.nextPath ? setAuthNextPath(options.nextPath) : '';
      const result = await state.client.auth.signInWithOAuth({
        provider: 'google',
        options: {
          redirectTo: getAuthRedirectUrl(options.nextPath, flowId),
        },
      });
      if (result.error) throw result.error;
      return result;
    },
    signUpWithPassword: async (email, password, options = {}) => {
      clearAuthError();
      if (!state.client) throw new Error('Supabase browser auth is not configured');
      const flowId = options.nextPath ? setAuthNextPath(options.nextPath) : '';
      const result = await state.client.auth.signUp({
        email,
        password,
        options: {
          emailRedirectTo: getAuthRedirectUrl(options.nextPath, flowId),
        },
      });
      if (result.error) throw result.error;
      window.__mcpTesterAuth.session = result.data.session;
      setState((current) => ({ ...current, session: result.data.session, error: null }));
      if (result.data.session) {
        clearAuthNextPath();
        // Same skipMeRefresh contract as signInWithPassword — when the
        // post-signup target is an invite-accept route, /api/me will 403
        // until the invite is accepted, so let InvitePage hydrate instead.
        if (options.skipMeRefresh) {
          return { data: result.data, me: null };
        }
        const me = await refreshMe({ flush: options.flush === true });
        return { data: result.data, me };
      }
      return result;
    },
    resendSignupEmail: async (email, options = {}) => {
      clearAuthError();
      if (!state.client) throw new Error('Supabase browser auth is not configured');
      const flowId = options.nextPath ? setAuthNextPath(options.nextPath) : '';
      const result = await state.client.auth.resend({
        type: 'signup',
        email,
        options: { emailRedirectTo: getAuthRedirectUrl(options.nextPath, flowId) },
      });
      if (result.error) throw result.error;
      return result;
    },
    resetPasswordForEmail: async (email, options = {}) => {
      clearAuthError();
      if (!state.client) throw new Error('Supabase browser auth is not configured');
      const nextPath = options.nextPath || '/reset-password';
      const flowId = setAuthNextPath(nextPath);
      const result = await state.client.auth.resetPasswordForEmail(email, {
        redirectTo: getAuthRedirectUrl(nextPath, flowId),
      });
      if (result.error) throw result.error;
      return result;
    },
    updatePassword: async (password, options = {}) => {
      clearAuthError();
      if (!state.client) throw new Error('Supabase browser auth is not configured');
      const result = await state.client.auth.updateUser({ password });
      if (result.error) throw result.error;
      window.__mcpTesterAuth.session = result.data?.user ? state.session : window.__mcpTesterAuth.session;
      if (options.flush) {
        await refreshMe({ flush: true });
      }
      return result;
    },
    useAccessToken: async (token, options = {}) => {
      const cleanToken = token.trim();
      window.localStorage.setItem(ACCESS_TOKEN_KEY, cleanToken);
      if (cleanToken.startsWith('dev:')) clearSupabaseBrowserSession();
      if (window.__mcpTesterAuth) window.__mcpTesterAuth.session = null;
      setState((current) => ({ ...current, loading: true, status: 'bootstrapping', hasStoredSession: true }));
      return refreshMe({ flush: options.flush === true });
    },
    createOrganization: async ({ name, slug, displayName }) => {
      await apiFetch('/api/onboarding/organization', {
        method: 'POST',
        body: JSON.stringify({ name, slug, displayName, display_name: displayName }),
      });
      return refreshMe();
    },
    signOut: async () => {
      // Bump generation BEFORE the async auth.signOut() call so any
      // in-flight refreshMe (or one started by onAuthStateChange while
      // signOut runs) is treated as stale and cannot resurrect signed-out
      // state by committing its older /api/me result.
      authGenerationRef.current += 1;
      window.localStorage.removeItem(ACCESS_TOKEN_KEY);
      clearStaleOrganizationSelection();
      if (state.client) await state.client.auth.signOut();
      if (window.__mcpTesterAuth) window.__mcpTesterAuth.session = null;
      window.resetPostHogUser?.();
      window.resetIntercomUser?.();
      setState(signedOutAuthState);
    },
  }), [state, refreshMe, clearAuthError, applyMembershipResponse]);

  return <AuthContext.Provider value={auth}>{children}</AuthContext.Provider>;
}

function useAuth() {
  return useContextApi(AuthContext);
}

function AuthScreen() {
  const auth = useAuth();
  const [authMode, setAuthMode] = useStateApi('signin');
  const [onboardingMode, setOnboardingMode] = useStateApi('create');
  const [email, setEmail] = useStateApi('');
  const [password, setPassword] = useStateApi('');
  const [confirmPassword, setConfirmPassword] = useStateApi('');
  const [organizationName, setOrganizationName] = useStateApi('');
  const [displayName, setDisplayName] = useStateApi('');
  const [inviteInput, setInviteInput] = useStateApi('');
  const [message, setMessage] = useStateApi('');
  const [busyAction, setBusyAction] = useStateApi('');
  const busy = Boolean(busyAction);
  const canUseSupabase = Boolean(auth?.config?.authConfigured && auth?.client);
  const completingCallback = isAuthCallbackUrl();
  const authLoadingLabel = completingCallback ? 'Completing sign-in...' : 'Connecting...';

  async function run(action, actionKey = 'default') {
    setBusyAction(actionKey);
    setMessage('');
    try {
      await action();
    } catch (error) {
      setMessage(error.message || 'Authentication failed');
    } finally {
      setBusyAction('');
    }
  }

  async function submitPasswordAuth() {
    const trimmedEmail = email.trim();
    if (authMode === 'signup') {
      if (password !== confirmPassword) throw new Error('Passwords do not match');
      const result = await auth.signUpWithPassword(trimmedEmail, password);
      if (!result?.data?.session) {
        setMessage('Account created. Check your email to confirm it, then sign in.');
      } else {
        const nextPath = readAuthNextPath();
        if (nextPath) navigateToPath(consumeAuthNextPath());
      }
      return;
    }
    await auth.signInWithPassword(trimmedEmail, password);
    const nextPath = readAuthNextPath();
    if (nextPath) navigateToPath(consumeAuthNextPath());
  }

  function openInviteFromInput() {
    const token = extractInvitationToken(inviteInput);
    if (!token) throw new Error('Enter a valid invite link or token');
    const nextPath = `/invite/${token}`;
    setAuthNextPath(nextPath);
    navigateToPath(nextPath);
  }

  const isAccessNotConfigured = auth?.error?.code === 'access_not_configured';
  const passwordReady = authMode === 'signup'
    ? email.trim() && password && confirmPassword
    : email.trim() && password;
  const onboardingReady = organizationName.trim().length > 0;

  return (
    <div className="auth-page">
      <div className="auth-panel">
        <div className="auth-logo">
          <img src="/frontend/assets/armature-wordmark-dark.svg" alt="Armature" />
        </div>
        <h1>{isAccessNotConfigured ? 'Access not configured' : 'Sign in to Armature'}</h1>
        {isAccessNotConfigured ? (
          <p className="muted">Your account is signed in, but it has not been added to an Armature workspace yet.</p>
        ) : (
          <p className="muted">Sign in to load your workspace dashboard.</p>
        )}

        {auth?.loading ? (
          <div className="code inline-loading">
            <LoadingSpinner size="sm" label={completingCallback ? 'Completing sign-in' : 'Connecting'} decorative />
            {authLoadingLabel}
          </div>
        ) : (
          <>
            {canUseSupabase && !isAccessNotConfigured && (
              <div className="auth-form">
                <Button
                  className="auth-oauth"
                  disabled={busy}
                  loading={busyAction === 'google'}
                  loadingLabel="Opening Google..."
                  onClick={() => run(auth.signInWithGoogle, 'google')}>
                  <img className="google-mark" src="/frontend/assets/google-g.png" alt="" aria-hidden="true" />Continue with Google
                </Button>
                <div className="auth-divider"><span>or</span></div>
                <div className="seg auth-mode" role="tablist" aria-label="Authentication mode">
                  <button type="button" className={authMode === 'signin' ? 'active' : ''} onClick={() => setAuthMode('signin')}>Sign in</button>
                  <button type="button" className={authMode === 'signup' ? 'active' : ''} onClick={() => setAuthMode('signup')}>Create account</button>
                </div>
                <input className="input" placeholder="email@example.com" value={email} onChange={(e) => setEmail(e.target.value)} />
                <input className="input" placeholder="Password" type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
                {authMode === 'signup' && (
                  <input className="input" placeholder="Confirm password" type="password" value={confirmPassword} onChange={(e) => setConfirmPassword(e.target.value)} />
                )}
                <Button
                  variant="primary"
                  className="auth-submit"
                  disabled={busy || !passwordReady}
                  loading={busyAction === 'password'}
                  loadingLabel={authMode === 'signup' ? 'Creating account...' : 'Signing in...'}
                  onClick={() => run(submitPasswordAuth, 'password')}>
                  <Icon name="key" size={13} />{authMode === 'signup' ? 'Create account' : 'Sign in'}
                </Button>
              </div>
            )}

            {isAccessNotConfigured && (
              <div className="auth-form">
                <div className="seg auth-mode" role="tablist" aria-label="Workspace setup mode">
                  <button type="button" className={onboardingMode === 'create' ? 'active' : ''} onClick={() => setOnboardingMode('create')}>Create workspace</button>
                  <button type="button" className={onboardingMode === 'join' ? 'active' : ''} onClick={() => setOnboardingMode('join')}>Join with invite</button>
                </div>
                {onboardingMode === 'create' ? (
                  <>
                    <input className="input" placeholder="Organization name" value={organizationName} onChange={(e) => setOrganizationName(e.target.value)} />
                    <input className="input" placeholder="Your name" value={displayName} onChange={(e) => setDisplayName(e.target.value)} />
                    <Button
                      variant="primary"
                      className="auth-submit"
                      disabled={busy || !onboardingReady}
                      loading={busyAction === 'organization'}
                      loadingLabel="Creating workspace..."
                      onClick={() => run(() => auth.createOrganization({ name: organizationName, displayName }), 'organization')}>
                      <Icon name="plus" size={13} />Create workspace
                    </Button>
                  </>
                ) : (
                  <>
                    <input
                      className="input"
                      placeholder="Invite link or token"
                      value={inviteInput}
                      onChange={(e) => setInviteInput(e.target.value)}
                    />
                    <Button
                      variant="primary"
                      className="auth-submit"
                      disabled={busy || !inviteInput.trim()}
                      onClick={() => run(openInviteFromInput, 'invite')}>
                      <Icon name="key" size={13} />Open invite
                    </Button>
                  </>
                )}
                <Button size="sm" onClick={auth.signOut}>Use another session</Button>
              </div>
            )}
          </>
        )}
        {(message || auth?.error?.message) && (
          <div className="auth-message">{message || auth.error.message}</div>
        )}
      </div>
      <a className="auth-help" href="mailto:help@armature.tech">
        <Icon name="mail" size={13} />Stuck? Email us
      </a>
    </div>
  );
}

function formatDuration(ms) {
  if (ms === null || ms === undefined) return '—';
  if (ms < 1000) return `${ms}ms`;
  if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
  return `${Math.round(ms / 60000)}m`;
}

function formatDateTime(value) {
  if (!value) return '—';
  const date = new Date(value);
  if (Number.isNaN(date.getTime())) return '—';
  return date.toLocaleString([], { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' });
}

function formatRelative(value) {
  if (!value) return '—';
  const date = new Date(value);
  const diffMs = Date.now() - date.getTime();
  if (Number.isNaN(diffMs)) return '—';
  const mins = Math.round(diffMs / 60000);
  if (mins < 1) return 'now';
  if (mins < 60) return `${mins}m ago`;
  const hours = Math.round(mins / 60);
  if (hours < 48) return `${hours}h ago`;
  return `${Math.round(hours / 24)}d ago`;
}

function formatPercent(value) {
  if (value === null || value === undefined) return '—';
  return `${Math.round(Number(value) * 100)}%`;
}

function formatNumber(value) {
  if (value === null || value === undefined) return '—';
  return Number(value).toLocaleString();
}

function formatCost(value) {
  if (value === null || value === undefined) return '—';
  return `$${Number(value).toFixed(3)}`;
}

function shortId(id) {
  return id ? `${id.slice(0, 8)}…` : '—';
}

window.apiFetch = apiFetch;
window.fetchOAuthProviders = fetchOAuthProviders;
window.startOAuthConnect = startOAuthConnect;
window.startOAuthReauthorize = startOAuthReauthorize;
window.ApiAuthProvider = ApiAuthProvider;
window.AuthScreen = AuthScreen;
window.extractInvitationToken = extractInvitationToken;
window.setAuthNextPath = setAuthNextPath;
window.useAuth = useAuth;
window.useApiResource = useApiResource;
Object.assign(window, {
  validateBrowserReturnPath,
  canUseTenantAction,
  tenantActionDisabledReason,
  requirePaidAction,
  TERMINAL_ONBOARDING_STEPS,
});
window.formatDuration = formatDuration;
window.formatDateTime = formatDateTime;
window.formatRelative = formatRelative;
window.formatPercent = formatPercent;
window.formatNumber = formatNumber;
window.formatCost = formatCost;
window.shortId = shortId;
Object.assign(window, {
  getBillingEntitlements,
  getPlanLimitState,
  workflowMinIntervalSeconds,
  cronIntervalSeconds,
  cronAllowedByPlan,
});
