/* global React, Button, CliSetupProgress, DiscoverToolsPanel, Icon, LoadingSpinner, McpConnectFormBody, McpOauthAwaitingPanel, ToolMonitorWizard, ToolWizardLoading, WorkflowTargetPicker, apiFetch, buildArgsFromState, buildInitialToolState, buildMcpConnectPayload, buildWorkflowTesterTargets, formatDuration, getModelsForWorkflowHarness, getWorkflowHarnessOption, getWorkflowHarnessOptions, normalizeWorkflowTargetRows, pickDefaultModelForHarness, runMcpOAuthFlow, startOAuthConnect, useApiResource, useAuth, useMcpConnectFormState, validateAllSelectedTools, validateBrowserReturnPath, validateMcpConnectFormInputs, workflowTargetRowsKey, TERMINAL_ONBOARDING_STEPS */
const { useEffect: useEffectOnb, useMemo: useMemoOnb, useRef: useRefOnb, useState: useStateOnb } = React;

const ONBOARDING_STEPS = {
  workspace: { index: 1, total: 5, label: 'Workspace' },
  plan: { index: 2, total: 5, label: 'Plan' },
  role: { index: 3, total: 5, label: 'Role' },
  connect: { index: 4, total: 5, label: 'Connect' },
  monitor: { index: 5, total: 5, label: 'Monitor' },
};
const PUBLIC_BILLING_PERIODS = ['monthly', 'annually'];
const CUSTOM_PLAN_FALLBACK_CONTACT_URL = 'mailto:sales@armature.tech?subject=Armature%20Enterprise%20plan';

// Cal.com embed wiring for the "Talk to a cofounder" CTA on the workspace
// step. Values mirror Cal's snippet generator output for the inline
// embed of the `armature` org's `founders/onboarding` event type:
//   - BOOKING_URL is the canonical user-facing URL we open as a hard
//     fallback when the embed fails to load.
//   - LINK is the path the embed script appends to ORIGIN to pull in
//     the iframe content; for organization-subdomain origins the
//     event-type slug is prefixed with `team/`.
//   - ORIGIN tells embed.js which Cal instance to talk to; we use the
//     org subdomain so bookings are branded as Armature instead of
//     generic cal.com.
//   - NAMESPACE scopes everything (init, inline, ui, on) to a dedicated
//     `Cal.ns[namespace]` instance so future Cal embeds in the app
//     can't trample our handlers.
//   - EMBED_SRC is the loader script the queueing stub injects.
const COFOUNDERS_CAL_BOOKING_URL = 'https://armature.cal.com/founders/onboarding';
const COFOUNDERS_CAL_LINK = 'team/founders/onboarding';
const COFOUNDERS_CAL_ORIGIN = 'https://armature.cal.com';
const COFOUNDERS_CAL_NAMESPACE = 'onboarding';
const COFOUNDERS_CAL_EMBED_SRC = 'https://app.cal.com/embed/embed.js';

// We persist a tiny "user already booked" flag in localStorage so a
// user who closes the tab between the booking and finishing onboarding
// still lands on the "You're booked" confirmation when they come back
// instead of the workspace form. The Cal embed only fires
// `bookingSuccessful` in the page that did the booking, so without
// this flag a refresh would silently revert to the default form.
//
// The key is versioned (`.v1`) so future schema changes can ignore
// stale entries instead of crashing on parse. We DON'T scope by user
// id because the workspace step runs pre-org-creation and the auth
// session already isolates browsers; cross-user leakage on a shared
// browser would just show a stale "you're booked" confirmation,
// which is harmless and easy to dismiss.
const COFOUNDERS_BOOKED_STORAGE_KEY = 'armature.onboarding.cofoundersBooked.v1';

function readPersistedCofoundersBooked() {
  try {
    const raw = localStorage.getItem(COFOUNDERS_BOOKED_STORAGE_KEY);
    if (!raw) return null;
    const parsed = JSON.parse(raw);
    if (!parsed || typeof parsed !== 'object' || typeof parsed.at !== 'string') return null;
    return parsed;
  } catch (_e) {
    // localStorage may be disabled (private browsing, blocked storage,
    // SSR-like sandbox) — treat as "no record".
    return null;
  }
}

function writePersistedCofoundersBooked() {
  try {
    localStorage.setItem(COFOUNDERS_BOOKED_STORAGE_KEY, JSON.stringify({
      at: new Date().toISOString(),
      calLink: COFOUNDERS_CAL_LINK,
    }));
  } catch (_e) { /* swallow — see read helper */ }
}

function clearPersistedCofoundersBooked() {
  try {
    localStorage.removeItem(COFOUNDERS_BOOKED_STORAGE_KEY);
  } catch (_e) { /* swallow */ }
}

// Lazily install the Cal.com embed loader exactly once. The standard
// snippet shipped by Cal's snippet generator — re-expressed here so the
// SPA doesn't need a global script tag — sets up `window.Cal` as a
// queueing stub that buffers calls until embed.js downloads and replaces
// it. We resolve the returned promise from `script.onload` and reject
// from `script.onerror` so embed-load failures (CSP, ad blockers,
// network errors) reliably surface to `mountCofoundersCalInline`'s
// catch block, which then triggers the new-tab fallback and error
// telemetry.
//
// Subsequent in-process calls return the cached promise. A failure
// clears the cache so the next user click can retry from scratch.
let calEmbedLoaderPromise = null;
function ensureCalEmbedLoader() {
  if (typeof window === 'undefined') return Promise.resolve(null);
  if (calEmbedLoaderPromise) return calEmbedLoaderPromise;
  // Tracks whether THIS loader call is the one that appended the
  // `<script src="embed.js">` tag. Set inside the queueing stub when
  // we actually install a fresh script element. Used for two distinct
  // decisions:
  //
  //   1. Resolution path. If we installed the script, the promise
  //      resolves only from the real `script.onload` (or rejects
  //      from `script.onerror`). If we DIDN'T install (because Cal
  //      was pre-installed by some earlier code), there's no onload
  //      to await — settle via a microtask so callers observe
  //      consistent async ordering.
  //
  //   2. Failure cleanup. On `script.onerror`, the queueing stub is
  //      left behind with `cal.loaded = true`, which would
  //      short-circuit the install on the next retry attempt and
  //      hand the caller a queue that no embed script will ever
  //      drain. If we installed the failing script ourselves, wipe
  //      `window.Cal` so the next retry rebuilds from scratch. If
  //      Cal was pre-installed by something else, leave it alone —
  //      we don't own that state.
  let scriptInstalledByUs = false;
  calEmbedLoaderPromise = new Promise((resolve, reject) => {
    let settled = false;
    function settle(error, value) {
      if (settled) return;
      settled = true;
      if (error) {
        calEmbedLoaderPromise = null;
        if (scriptInstalledByUs) {
          try { delete window.Cal; }
          catch (_deleteError) {
            try { window.Cal = undefined; } catch (_assignError) { /* swallow */ }
          }
        }
        reject(error);
        return;
      }
      resolve(value);
    }

    try {
      // Stamp the queueing stub. Mirrors Cal's official snippet so we
      // stay compatible if they tweak how `Cal.ns` / `Cal.q` are read
      // on first call.
      (function (C, A, L) {
        const p = function (a, ar) { a.q.push(ar); };
        const d = C.document;
        C.Cal = C.Cal || function () {
          const cal = C.Cal;
          const ar = arguments;
          if (!cal.loaded) {
            cal.ns = {};
            cal.q = cal.q || [];
            const script = d.createElement('script');
            script.src = A;
            script.async = true;
            script.onload = () => settle(null, C.Cal);
            script.onerror = () => settle(new Error('Could not load the Cal.com embed.'));
            d.head.appendChild(script);
            cal.loaded = true;
            scriptInstalledByUs = true;
          }
          if (ar[0] === L) {
            const api = function () { p(api, arguments); };
            const namespace = ar[1];
            api.q = api.q || [];
            if (typeof namespace === 'string') {
              cal.ns[namespace] = cal.ns[namespace] || api;
              p(cal.ns[namespace], ar);
              p(cal, ['initNamespace', namespace]);
            } else { p(cal, ar); }
            return;
          }
          p(cal, ar);
        };
      })(window, COFOUNDERS_CAL_EMBED_SRC, 'init');
      // Initialize the namespaced instance up front so the inline /
      // ui / on calls dispatch against `Cal.ns[COFOUNDERS_CAL_NAMESPACE]`
      // and don't pollute callers who init Cal for unrelated event
      // types. `origin` is the org subdomain so the iframe loads
      // Armature-branded scheduling chrome.
      window.Cal('init', COFOUNDERS_CAL_NAMESPACE, { origin: COFOUNDERS_CAL_ORIGIN });
      // No <script> to await — see the `scriptInstalledByUs` doc
      // comment for why we resolve on a microtask instead.
      if (!scriptInstalledByUs) {
        Promise.resolve().then(() => settle(null, window.Cal));
      }
    } catch (error) {
      settle(error);
    }
  });
  return calEmbedLoaderPromise;
}

// Mount the Cal.com inline embed into `element` and wire a callback
// that fires once the user finishes booking. Returns a cleanup
// function that detaches the booking listener (the iframe itself is
// removed by React when the wrapping element unmounts).
//
// The async loader path can race the React unmount, so callers
// receive a `cancelled` accessor — when it returns true we skip the
// Cal API calls entirely and resolve to a no-op cleanup. Errors are
// reported through `onError` so the calling component can render a
// retry UI plus the new-tab fallback.
async function mountCofoundersCalInline({ element, onBooked, onError, isCancelled }) {
  try {
    await ensureCalEmbedLoader();
    if (isCancelled?.()) return () => {};
    const namespacedCal = window.Cal?.ns?.[COFOUNDERS_CAL_NAMESPACE];
    if (typeof namespacedCal !== 'function') {
      throw new Error('Cal.com embed namespace is not initialized.');
    }
    // Inline mount. Mirrors the snippet generator output for
    // `armature/founders/onboarding`. `useSlotsViewOnSmallScreen` is
    // intentionally the string "true" — Cal's snippet generator emits
    // it that way, presumably for legacy serialization compatibility.
    namespacedCal('inline', {
      elementOrSelector: element,
      config: { layout: 'month_view', useSlotsViewOnSmallScreen: 'true' },
      calLink: COFOUNDERS_CAL_LINK,
    });
    // UI tweaks scoped to this namespace. Hiding event-type details
    // keeps the inline view focused on the calendar grid since the
    // outer card already explains what the booking is for.
    namespacedCal('ui', { hideEventTypeDetails: true, layout: 'month_view' });
    // Subscribe to Cal's booking-completion event. The action name is
    // documented at https://cal.com/docs/embed/embed-events.
    let bookingFired = false;
    namespacedCal('on', {
      action: 'bookingSuccessful',
      callback: () => {
        if (bookingFired || isCancelled?.()) return;
        bookingFired = true;
        try { onBooked?.(); } catch (_callbackError) {
          // Never let a callback error tear down the calendar.
        }
      },
    });
    return function detachListener() {
      try {
        namespacedCal('off', { action: 'bookingSuccessful' });
      } catch (_offError) {
        // Cal's `off` is best-effort and undocumented across versions;
        // failures here aren't worth surfacing — the iframe is about
        // to be removed anyway.
      }
    };
  } catch (error) {
    if (isCancelled?.()) return () => {};
    trackOnboardingEvent('onboarding.cofounders.inline_error', {
      calLink: COFOUNDERS_CAL_LINK,
      message: error?.message || 'cal_embed_load_failed',
    });
    onError?.(error);
    return () => {};
  }
}

function isPublicBillingPeriod(period) {
  return PUBLIC_BILLING_PERIODS.includes(period);
}

function publicBillingPeriodOrDefault(period) {
  return isPublicBillingPeriod(period) ? period : 'monthly';
}

function trackOnboardingEvent(name, payload) {
  try {
    /** @type {any} */ (window).posthog?.capture?.(name, payload);
  } catch (error) {
    // Telemetry must never block onboarding.
  }
}

function openCustomPlanCta(plan) {
  if (plan?.ctaUrl) {
    window.open(plan.ctaUrl, '_blank', 'noopener,noreferrer');
    return;
  }
  window.location.href = CUSTOM_PLAN_FALLBACK_CONTACT_URL;
}

async function persistOnboardingState(patch, options = {}) {
  try {
    return await apiFetch('/api/onboarding/state', {
      method: 'POST',
      body: JSON.stringify({ onboardingState: patch }),
    });
  } catch (error) {
    trackOnboardingEvent('onboarding.step.error', { step: patch.currentStep, error_code: error.code || 'state_persist_failed' });
    if (options.throwOnError) throw error;
    return null;
  }
}

function OnboardingProgress({ step, navigate, backPath = null }) {
  const meta = ONBOARDING_STEPS[step];
  if (!meta) return null;
  return (
    <div className="onboarding-progress">
      <div className="onboarding-progress-top">
        {backPath ? (
          <button className="btn btn-sm btn-ghost" onClick={() => navigate(backPath)}>
            <Icon name="chevronLeft" size={12} />Back
          </button>
        ) : <span />}
        <span>Step {meta.index} of {meta.total}</span>
      </div>
      <div className="onboarding-dots">
        {Array.from({ length: meta.total }).map((_, index) => (
          <span key={index} className={index < meta.index ? 'active' : ''}></span>
        ))}
      </div>
    </div>
  );
}

function OnboardingShell({ step, children, navigate, backPath = null, hideProgress = false }) {
  const enteredAtRef = useRefOnb(Date.now());

  useEffectOnb(() => {
    if (!step) return undefined;
    trackOnboardingEvent('onboarding.step.enter', { step });
    // currentStep is server-driven now (api/onboarding/organization +
    // api/billing/session/[id]). Don't POST it from the client — that was
    // the bypass that let users self-advance past the gate.
    return () => {
      trackOnboardingEvent('onboarding.step.exit', { step, ms_in_step: Date.now() - enteredAtRef.current });
    };
  }, [step]);

  return (
    <div className="onboarding-page">
      <div className="onboarding-shell">
        {!hideProgress && <OnboardingProgress step={step} navigate={navigate} backPath={backPath} />}
        {children}
      </div>
    </div>
  );
}

function slugifyWorkspaceName(value) {
  const slug = String(value || '')
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-+|-+$/g, '')
    .slice(0, 32)
    .replace(/-+$/g, '');
  return slug.length >= 3 ? slug : `${slug || 'app'}-workspace`.slice(0, 32);
}

const SLUG_CHECK_DEBOUNCE_MS = 150;
const SLUG_PATTERN_RE = /^[a-z0-9-]{3,32}$/;

function buildSlugSuggestionCandidates(slug) {
  const base = slug.replace(/-+$/, '').slice(0, 28) || slug.slice(0, 28);
  return [`${base}-2`, `${base}-3`, `${base}-${Math.floor(Math.random() * 9000) + 1000}`]
    .map((candidate) => candidate.slice(0, 32))
    .filter((candidate) => SLUG_PATTERN_RE.test(candidate));
}

async function findFirstAvailableSlug(candidates) {
  const probes = await Promise.all(candidates.map(async (candidate) => {
    try {
      const result = await apiFetch(`/api/onboarding/slug-available?slug=${encodeURIComponent(candidate)}`);
      return result.available ? candidate : null;
    } catch {
      return null;
    }
  }));
  return probes.find(Boolean) || '';
}

function SlugStatus({ slugCheck, onApplySuggestion = null }) {
  if (slugCheck.kind === 'checking') {
    return (
      <span className="workspace-slug-status is-checking" aria-live="polite">
        <LoadingSpinner size="sm" decorative />Checking
      </span>
    );
  }
  if (slugCheck.kind === 'available') {
    return (
      <span className="workspace-slug-status is-ok" aria-live="polite">Available</span>
    );
  }
  if (slugCheck.kind === 'taken') {
    return (
      <span className="workspace-slug-status is-bad" aria-live="polite">
        Already taken{slugCheck.suggestion && onApplySuggestion ? (
          <>
            {' · '}
            <button type="button" className="slug-suggest-action" onClick={onApplySuggestion}>Use {slugCheck.suggestion}</button>
          </>
        ) : null}
      </span>
    );
  }
  if (slugCheck.kind === 'invalid' || slugCheck.kind === 'error') {
    return (
      <span className="workspace-slug-status is-bad" aria-live="polite">{slugCheck.text}</span>
    );
  }
  return null;
}

function WorkspacePage({ navigate, queryString = '' }) {
  const auth = useAuth();
  const [name, setName] = useStateOnb(() => {
    const first = auth?.me?.profile?.firstName || '';
    return first ? `${first}'s workspace` : 'My workspace';
  });
  const [slug, setSlug] = useStateOnb(() => slugifyWorkspaceName(name));
  const [manualSlug, setManualSlug] = useStateOnb(false);
  const [slugEditing, setSlugEditing] = useStateOnb(false);
  const [message, setMessage] = useStateOnb('');
  const [slugCheck, setSlugCheck] = useStateOnb({ kind: 'checking', text: '', suggestion: '' });
  const [busy, setBusy] = useStateOnb(false);
  // Three-state machine that swaps the bottom of the workspace card:
  //   - 'closed' (default): "or" divider + workspace creation form
  //   - 'open':   inline Cal.com calendar so the user can pick a slot
  //   - 'booked': confirmation message + discrete escape hatch
  // The cofounders hero block stays mounted across all three states so
  // the user always has visual continuity for the alternative path.
  //
  // Initial state is hydrated from localStorage so a user who booked
  // in a previous visit returns to the "You're booked" confirmation
  // instead of the default form. Lazy initializer runs once on mount.
  const [calMode, setCalMode] = useStateOnb(() => (readPersistedCofoundersBooked() ? 'booked' : 'closed'));
  const inputRef = useRefOnb(null);
  const slugInputRef = useRefOnb(null);
  const slugCheckSeqRef = useRefOnb(0);

  useEffectOnb(() => {
    requestAnimationFrame(() => inputRef.current?.focus());
  }, []);

  useEffectOnb(() => {
    if (!manualSlug) setSlug(slugifyWorkspaceName(name));
  }, [name, manualSlug]);

  useEffectOnb(() => {
    if (slugEditing) {
      requestAnimationFrame(() => slugInputRef.current?.focus());
    }
  }, [slugEditing]);

  useEffectOnb(() => {
    const candidate = slug.trim().toLowerCase();
    const requestId = slugCheckSeqRef.current + 1;
    slugCheckSeqRef.current = requestId;

    if (!candidate) {
      setSlugCheck({ kind: 'idle', text: '', suggestion: '' });
      return undefined;
    }
    if (!SLUG_PATTERN_RE.test(candidate)) {
      setSlugCheck({ kind: 'invalid', text: 'Use 3-32 lowercase letters, numbers, or hyphens.', suggestion: '' });
      return undefined;
    }

    setSlugCheck({ kind: 'checking', text: '', suggestion: '' });
    const timer = setTimeout(async () => {
      try {
        const result = await apiFetch(`/api/onboarding/slug-available?slug=${encodeURIComponent(candidate)}`);
        if (slugCheckSeqRef.current !== requestId) return;
        if (result.available) {
          setSlugCheck({ kind: 'available', text: 'Available', suggestion: '' });
          return;
        }
        const availableVariant = await findFirstAvailableSlug(buildSlugSuggestionCandidates(candidate));
        if (slugCheckSeqRef.current !== requestId) return;
        if (!manualSlug && availableVariant) {
          // Auto-bump silently. The effect re-runs on the new slug and
          // confirms `available` after one more debounce window.
          setSlug(availableVariant);
          return;
        }
        setSlugCheck({ kind: 'taken', text: 'Already taken', suggestion: availableVariant });
      } catch (error) {
        if (slugCheckSeqRef.current !== requestId) return;
        setSlugCheck({ kind: 'error', text: error.message || 'Could not check this slug', suggestion: '' });
      }
    }, SLUG_CHECK_DEBOUNCE_MS);

    return () => clearTimeout(timer);
  }, [slug, manualSlug]);

  function startEditingSlug() {
    setManualSlug(true);
    setSlugEditing(true);
  }

  function resetSlugToAuto() {
    setManualSlug(false);
    setSlugEditing(false);
    setSlug(slugifyWorkspaceName(name));
  }

  function applySuggestion() {
    if (!slugCheck.suggestion) return;
    setSlug(slugCheck.suggestion);
  }

  async function submit(event) {
    event.preventDefault();
    if (slugCheck.kind !== 'available' || busy) return;
    setMessage('');
    setBusy(true);
    try {
      await auth.createOrganization({ name, slug, displayName: auth?.me?.profile?.displayName || '' });
      navigate(`/onboarding/plan${queryString ? `?${queryString}` : ''}`);
    } catch (error) {
      setMessage(error.message);
    } finally {
      setBusy(false);
    }
  }

  const submitDisabled = !name.trim() || !slug.trim() || slugCheck.kind !== 'available' || busy;

  function openCal() {
    trackOnboardingEvent('onboarding.cofounders.cta_click', { calLink: COFOUNDERS_CAL_LINK });
    setCalMode('open');
  }
  function closeCal() {
    // If the user is dismissing the booked confirmation via the
    // discrete escape hatch, forget the persisted flag so they don't
    // see "You're booked" again on their next visit — they explicitly
    // chose the workspace form. Closing from the open (calendar
    // visible) state doesn't touch the flag because the flag is only
    // ever set after a successful booking.
    if (calMode === 'booked') clearPersistedCofoundersBooked();
    trackOnboardingEvent('onboarding.cofounders.cal_close', { from: calMode });
    setCalMode('closed');
  }
  function handleBooked() {
    writePersistedCofoundersBooked();
    trackOnboardingEvent('onboarding.cofounders.booking_success', { calLink: COFOUNDERS_CAL_LINK });
    setCalMode('booked');
  }

  return (
    <OnboardingShell step="workspace" navigate={navigate}>
      <div className="onboarding-split">
        <div className={`onboarding-card workspace-card workspace-card-cal-${calMode}`}>
          <div className="onboarding-kicker">Workspace</div>
          <CofoundersCallBlock onOpen={openCal} disabled={calMode !== 'closed'} />
          {calMode === 'closed' && (
            <>
              <div className="onboarding-or-divider"><span>or set up your workspace directly</span></div>
              <form className="workspace-direct-form" onSubmit={submit}>
                <label className="field">
                  <span className="field-label">Workspace name</span>
                  <input ref={inputRef} className="input input-lg" value={name} onChange={(e) => setName(e.target.value)} />
                  {!slugEditing && (
                    <span className="workspace-slug-line">
                      <span className="mono">{slug || '—'}</span>
                      <span className="workspace-slug-line-sep">·</span>
                      <SlugStatus slugCheck={slugCheck} />
                      <span className="workspace-slug-line-sep">·</span>
                      <button type="button" className="workspace-slug-line-edit" onClick={startEditingSlug}>Edit</button>
                    </span>
                  )}
                </label>
                {slugEditing && (
                  <div className="field workspace-slug-field">
                    <span className="field-label">Slug</span>
                    <input
                      ref={slugInputRef}
                      className="input mono"
                      value={slug}
                      onChange={(e) => {
                        setManualSlug(true);
                        setSlug(e.target.value.toLowerCase());
                      }}
                    />
                    <div className="workspace-slug-edit-row">
                      <SlugStatus slugCheck={slugCheck} onApplySuggestion={applySuggestion} />
                      <button type="button" className="workspace-slug-reset" onClick={resetSlugToAuto}>
                        <Icon name="refresh" size={12} />Use auto
                      </button>
                    </div>
                  </div>
                )}
                {message && <div className="auth-message">{message}</div>}
                <Button variant="secondary" type="submit" disabled={submitDisabled} loading={busy} loadingLabel="Creating workspace...">
                  Continue<Icon name="chevronRight" size={13} />
                </Button>
              </form>
            </>
          )}
          {calMode === 'open' && (
            <CofoundersCalInline onBooked={handleBooked} onClose={closeCal} />
          )}
          {calMode === 'booked' && (
            <CofoundersCalBooked onClose={closeCal} />
          )}
        </div>
        <DiscoverToolsPanel />
      </div>
    </OnboardingShell>
  );
}

// Top-of-card CTA on the workspace step. Frames the alternative path
// (book a 10-minute call with a cofounder) before the "set up your
// workspace directly" form. Parent controls the open/close state so
// the form below this block can be swapped for the inline Cal embed
// without remounting the hero, which would lose visual continuity.
function CofoundersCallBlock({ onOpen, disabled = false }) {
  return (
    <div className="onboarding-cofounders-block">
      <h1>Let us walk you through Armature</h1>
      <p className="muted">
        We&apos;d love to onboard you and show you what&apos;s possible. Book a free
        {' '}<strong>10-minute call</strong> with a cofounder — pick any time that works.
      </p>
      <Button
        variant="primary"
        type="button"
        onClick={onOpen}
        disabled={disabled}>
        <Icon name="calendar" size={13} />
        Book a 10-minute intro<Icon name="chevronRight" size={13} />
      </Button>
    </div>
  );
}

// Inline Cal.com calendar that takes the place of the workspace
// creation form while open. Mounts on first render via
// `mountCofoundersCalInline`, listens for Cal's `bookingSuccessful`
// event to flip the parent into the "booked" view, and always
// surfaces a Close affordance so the user can bail back to the
// direct-setup form. If the embed fails to load (CSP regression, ad
// blocker, network failure) we surface a retry message + a new-tab
// fallback so the user is never stuck.
function CofoundersCalInline({ onBooked, onClose }) {
  const containerRef = useRefOnb(null);
  const cancelledRef = useRefOnb(false);
  // Read `onBooked` through a ref so the mount effect can stay
  // dependency-free. Putting `onBooked` directly in the effect deps
  // would tear down + remount the live calendar on every parent
  // re-render — including the debounced slug-check writes that fire
  // after the user has switched to the calendar view but before the
  // slug-check timer has resolved. Cal's `inline` action is not
  // documented as idempotent on a re-mount against the same DOM
  // node, so a remount can produce a duplicated iframe or a broken
  // calendar.
  const onBookedRef = useRefOnb(onBooked);
  useEffectOnb(() => {
    onBookedRef.current = onBooked;
  }, [onBooked]);
  const [error, setError] = useStateOnb('');

  useEffectOnb(() => {
    cancelledRef.current = false;
    if (!containerRef.current) return undefined;
    let detach = null;
    let active = true;
    mountCofoundersCalInline({
      element: containerRef.current,
      onBooked: () => onBookedRef.current?.(),
      onError: (err) => {
        if (!active) return;
        setError(err?.message || 'Could not load the calendar.');
      },
      isCancelled: () => cancelledRef.current,
    }).then((cleanup) => {
      if (!active) {
        try { cleanup?.(); } catch (_e) { /* swallow */ }
        return;
      }
      detach = cleanup;
    });
    return () => {
      active = false;
      cancelledRef.current = true;
      if (typeof detach === 'function') {
        try { detach(); } catch (_e) { /* swallow */ }
      }
    };
    // Mount once per CofoundersCalInline lifecycle. The mode change
    // from 'open' to 'booked'/'closed' unmounts this whole subtree,
    // which IS the right time to tear down the calendar.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <div className="cofounders-cal-inline">
      <div className="cofounders-cal-inline-head">
        <span className="cofounders-cal-inline-eyebrow">Pick a time</span>
        <button type="button" className="cofounders-cal-inline-close" onClick={onClose}>
          <Icon name="x" size={13} />Close calendar
        </button>
      </div>
      {error && (
        <div className="cofounders-cal-inline-error">
          <p>{error}</p>
          <Button
            variant="secondary"
            type="button"
            onClick={() => {
              try {
                window.open(COFOUNDERS_CAL_BOOKING_URL, '_blank', 'noopener,noreferrer');
              } catch (_e) { /* swallow */ }
            }}>
            Open in a new tab
          </Button>
        </div>
      )}
      {/* Hide the iframe wrapper when an error is showing so the user
          doesn't see a 540px empty box stacked under the error card.
          The ref is always attached so the mount effect can still
          point Cal at the container if it's reattached later (e.g. a
          retry path we add in the future). */}
      <div
        ref={containerRef}
        className="cofounders-cal-inline-frame"
        hidden={Boolean(error)}
      />
    </div>
  );
}

// Confirmation panel rendered after Cal fires `bookingSuccessful`.
// Tells the user it's safe to leave the page, and tucks a discrete
// text-link escape hatch underneath for users who'd rather finish
// onboarding now instead of waiting for the call. The escape hatch
// is intentionally NOT a primary/secondary button — once a user
// successfully booked, the call is the next step and we don't want
// to nudge them back into the form.
function CofoundersCalBooked({ onClose }) {
  return (
    <div className="cofounders-cal-booked" role="status" aria-live="polite">
      <div className="cofounders-cal-booked-icon" aria-hidden="true">
        <Icon name="check" size={28} />
      </div>
      <h2>You&apos;re booked</h2>
      <p className="muted">
        Check your inbox for the calendar invite. You can leave this page now —
        we&apos;ll see you on the call.
      </p>
      <button type="button" className="cofounders-cal-booked-skip" onClick={onClose}>
        I&apos;d rather set up my workspace now
      </button>
    </div>
  );
}

function usePlans(period) {
  const [state, setState] = useStateOnb({ loading: true, plans: [], error: null });
  useEffectOnb(() => {
    const billingPeriod = publicBillingPeriodOrDefault(period);
    let cancelled = false;
    async function load() {
      setState((cur) => ({ ...cur, loading: true, error: null }));
      try {
        const response = await fetch(`/api/billing/plans?billing_period=${encodeURIComponent(billingPeriod)}`, { headers: { Accept: 'application/json' } });
        const data = await response.json();
        if (!response.ok) throw new Error(data?.error?.message || 'Could not load plans');
        if (!cancelled) setState({ loading: false, plans: data.plans || [], error: null });
      } catch (error) {
        if (!cancelled) setState({ loading: false, plans: [], error });
      }
    }
    load();
    return () => {
      cancelled = true;
    };
  }, [period]);
  return state;
}

// Parse `{{text}}` (price-key highlight) and `[[text]]` (price-soon pill)
// markup inside a feature line. Used for both server-generated limit lines
// and admin-authored `included_features`. Returns an array of React nodes.
function renderFeatureMarkup(text) {
  const out = [];
  const re = /(\{\{([^}]+)\}\}|\[\[([^\]]+)\]\])/g;
  let lastIndex = 0;
  let key = 0;
  let match;
  while ((match = re.exec(text)) !== null) {
    if (match.index > lastIndex) {
      out.push(text.slice(lastIndex, match.index));
    }
    if (match[2] != null) {
      out.push(<span key={`k${key++}`} className="price-key">{match[2]}</span>);
    } else if (match[3] != null) {
      out.push(<span key={`s${key++}`} className="price-soon">{match[3]}</span>);
    }
    lastIndex = match.index + match[0].length;
  }
  if (lastIndex < text.length) out.push(text.slice(lastIndex));
  return out;
}

function isDemoTriggerBadge(plan, badge) {
  return String(badge || '').trim().toLowerCase() === 'most popular' && !plan?.contactSales;
}

function PlanCard({ plan, period, isFeatured, isCurrent, onChoose, choosing, isDemoModeActive = false, onActivateDemoMode }) {
  const showWasPrice = period === 'annually'
    && plan.displayPrice?.annually != null
    && plan.displayPrice?.monthly != null
    && plan.displayPrice.monthly > plan.displayPrice.annually;
  const price = plan.contactSales || plan.priceLabel
    ? (plan.priceLabel || 'Custom')
    : plan.periodPrice;
  const isCustomPrice = plan.contactSales || plan.priceLabel || plan.periodPrice == null;
  const showDemoTrialButton = isDemoModeActive && plan.badges?.some((badge) => isDemoTriggerBadge(plan, badge));
  const ctaLabel = isCurrent
    ? 'Current plan'
    : showDemoTrialButton
      ? 'Start Free Trial (Live Demo)'
    : (plan.ctaLabel || 'Get started');

  function handlePlanBadgeClick(event, badge) {
    if (!isDemoTriggerBadge(plan, badge)) return;
    event.preventDefault();
    event.stopPropagation();
    if (event.detail !== 3) return;
    onActivateDemoMode?.(plan.id);
  }

  return (
    <div className={`price-card${isFeatured ? ' featured' : ''}${isCurrent ? ' is-current' : ''}`}>
      {isFeatured && plan.badges?.length > 0 && (
        <span
          className={`price-tag ${isDemoTriggerBadge(plan, plan.badges[0]) ? 'plan-demo-trigger' : ''}`.trim()}
          onClick={(event) => handlePlanBadgeClick(event, plan.badges[0])}>
          {plan.badges[0]}
        </span>
      )}
      <div className="price-tier-name">{plan.name}</div>
      <div className="price-amount">
        {isCustomPrice ? (
          <span className="num">{price}</span>
        ) : (
          <>
            <span className="num">${price}</span>
            <span className="per">/ month</span>
            {showWasPrice && <span className="price-was">${plan.displayPrice.monthly}</span>}
          </>
        )}
      </div>
      <span className="price-amount-meta">
        {isCustomPrice
          ? 'tailored to your team'
          : period === 'annually' ? 'billed annually' : 'billed monthly'}
      </span>
      {plan.description && <p className="price-description">{plan.description}</p>}
      <ul className="price-features">
        {plan.features.map((feature, index) => (
          <li key={`${feature}-${index}`}>{renderFeatureMarkup(feature)}</li>
        ))}
      </ul>
      <Button
        variant={isFeatured ? 'primary' : 'secondary'}
        className="price-cta"
        href={plan.ctaUrl || undefined}
        target={plan.ctaUrl ? '_blank' : undefined}
        rel={plan.ctaUrl ? 'noopener noreferrer' : undefined}
        disabled={isCurrent}
        loading={choosing}
        loadingLabel="Opening Stripe..."
        onClick={(event) => {
          event.stopPropagation();
          if (isCurrent) return;
          if (!plan.ctaUrl) {
            if (showDemoTrialButton) {
              onChoose(plan.id, plan, { isDemoMode: true });
              return;
            }
            onChoose(plan.id, plan);
          }
        }}>
        {ctaLabel}{!isCurrent && <span className="price-cta-arrow" aria-hidden="true">→</span>}
      </Button>
      {!isCustomPrice && (
        <span className="price-cta-meta">14-day free trial · cancel anytime</span>
      )}
    </div>
  );
}

function PlanPicker({ period, setPeriod, loading, plans, onChoose, choosingPlan, currentPlanId = '', currentBillingPeriod = '', initialDemoMode = false }) {
  const [isDemoModeActive, setIsDemoModeActive] = useStateOnb(Boolean(initialDemoMode));
  const billingAttr = period === 'annually' ? 'annual' : 'monthly';
  const legacyCurrentBillingPeriod = currentBillingPeriod && !isPublicBillingPeriod(currentBillingPeriod);
  return (
    <div className="armature-pricing" data-billing={billingAttr}>
      <div className="pricing-toggle-wrap">
        <div className="pricing-toggle" role="group" aria-label="Billing period">
          <button
            type="button"
            className={period === 'monthly' ? 'active' : ''}
            aria-pressed={period === 'monthly'}
            onClick={() => setPeriod('monthly')}>
            Monthly
          </button>
          <button
            type="button"
            className={period === 'annually' ? 'active' : ''}
            aria-pressed={period === 'annually'}
            onClick={() => setPeriod('annually')}>
            Annual <span className="save">−20%</span>
          </button>
        </div>
      </div>
      {loading ? (
        <div className="pricing-loading"><LoadingSpinner size="sm" label="Loading plans" />Loading plans...</div>
      ) : (
        <div className="pricing-grid">
          {plans.map((plan) => {
            const isCurrent = currentPlanId === plan.id && (currentBillingPeriod === period || legacyCurrentBillingPeriod);
            const isFeatured = plan.id === 'pro';
            return (
              <PlanCard
                key={plan.id}
                plan={plan}
                period={period}
                isFeatured={isFeatured}
                isCurrent={isCurrent}
                choosing={choosingPlan === plan.id}
                onChoose={onChoose}
                isDemoModeActive={isDemoModeActive}
                onActivateDemoMode={() => setIsDemoModeActive(true)}
              />
            );
          })}
        </div>
      )}
    </div>
  );
}

function PlanPage({ navigate, queryString = '' }) {
  const auth = useAuth();
  const params = useMemoOnb(() => new URLSearchParams(queryString || ''), [queryString]);
  const requestedBilling = params.get('billing');
  const [period, setPeriod] = useStateOnb(isPublicBillingPeriod(requestedBilling) ? requestedBilling : 'monthly');
  const [message, setMessage] = useStateOnb(params.get('canceled') ? 'Checkout was canceled. Pick a plan to continue.' : '');
  const [choosingPlan, setChoosingPlan] = useStateOnb('');
  const [skippingPlan, setSkippingPlan] = useStateOnb(false);
  const { loading, plans, error } = usePlans(period);
  const initialDemoMode = params.get('demo') === '1';

  async function choose(planId, plan = null, options = {}) {
    if (plan?.contactSales) {
      openCustomPlanCta(plan);
      return;
    }
    setMessage('');
    setChoosingPlan(planId);
    try {
      const result = await apiFetch('/api/billing/checkout-session', {
        method: 'POST',
        body: JSON.stringify({
          plan_id: planId,
          billing_period: publicBillingPeriodOrDefault(period),
          is_demo_mode: options.isDemoMode === true,
        }),
      });
      window.location.assign(result.url);
    } catch (err) {
      setMessage(err.message);
    } finally {
      setChoosingPlan('');
    }
  }

  async function skipPlan() {
    setMessage('');
    setSkippingPlan(true);
    try {
      const result = await apiFetch('/api/onboarding/plan-skip', { method: 'POST' });
      await auth.refreshMe({ flush: true });
      navigate(result?.nextPath || '/onboarding/role');
    } catch (err) {
      setMessage(err.message);
    } finally {
      setSkippingPlan(false);
    }
  }

  return (
    <OnboardingShell step="plan" navigate={navigate} backPath="/onboarding/workspace">
      <div className="onboarding-wide onboarding-pricing-wide">
        <div className="pricing-head">
          <span className="pricing-eyebrow">Pricing</span>
          <h1 className="pricing-title">Simple pricing. 14-day free trial.</h1>
          <p className="pricing-lede">Try free for 14 days. Credit card required. Cancel anytime.</p>
        </div>
        {error && <div className="auth-message">{error.message}</div>}
        {message && <div className="auth-message">{message}</div>}
        <PlanPicker
          period={period}
          setPeriod={setPeriod}
          loading={loading}
          plans={plans}
          onChoose={choose}
          choosingPlan={choosingPlan}
          initialDemoMode={initialDemoMode}
        />
        <div className="plan-skip-actions">
          <Button variant="ghost" disabled={Boolean(choosingPlan) || skippingPlan} loading={skippingPlan} loadingLabel="Skipping..." onClick={skipPlan}>Skip for now</Button>
        </div>
      </div>
    </OnboardingShell>
  );
}

function PlanReturnPage({ navigate, queryString = '' }) {
  const auth = useAuth();
  const [message, setMessage] = useStateOnb('Confirming Checkout...');
  useEffectOnb(() => {
    let cancelled = false;
    async function confirm() {
      const params = new URLSearchParams(queryString || '');
      const sessionId = params.get('session_id');
      const returnTo = validateBrowserReturnPath(params.get('return_to'));
      if (!sessionId) {
        setMessage('Missing Checkout session id.');
        return;
      }
      try {
        await apiFetch(`/api/billing/session/${encodeURIComponent(sessionId)}`);
        const me = await auth.refreshMe({ flush: true });
        const step = me?.profile?.onboardingState?.currentStep;
        if (TERMINAL_ONBOARDING_STEPS.has(step)) {
          if (!cancelled) navigate(returnTo || '/dashboard');
          return;
        }
        if (!cancelled) navigate('/onboarding/role');
      } catch (error) {
        if (!cancelled) setMessage(error.message);
      }
    }
    confirm();
    return () => {
      cancelled = true;
    };
  }, [queryString]);

  return (
    <OnboardingShell step="plan" navigate={navigate}>
      <div className="onboarding-card">
        <div className="code inline-loading"><LoadingSpinner size="sm" label="Confirming" />{message}</div>
      </div>
    </OnboardingShell>
  );
}

function RolePage({ navigate }) {
  const auth = useAuth();
  const [busy, setBusy] = useStateOnb(false);
  const [message, setMessage] = useStateOnb('');
  const isInviteeOnboarding = Boolean(auth?.me?.role && auth.me.role !== 'owner');

  // Tester defaults picker. Only owners/admins set the workspace-wide
  // defaults — invitees skip this section entirely (the API would 403
  // them anyway). Catalog fetch is unconditional but cheap; pages-workflows
  // already keys it once on the dashboard so the response is typically
  // pre-warmed by the time the user lands here.
  const catalog = useApiResource(isInviteeOnboarding ? null : '/api/catalog/models');
  const modelOptions = catalog.data?.models || [];
  const orgDefaultTargets = auth?.me?.organization?.workflowDefaultTargets || [];
  const [targetRows, setTargetRows] = useStateOnb([]);
  // Invitee-only chip state. Owners derive chips from `targetRows` so
  // the picker and the chips stay in sync through any edit path;
  // invitees never see the picker (the API forbids
  // workflow_default_targets writes for non-owners) so they need their
  // own small Set.
  const [inviteeHarnessKeys, setInviteeHarnessKeys] = useStateOnb(() => new Set());

  // Canonical harness list, filtered to the ones a user can actually
  // pick today (the "coming soon" harness has `disabled: true`).
  const harnessOptions = useMemoOnb(
    () => getWorkflowHarnessOptions().filter((option) => !option.disabled),
    [],
  );

  const ownerHarnessKeys = useMemoOnb(
    () => new Set(targetRows.map((row) => row.harness).filter(Boolean)),
    [targetRows],
  );
  const selectedHarnessKeys = isInviteeOnboarding ? inviteeHarnessKeys : ownerHarnessKeys;

  // Seed targetRows from any prior org-level selection (returning
  // owner re-entering the role step). Unlike the previous design we
  // do NOT fall back to a hard-coded system default — an empty
  // `workflow_default_targets` means "ask the user to pick", and the
  // picker stays hidden until the first chip is ticked.
  useEffectOnb(() => {
    if (isInviteeOnboarding || modelOptions.length === 0) return;
    const seedSource = targetRows.length > 0 ? targetRows : orgDefaultTargets;
    const kept = normalizeWorkflowTargetRows(modelOptions, seedSource);
    if (workflowTargetRowsKey(kept) !== workflowTargetRowsKey(targetRows)) {
      setTargetRows(kept);
    }
    // Key on a content hash of the org defaults so a same-size swap
    // (owner returning and the workspace defaults changed between
    // visits) still re-seeds. Excluding `targetRows` keeps manual
    // edits and chip toggles from being clobbered on re-render.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [isInviteeOnboarding, modelOptions.length, workflowTargetRowsKey(orgDefaultTargets)]);

  function toggleHarness(harnessKey) {
    if (busy) return;
    if (isInviteeOnboarding) {
      setInviteeHarnessKeys((prev) => {
        const next = new Set(prev);
        if (next.has(harnessKey)) next.delete(harnessKey);
        else next.add(harnessKey);
        return next;
      });
      return;
    }
    if (ownerHarnessKeys.has(harnessKey)) {
      // Drop every row for this harness — including ones the user
      // added through the picker — so the chip and the rows agree.
      setTargetRows((rows) => rows.filter((row) => row.harness !== harnessKey));
      return;
    }
    const defaultModel = pickDefaultModelForHarness(modelOptions, harnessKey);
    if (!defaultModel) return;
    setTargetRows((rows) => [...rows, { harness: harnessKey, modelId: defaultModel.id }]);
  }

  // Composed `use_case` string sent to the server. Stable order from
  // the harness catalog so two users picking the same set produce the
  // same row, and labels-only joining keeps us well under the 120-char
  // server cap even with every harness ticked.
  const useCaseFromHarnesses = useMemoOnb(() => {
    const labels = harnessOptions
      .filter((option) => selectedHarnessKeys.has(option.key))
      .map((option) => option.label);
    return labels.length > 0 ? `Clients: ${labels.join(', ')}` : '';
  }, [harnessOptions, selectedHarnessKeys]);

  const hasSelection = selectedHarnessKeys.size > 0;
  const canContinue = hasSelection && !busy;
  // Strictly the fetch-in-flight signal. An empty `modelOptions` is a
  // valid *settled* state — the per-chip `harnessHasModel` guard below
  // disables those chips and surfaces a tooltip explaining why; folding
  // emptiness into the loading flag would leave owners in a workspace
  // without any active models stuck behind a spinner forever.
  const isCatalogLoading = !isInviteeOnboarding && catalog.loading;

  async function submit(skip = false) {
    setBusy(true);
    setMessage('');
    try {
      const useCase = !skip ? useCaseFromHarnesses : '';
      if (isInviteeOnboarding) {
        try {
          const payload = useCase ? { roleStepComplete: false, useCase } : null;
          if (payload) {
            await apiFetch('/api/onboarding/state', {
              method: 'POST',
              body: JSON.stringify(payload),
            });
          }
        } catch (error) {
          // Persisting the use-case is best-effort for invitees; don't block dashboard.
        }
        navigate('/dashboard');
        return;
      }
      const payload = { roleStepComplete: true };
      if (!skip && useCase) {
        payload.useCase = useCase;
      }
      // Only send the targets payload when the user actually saw the
      // picker (i.e. didn't skip and the catalog had a chance to load).
      // An explicit empty array would clear an existing selection, which
      // is wrong if the picker never rendered.
      if (!skip && modelOptions.length > 0) {
        payload.workflowDefaultTargets = buildWorkflowTesterTargets(targetRows, modelOptions);
      }
      const result = await apiFetch('/api/onboarding/state', {
        method: 'POST',
        body: JSON.stringify(payload),
      });
      await auth.refreshMe({ flush: true });
      navigate(result?.nextPath || '/dashboard');
    } catch (error) {
      setMessage(error.message);
    } finally {
      setBusy(false);
    }
  }

  const chipsDisabled = busy || isCatalogLoading;
  const showPicker = !isInviteeOnboarding && targetRows.length > 0;

  return (
    <OnboardingShell step="role" navigate={navigate} backPath={isInviteeOnboarding ? null : '/onboarding/plan'}>
      <div className="onboarding-card onboarding-role-card">
        <button className="onboarding-corner-skip" disabled={busy} onClick={() => submit(true)}>Skip</button>
        <div className="onboarding-kicker">Question</div>
        <h1>Tell us about you</h1>
        <div className="role-question-list">
          <section className="role-question">
            <div className="role-question-heading">
              <h2>Which clients do your users connect from?</h2>
              <p className="muted">Pick everything that applies — we'll preselect these clients on every new workflow.</p>
            </div>
            <div className="role-chip-grid role-chip-grid-harness">
              {harnessOptions.map((option) => {
                const isSelected = selectedHarnessKeys.has(option.key);
                // Owners need at least one active model behind a harness to
                // attach a row to it; without one, `toggleHarness` would
                // silently no-op. Disabling the chip surfaces that state
                // instead of giving the user a dead click. Invitees never
                // hit the picker so the catalog gate doesn't apply to them.
                const harnessHasModel = isInviteeOnboarding
                  || (modelOptions.length > 0 && getModelsForWorkflowHarness(modelOptions, option.key).length > 0);
                const chipDisabled = chipsDisabled || !harnessHasModel;
                return (
                  <button
                    key={option.key}
                    type="button"
                    className={`role-harness-chip${isSelected ? ' selected' : ''}`}
                    aria-pressed={isSelected}
                    disabled={chipDisabled}
                    title={!harnessHasModel && !isCatalogLoading ? `No active ${option.label} models available yet` : undefined}
                    onClick={() => toggleHarness(option.key)}>
                    {option.logoSrc ? (
                      <img className="role-harness-chip-logo" src={option.logoSrc} alt="" />
                    ) : (
                      <span className="role-harness-chip-logo role-harness-chip-logo-fallback">
                        {option.label.slice(0, 2)}
                      </span>
                    )}
                    <span className="role-harness-chip-label">{option.label}</span>
                    {isSelected && <Icon name="check" size={12} className="role-harness-chip-check" />}
                  </button>
                );
              })}
            </div>
            {isCatalogLoading && (
              <div className="role-targets-loading">
                <LoadingSpinner size="sm" label="Loading harnesses and models" />Loading harnesses and models...
              </div>
            )}
          </section>
          {showPicker && (
            <section className="role-question">
              <div className="role-question-heading">
                <h2>Confirm your test plan</h2>
                <p className="muted">We picked a recommended model per client. Change models or add more — you can edit any of this per workflow later.</p>
              </div>
              <WorkflowTargetPicker
                models={modelOptions}
                modelIds={targetRows.map((row) => row.modelId)}
                targetRows={targetRows}
                onChange={() => {
                  // No-op: the picker also fires onChangeTargets with the full
                  // {harness, modelId} rows, which is the only shape we
                  // persist to organizations.workflow_default_targets. The
                  // sibling onChange(modelIds) callback exists for the
                  // workflow editor's legacy modelIds path.
                }}
                onChangeTargets={setTargetRows}
                disabled={busy}
              />
            </section>
          )}
        </div>
        {message && <div className="field-help error-text" role="alert">{message}</div>}
        <Button variant="primary" disabled={!canContinue} loading={busy} loadingLabel="Saving..." onClick={() => submit(false)}>
          Continue<Icon name="chevronRight" size={13} />
        </Button>
      </div>
    </OnboardingShell>
  );
}

// Onboarding "add your first MCP" form. A thin onboarding-only wrapper
// around the same `useMcpConnectFormState` + `McpConnectFormBody` +
// `validateMcpConnectFormInputs` + `buildMcpConnectPayload` shipped by
// `pages-mcp-servers.jsx`. The submit path also runs through the shared
// `runMcpOAuthFlow` helper, so loopback paste-back providers (Vercel,
// etc.) and popup-blocker fallbacks behave identically here and inside
// the production Connect MCP server modal. Onboarding-only behaviors
// stay local: the form mounts inline (no modal chrome), persists
// `lastServerId` to the onboarding state, and best-effort cleans up
// orphan server rows when an OAuth attempt aborts before the auth
// profile is attached so a retry doesn't leak rows.
function OnboardingConnectForm({ onConnected, onSkip, hideSkip = false }) {
  const formState = useMcpConnectFormState({ targetKind: 'mcp' });
  const nameInputRef = useRefOnb(null);

  const [busy, setBusy] = useStateOnb(false);
  const [message, setMessage] = useStateOnb('');
  const [probe, setProbe] = useStateOnb(null);
  // Mirrors the production modal's "OAuth handshake in flight" state —
  // while non-null we render `<McpOauthAwaitingPanel />` in place of the
  // form. Loopback flows expose `redirectUriMode === 'loopback'` so the
  // copy can guide the user to the paste-back dialog rendered by
  // `<LoopbackOAuthModalRoot />` further down this component.
  const [oauthInFlight, setOauthInFlight] = useStateOnb(/** @type {{popup: Window | null, providerLabel: string, redirectUriMode: string} | null} */ (null));
  // Hosted-MCP provisioning progress for the "Local (npm)" path. Same
  // structure as TargetSourcesPage's CLI setup state — keeps the UI
  // identical when a user picks a Local target during onboarding.
  const [hostedSetupState, setHostedSetupState] = useStateOnb(/** @type {{serverId: string, serverName: string, provisioningRunId: string | null, status: string, failedAtStatus: string | null, errorCode: string | null, updatedAt: string} | null} */ (null));
  const [hostedPollCount, setHostedPollCount] = useStateOnb(0);

  const isOauth = formState.authType === 'oauth_authorization_code';

  // Best-effort orphan cleanup. Called when an OAuth attempt aborted before
  // the auth profile finished attaching (popup closed/failed). The MCP
  // server record was already created up-front by `POST /api/mcp-servers`,
  // and onboarding's form stays mounted across retries (unlike the
  // production modal, which closes immediately after `startOAuthConnect`
  // resolves), so without this cleanup each retry would leak a fresh row
  // with no usable auth profile attached. Errors are swallowed so a flaky
  // delete cannot block the user from seeing the inline retry message.
  async function deleteOrphanServer(serverId) {
    if (!serverId) return;
    try {
      await apiFetch(`/api/mcp-servers/${serverId}`, { method: 'DELETE' });
    } catch (_error) {
      // Intentional: surface the original failure, not the cleanup miss.
    }
  }

  // Hosted-MCP provisioning poll. Mirrors the per-run polling in
  // TargetSourcesPage but writes into `hostedSetupState`.
  useEffectOnb(() => {
    if (!hostedSetupState?.serverId) return undefined;
    const status = hostedSetupState.status;
    if (status && status !== 'pending' && status !== 'provisioning' && status !== 'discovering') {
      return undefined;
    }
    if (hostedPollCount >= 90) return undefined;
    let cancelled = false;
    const timer = setTimeout(async () => {
      if (cancelled) return;
      try {
        let nextStatus = null;
        let errorCode = null;
        let updatedAt = null;
        const runId = hostedSetupState.provisioningRunId;
        if (runId) {
          const data = await apiFetch(`/api/mcp-servers/${hostedSetupState.serverId}/provisioning-runs/${runId}`);
          const run = data?.provisioningRun || null;
          if (run) {
            errorCode = run.error_code || null;
            updatedAt = run.completed_at || run.started_at || null;
            const phase = run.result?.phase || run.result?.status || null;
            if (run.status === 'failed' || run.status === 'canceled') nextStatus = 'failed';
            else if (run.status === 'completed') nextStatus = 'ready';
            else if (phase === 'discovering') nextStatus = 'discovering';
            else if (phase === 'provisioning' || phase === 'installing') nextStatus = 'provisioning';
            else if (run.status === 'running') nextStatus = 'provisioning';
            else nextStatus = 'pending';
          }
        }
        if (cancelled) return;
        if (nextStatus) {
          setHostedSetupState((current) => current ? {
            ...current,
            status: nextStatus,
            // Freeze the failed-at phase so the timeline keeps the red node
            // anchored where the failure happened (mirrors TargetSourcesPage).
            failedAtStatus: nextStatus === 'failed'
              ? (current.failedAtStatus
                || (['pending', 'provisioning', 'discovering'].includes(current.status) ? current.status : 'pending'))
              : null,
            errorCode: errorCode || current.errorCode || null,
            updatedAt: updatedAt || current.updatedAt,
          } : current);
          if (nextStatus === 'ready' && hostedSetupState.serverId) {
            await persistOnboardingState({ lastServerId: hostedSetupState.serverId });
            onConnected?.(hostedSetupState.serverId);
          }
        }
        setHostedPollCount((count) => count + 1);
      } catch (_error) {
        if (!cancelled) setHostedPollCount((count) => count + 1);
      }
    }, 2000);
    return () => {
      cancelled = true;
      clearTimeout(timer);
    };
  }, [hostedSetupState?.serverId, hostedSetupState?.status, hostedPollCount, onConnected]);

  async function submit(event) {
    event.preventDefault();
    if (busy) return;
    setMessage('');
    setProbe(null);

    const validationError = validateMcpConnectFormInputs(formState);
    if (validationError) {
      setMessage(validationError);
      return;
    }

    setBusy(true);
    let createdServerId = null;
    // Tracks whether the server has an auth profile attached and is therefore
    // legitimately owned by the user (so the catch block's orphan cleanup
    // must NOT delete it on a downstream probe failure). Non-OAuth flows
    // attach the auth profile inline at create time; OAuth flows only
    // become "attached" after `runMcpOAuthFlow` returns `connected`/`pending`.
    let authAttached = !isOauth;
    try {
      const built = buildMcpConnectPayload(formState);
      const created = await apiFetch('/api/mcp-servers', {
        method: 'POST',
        body: JSON.stringify(built.payload),
      });
      createdServerId = created?.mcpServer?.id || null;

      if (createdServerId && built.kind === 'hosted') {
        // Hosted (Local) MCP runs through the same provisioning state
        // machine as a CLI target. Stash setup state, poll for progress,
        // and let the user wait inside the onboarding form — when status
        // flips to `ready`, the polling effect persists `lastServerId`
        // and calls `onConnected`.
        setHostedSetupState({
          serverId: createdServerId,
          serverName: formState.serverName.trim(),
          provisioningRunId: created?.provisioningRun?.id || null,
          status: created?.mcpServer?.provisioning_status || 'pending',
          failedAtStatus: null,
          errorCode: null,
          updatedAt: new Date().toISOString(),
        });
        setHostedPollCount(0);
        authAttached = true;
        return;
      }

      if (createdServerId && built.kind === 'remote' && built.oauth) {
        const oauthStart = await startOAuthConnect({
          mcpServerId: createdServerId,
          providerSlug: built.oauth.providerSlug,
          requestedScopes: built.oauth.requestedScopes,
          authConfig: {},
        });
        const inFlight = {
          popup: null,
          providerLabel: built.oauth.providerSlug || 'the provider',
          redirectUriMode: oauthStart?.redirectUriMode === 'loopback' ? 'loopback' : 'web',
        };
        setOauthInFlight(inFlight);
        // The inner finally guarantees the awaiting panel is taken down
        // on every exit path, including unexpected throws from
        // runMcpOAuthFlow / its inner awaits.
        let popupResult;
        try {
          popupResult = await runMcpOAuthFlow(oauthStart, {
            onPopupOpened: (popup) => setOauthInFlight((current) => current ? { ...current, popup } : current),
          });
        } finally {
          setOauthInFlight(null);
        }
        if (popupResult.outcome === 'navigated') {
          // Popup was blocked; the same tab is now navigating to the
          // authorize URL, so unwinding state here is moot. Leaving the
          // server alive — when the user returns to onboarding, callback
          // handling on the auth profile will surface its status.
          authAttached = true;
          await persistOnboardingState({ lastServerId: createdServerId });
          return;
        }
        if (popupResult.outcome === 'closed') {
          await deleteOrphanServer(createdServerId);
          setMessage('The authorization window was closed before consent finished. Try again to connect.');
          return;
        }
        if (popupResult.outcome === 'failed') {
          await deleteOrphanServer(createdServerId);
          const text = popupResult.message || 'OAuth authorization did not complete.';
          setProbe({ ok: false, errorMessage: text, tools: [] });
          setMessage(text);
          return;
        }
        if (popupResult.outcome === 'pending') {
          authAttached = true;
          await persistOnboardingState({ lastServerId: createdServerId });
          setMessage('OAuth completed; provisioning is still in progress. Tools will appear once it finishes.');
          return;
        }
        // popupResult.outcome === 'connected' — fall through to probe.
        // Mark attached BEFORE the probe so a probe-time failure thrown
        // into the catch block does not delete an authorized server.
        authAttached = true;
      }

      const result = await apiFetch(`/api/mcp-servers/${createdServerId}/probe`, { method: 'POST' });
      setProbe(result);
      await persistOnboardingState({ lastServerId: createdServerId });
      if (result.ok) onConnected?.(createdServerId);
    } catch (error) {
      // The exception covers both the create POST and any downstream call.
      // Only clean up when the server is still an *orphan* — i.e. no auth
      // profile has been attached yet. After `connected`/`pending` (or the
      // non-OAuth path's inline attach), the server is legitimately the
      // user's even if a later step like the probe fails, and deleting it
      // would silently discard their authorization.
      if (createdServerId && !authAttached) await deleteOrphanServer(createdServerId);
      if (createdServerId && authAttached) {
        try {
          await persistOnboardingState({ lastServerId: createdServerId });
        } catch (_persistError) {
          // persistOnboardingState already swallows by default; the catch
          // here only fires if the caller passed throwOnError. We didn't.
        }
      }
      setMessage(error.message);
      setProbe({ ok: false, errorMessage: error.message, tools: [] });
    } finally {
      setBusy(false);
    }
  }

  // Submit-button copy mirrors the modal — "Connect MCP server" /
  // "Connect with OAuth" — and surfaces hosted-provisioning state when
  // the user picked Local. The button stays disabled while the form is
  // invalid (`formState.canConnect` runs through the same predicate the
  // production modal uses), or while a hosted setup is mid-flight.
  const submitDisabled = !formState.canConnect || busy || Boolean(hostedSetupState);
  const submitLabel = isOauth ? 'Connect with OAuth' : (formState.isHostedMcpConnect ? 'Provision MCP server' : 'Test connection');
  const submitLoadingLabel = isOauth
    ? 'Starting OAuth...'
    : formState.isHostedMcpConnect
      ? 'Provisioning...'
      : 'Testing...';

  return (
    <>
      {/* `LoopbackOAuthModalRoot` lives at the app root in app.jsx so a
          single instance owns paste-back flows. We rely on it being
          mounted upstream — the modal itself is a no-op until a flow
          calls `presentLoopbackOAuthFlow`. */}
      <form className="onboarding-connect-form" onSubmit={submit}>
        {oauthInFlight ? (
          <McpOauthAwaitingPanel inFlight={oauthInFlight} />
        ) : hostedSetupState ? (
          <CliSetupProgress state={hostedSetupState} />
        ) : (
          <McpConnectFormBody
            formState={formState}
            saving={busy}
            nameInputRef={nameInputRef} />
        )}

        {probe && !probe.ok && !oauthInFlight && !hostedSetupState && (
          <div className="tool-wizard-error">
            <strong>Could not list tools.</strong>
            <p>{probe.errorMessage || 'Unknown MCP error'}</p>
            <ul>
              <li>Confirm the URL points to a Streamable HTTP MCP endpoint.</li>
              <li>Check that the auth credentials match what your server expects.</li>
              <li>Check CORS and network reachability from this origin.</li>
            </ul>
          </div>
        )}
        {message && <div className="auth-message">{message}</div>}

        {!oauthInFlight && (
          <div className="onboarding-actions">
            {!hideSkip && (
              <button type="button" className="onboarding-skip" onClick={onSkip}>Skip for now</button>
            )}
            {!hostedSetupState && (
              <Button
                variant="primary"
                type="submit"
                disabled={submitDisabled}
                loading={busy}
                loadingLabel={submitLoadingLabel}>
                <Icon name="mcp" size={13} />
                {submitLabel}
              </Button>
            )}
          </div>
        )}
      </form>
    </>
  );
}

function ConnectPage({ navigate }) {
  useEffectOnb(() => {
    navigate('/dashboard');
  }, []);
  return null;
}

function OnboardingMonitorWizard({ serverId, onCompleted, onSkip, hideSkip = false, onReadyChange = null, hidePrimaryAction = false, onPrimaryActionChange = null }) {
  const auth = useAuth();
  const [probe, setProbe] = useStateOnb(null);
  const [selectedTools, setSelectedTools] = useStateOnb({});
  const [loadingProbe, setLoadingProbe] = useStateOnb(false);
  const [saving, setSaving] = useStateOnb(false);
  const [message, setMessage] = useStateOnb('');
  const [agentState, setAgentState] = useStateOnb(/** @type {{ status: string, error?: string }} */ ({ status: 'idle' }));

  useEffectOnb(() => {
    if (typeof onReadyChange !== 'function') return;
    const probeDone = !loadingProbe && probe !== null;
    const hasTools = probe?.ok && Array.isArray(probe.tools) && probe.tools.length > 0;
    const agentDone = agentState.status === 'ready' || agentState.status === 'error';
    onReadyChange(probeDone && (!hasTools || agentDone));
  }, [loadingProbe, probe, agentState.status, onReadyChange]);

  useEffectOnb(() => {
    if (!serverId) return undefined;
    let cancelled = false;
    async function runAgentFill(tools) {
      if (!tools || tools.length === 0) return;
      setAgentState({ status: 'loading' });
      try {
        const data = await apiFetch(`/api/mcp-servers/${serverId}/agent-fill`, {
          method: 'POST',
          body: JSON.stringify({ tools }),
        });
        if (cancelled) return;
        setSelectedTools((cur) => window.mergeAgentFills(cur, tools, data?.fills || {}));
        setAgentState({ status: 'ready' });
      } catch (err) {
        if (cancelled) return;
        setAgentState({ status: 'error', error: err.message });
      }
    }
    async function loadTools() {
      setLoadingProbe(true);
      setProbe(null);
      setMessage('');
      try {
        const result = await apiFetch(`/api/mcp-servers/${serverId}/probe`, { method: 'POST' });
        if (cancelled) return;
        setProbe(result);
        if (result?.ok && Array.isArray(result.tools)) {
          setSelectedTools(buildInitialToolState(result.tools));
          runAgentFill(result.tools);
        }
      } catch (error) {
        if (!cancelled) setMessage(error.message);
      } finally {
        if (!cancelled) setLoadingProbe(false);
      }
    }
    loadTools();
    return () => { cancelled = true; };
  }, [serverId]);

  function retryAgentFill() {
    if (!serverId || !probe?.ok) return;
    setAgentState({ status: 'loading' });
    apiFetch(`/api/mcp-servers/${serverId}/agent-fill`, {
      method: 'POST',
      body: JSON.stringify({ tools: probe.tools || [] }),
    })
      .then((data) => {
        setSelectedTools((cur) => window.mergeAgentFills(cur, probe.tools || [], data?.fills || {}));
        setAgentState({ status: 'ready' });
      })
      .catch((err) => setAgentState({ status: 'error', error: err.message }));
  }

  async function save() {
    if (!probe?.ok) return;
    const validation = validateAllSelectedTools(probe.tools, selectedTools);
    if (validation.hasErrors) {
      setSelectedTools(validation.next);
      return;
    }
    setSaving(true);
    setMessage('');
    try {
      const picked = probe.tools.filter((tool) => validation.next[tool.name]?.checked);
      const monitorIds = [];
      for (const tool of picked) {
        const cfg = validation.next[tool.name];
        const created = await apiFetch(`/api/mcp-servers/${serverId}/tool-monitors`, {
          method: 'POST',
          body: JSON.stringify({
            toolName: tool.name,
            intervalSeconds: cfg.interval || 300,
            arguments: buildArgsFromState(tool, cfg),
            inputSchema: tool.inputSchema,
          }),
        });
        if (created?.monitor?.id) {
          monitorIds.push(created.monitor.id);
          await apiFetch(`/api/tool-monitors/${created.monitor.id}/run-now`, { method: 'POST' });
        }
      }
      await persistOnboardingState({ lastServerId: serverId, lastMonitorIds: monitorIds });
      await auth.refreshMe();
      onCompleted?.(monitorIds);
    } catch (error) {
      setMessage(error.message);
    } finally {
      setSaving(false);
    }
  }

  return (
    <div className="onboarding-monitor-body">
      {message && <div className="auth-message">{message}</div>}
      {loadingProbe && <ToolWizardLoading phase="probe" />}
      {probe?.ok && (
        <ToolMonitorWizard
          tools={probe.tools || []}
          selectedTools={selectedTools}
          setSelectedTools={setSelectedTools}
          saving={saving}
          onSave={save}
          onCancel={onSkip}
          showCancel={false}
          agentState={agentState}
          onAgentRetry={retryAgentFill}
          savingMode="overlay"
          hidePrimaryAction={hidePrimaryAction}
          onPrimaryActionChange={onPrimaryActionChange}
        />
      )}
      {!hideSkip && (
        <button type="button" className="onboarding-skip" onClick={onSkip}>Skip for now</button>
      )}
    </div>
  );
}

function MonitorPage({ navigate }) {
  useEffectOnb(() => {
    navigate('/dashboard');
  }, []);
  return null;
}

function DonePage({ navigate }) {
  useEffectOnb(() => {
    navigate('/dashboard');
  }, []);
  return null;
}

window.WorkspacePage = WorkspacePage;
window.OnboardingProgress = OnboardingProgress;
window.PlanPage = PlanPage;
window.PlanReturnPage = PlanReturnPage;
window.RolePage = RolePage;
window.OnboardingConnectForm = OnboardingConnectForm;
window.ConnectPage = ConnectPage;
window.OnboardingMonitorWizard = OnboardingMonitorWizard;
window.MonitorPage = MonitorPage;
window.DonePage = DonePage;
window.PlanPicker = PlanPicker;
window.usePlans = usePlans;
window.openCustomPlanCta = openCustomPlanCta;
window.trackOnboardingEvent = trackOnboardingEvent;
window.persistOnboardingState = persistOnboardingState;
