/* MatchPlan.ai — root app + routing. */

// pitchiq#522 — there is ONE entry call per page load: GET /api/me, which the shared `evolve_session`
// cookie authenticates (the browser sends it by itself). It answers the person, or the settled refusal
// (not enabled, no access, no workspace), or 401 — with the server's own word on whether the cookie was
// ABSENT. This used to be two doors — a POST /auth/sso-bootstrap for a visitor with no Edge token, and
// the token holder's re-validation — and the Edge token that told them apart is gone. Every route
// loads this way, /login and /onboarding included: a visitor with no cookie is answered 401 without
// the server asking the platform anything.

// ---- Signed-out visitor -> Suite sign-in (issue #675) ----------------------
//
// THE DECISION ITSELF IS NOT HERE. It is one pure function in
// web/app/suite-redirect.js, loaded by its own <script> before this file
// (web/index.html:51). What lives here is the wiring, and the wiring is where
// the harm lives: this is the code that calls location.replace and takes a real
// person off Edge, away from Edge's own working login.
//
// Two rules shape everything below.
//
// G7 — DECIDE ONCE PER PAGE LOAD, THEN LATCH. Every input except `authed` is
// frozen at load: `__mpFreshArrival` is hash-presence at load, the shared
// session is what one GET /me answered, the Suite URL is what one fetch
// returned. `authed` is the only one that moves. Re-evaluating later therefore
// mixes a stale snapshot with a fresh `authed` — and an earlier draft of this
// work did exactly that and fired the redirect ON THE SIGN-OUT CLICK: sign-out
// is a hash change with no reload (pages-settings.jsx:56), so the whole stale
// snapshot was still sitting there saying "signed out, fresh arrival, go".
//
// C6 — DECIDE ONLY ONCE EVERY INPUT HAS SETTLED. "Do not block the render on
// the config" and "never flash LoginPage" cannot both hold: the no-cookie
// GET /me answers locally with no platform call at all, while
// /api/public-config is a separate cold round trip, so the config is normally
// the slower of the two. Decide on the entry call alone and LoginPage renders
// first, every time, with the redirect landing afterwards — possibly while the
// visitor is typing their email into a form we are about to throw away.
//
// So: hold the existing loading state until both have settled, decide once,
// latch, and never ask again.
const CONFIG_SETTLE_TIMEOUT_MS = 1500;

// The latch. `null` means "not decided yet"; once set it is never recomputed.
let suiteDecision = null;
// The two inputs that can only be known asynchronously, captured at the moment
// they settled. Read back from here — never re-fetched — so a later evaluation
// cannot see a different world than the one that was decided on.
let suiteSharedSession = null;
let suiteConfiguredUrl = null;

/**
 * Resolve when `p` does, or to `null` after `ms` — whichever comes first.
 *
 * Waiting for the config is right; waiting for it FOREVER is a blank page for
 * someone who only wanted to sign in. On the timeout we resolve null, which
 * reads as "no Suite we can point at" and keeps the visitor on Edge.
 */
function settleWithin(p, ms) {
  return new Promise(function (resolve) {
    let done = false;
    const timer = setTimeout(function () {
      if (done) return;
      done = true;
      resolve(null);
    }, ms);
    p.then(
      function (v) { if (!done) { done = true; clearTimeout(timer); resolve(v); } },
      function () { if (!done) { done = true; clearTimeout(timer); resolve(null); } }
    );
  });
}

/**
 * Everything the decision needs — read fresh, except the two settled values.
 *
 * The `PSA.session` guards are not defensive noise. The first version of
 * #504's suite-wide sign-out defined a PSA.auth member on PSA instead; the TypeError escaped the
 * async block below, `setReady(true)` never ran, and the app sat forever on "Loading your
 * workspace…" for every one of Edge's own users. This file is plain JSX and nothing typechecks PSA's shape
 * against its callers, so a missing member has to mean something SAFE rather
 * than throw. Missing marker reads as "signed out" — the value that keeps the
 * visitor here (G1); missing override reads as "nobody asked for the hatch".
 */
function suiteSnapshot(path) {
  return {
    authed: MP.isReturning(),
    path: path,
    freshArrival: window.__mpFreshArrival === true,
    // pitchiq#352 — the arrival's raw hash, captured before index.html's #/login
    // default (index.html, before its rewrite). "" for a hashless root arrival;
    // the deep destination (e.g. "#/invite?token=…") otherwise. suite-redirect.js
    // carries it into returnUrl so a signed-out deep link survives the Suite
    // sign-in round trip.
    arrivalHash: window.__mpArrivalHash || "",
    signedOutMarker: PSA.session ? PSA.session.signedOutMarker() : true,
    // #128 — same fail-closed default as signedOutMarker above: no session module means we cannot
    // tell, and "cannot tell" must keep the visitor on Edge.
    suiteRedirected: PSA.session ? PSA.session.suiteRedirected() : true,
    sharedSession: suiteSharedSession,
    suiteUrl: suiteConfiguredUrl,
    localOverride: PSA.session ? PSA.session.localOverride() : false,
    edgeOrigin: (window.location && window.location.origin) || ""
  };
}

/**
 * The one evaluation, and the navigation it may cause. Idempotent by
 * construction: the first call decides, every later call returns that same
 * answer without looking at the world again.
 *
 * A throw latches "stay". Anything we could not compute is, by definition, not
 * the positive proof a one-way door requires.
 */
function decideSuiteRedirectOnce(path) {
  if (suiteDecision) return suiteDecision;
  try {
    suiteDecision = window.shouldRedirectToSuite(suiteSnapshot(path));
  } catch (e) {
    suiteDecision = { redirect: false, url: null };
  }
  // REPLACE, not assign. `assign` pushes a history entry, so the stack becomes
  // […prev, edge/app, suite/sign-in] and Back lands the visitor back on
  // `edge/app` — which re-evaluates from scratch (module state is gone on a
  // reload) and throws them at the Suite again, or, on a bfcache restore,
  // repaints the latched `suiteRedirecting()` spinner with no effect left to
  // re-run and no `pageshow` handling. Either way one Back press cannot reach
  // Edge's own login. `replace` leaves […prev, suite/sign-in]: Back goes
  // where the visitor actually came from. The returnUrl round trip is
  // unaffected — the Suite navigates forward, it does not go Back.
  // #128 — mark BEFORE navigating. `location.replace` does not halt execution, but the browser is
  // leaving and anything queued behind it is not guaranteed to run. This is what stops the redirect
  // firing a second time for a visitor who abandoned or failed sign-in at the Suite and came back.
  if (suiteDecision.redirect) {
    if (PSA.session) PSA.session.markSuiteRedirected();
    window.location.replace(suiteDecision.url);
  }
  return suiteDecision;
}

/** Have we decided to leave? A pure read of the latch — it never decides. */
function suiteRedirecting() {
  return suiteDecision !== null && suiteDecision.redirect === true;
}

// ---- A member denied Edge (matchiq-for-sellers#1297) ----------------------
//
// api.js has ALREADY navigated to the Suite app chooser when it could (single-shot, loop-broken — see
// leaveForSuiteAppChooser there). This file only decides what the page shows meanwhile: the loading
// screen while leaving, the server's message when it is not (no Suite URL, or the loop breaker held).
// Never the login form — the member IS signed in to the Suite — and never the Suite sign-in redirect.
// The store holds the answer (`MP.accessDenied`), set by hydrate from GET /me.

// ---- An entry door refused, and said why (pitchiq#224) -------------------
//
// GET /me's other settled refusals (no workspace on the session, the workspace unreadable, Edge not
// enabled on it, the workspace refused) each carry a message written for the member. Without them the
// render below falls through to LoginPage — telling somebody who is signed in to the Suite to go and
// sign in to the Suite. The store holds the answer (`MP.refusal`), set by hydrate.

/** pitchiq#471 — the one refusal a bfcache restore re-asks the door about (server `refuseSessionWithNoWorkspace`). */
const SESSION_HAS_NO_WORKSPACE_REFUSAL_CODE = "PLATFORM_SESSION_HAS_NO_WORKSPACE";
/** The refusal frames' way out — Coach's label for the same link (`app/app-not-enabled/page.tsx`). */
const GO_TO_SUITE_LABEL = "Go to Evolve Suite";
/**
 * Where every refusal's way out lands (pitchiq#312): the Suite's Apps page — Coach's
 * SUITE_NOT_ENABLED_PATH (`lib/api/server.ts`). It is where a workspace enables an app, and it never
 * forwards into a product, so a refused member who follows it is not sent straight back here. NOT the
 * Suite's `/choose-app`, which remembers the last app and does exactly that.
 */
const SUITE_APPS_PATH = "/apps";

/**
 * The public config's Suite URL, or null — not configured, unreadable, or not answered within
 * CONFIG_SETTLE_TIMEOUT_MS. The read api.js started at load (single-flight), so asking again costs
 * no request. Guarded like every other PSA member this file reaches for.
 */
function settledSuiteUrl() {
  return settleWithin(
    typeof PSA.publicConfig === "function" ? PSA.publicConfig() : Promise.resolve(null),
    CONFIG_SETTLE_TIMEOUT_MS
  ).then(function (cfg) { return (cfg && cfg.suiteUrl) || null; });
}

/**
 * `{ code, message }` when GET /me refused this page load and explained why, else null.
 * Guarded like `accessDeniedNow` below.
 */
function refusalNow() {
  return typeof MP.refusal === "function" ? MP.refusal() : null;
}

/**
 * pitchiq#489 — true when this page load's hydrate failed for a reason that says nothing about the session
 * (store.js `MP.firstLoadFailed`: a 500, a dropped connection, a non-JSON answer, and — pitchiq#511 — GET /me's own
 * #399 platform outage). The token is kept; the page
 * shows the Error Slot instead of the login form. Guarded like `refusalNow` above.
 */
function firstLoadFailedNow() {
  return typeof MP.firstLoadFailed === "function" && MP.firstLoadFailed();
}

/** Spec DwabVFr7XU rev 8 L16-L17 — the shell's `ErrorSlot` carries this copy itself; these two are for the page drawn when the shell script is absent. */
const FIRST_LOAD_FAILED_TEXT = "Couldn't load content.";
const FIRST_LOAD_RETRY_LABEL = "Try again";

/** `{ leaving, message }` when this page load was refused because the member may not use Edge, else null. */
function accessDeniedNow() {
  // Guarded: a missing member must mean "no denial", never a TypeError that leaves the app on the
  // loading screen forever.
  return typeof MP.accessDenied === "function" ? MP.accessDenied() : null;
}

/** How fresh Edge's own "left through a loader" stamp must be to count as a round trip (Edge → Suite door → Edge takes seconds). */
const HANDOFF_STAMP_MAX_AGE_MS = 30000;

// Whether this page load is a hand-off, decided ONCE: the stamp is consumed by its first read, and the hold is asked for twice
// (the initial state and its timer). Module state is per page load (this file is a plain script, evaluated once per load).
let arrivalIsHandoff = null;
function isHandoffArrival() {
  if (arrivalIsHandoff === null) {
    // Both signals are read, so the stamp is always consumed, even when the server's flag already answered.
    // (switch-loader.js is its own <script>: if it failed to load there is no stamp to read, and the app must still boot.)
    const stamped = window.SwitchLoader ? window.SwitchLoader.consumeLeavingStamp(HANDOFF_STAMP_MAX_AGE_MS) : false;
    arrivalIsHandoff = window.__mpHandoffArrival === true || stamped;
  }
  return arrivalIsHandoff;
}

/**
 * How much of the arrival loader's minimum is still left. index.html stamps `window.__mpLoaderShownAt` (from
 * `performance.now()`, in a requestAnimationFrame) when its static loader is really DISPLAYED, which is later than
 * navigation start: the stylesheets have to arrive first. No stamp (that script never ran) means no hold: 0.
 */
function arrivalLoaderRemainingMs() {
  // A hand-off from the Suite or Coach already showed its own loader for >= 1,500 ms on the leaving side, so no second hold
  // here: the loader stays only until the app is ready. The SERVER decides (from the Referer) and injects one boolean
  // into index.html. The hold is skipped whenever the request's Referer ORIGIN is the Suite or Coach, including a RELOAD of
  // such an arrival (browsers resend the original Referer on reload); a direct visit, an absent or unknown Referer keep it.
  // The server answers true as well for a workspace pick that carried the Suite's marker (evolve-suite#1332), whatever the Referer.
  // The other hand-off the server cannot see: Edge's own round trip (Edge → Suite door → Edge) starts on an Edge page, whose
  // Referrer-Policy is no-referrer, so the switch loader stamped this tab on its way out (see `isHandoffArrival`).
  if (isHandoffArrival()) return 0;
  const shownAt = window.__mpLoaderShownAt;
  // The minimum is the SHELL's (spec 2hT2HgPht5 r5: "at least 1,500 ms, measured from when the transition screen is
  // displayed"), so Edge keeps no number of its own; without the shell global there is no loader to hold.
  const minMs = window.EvolveShell && window.EvolveShell.SWITCHING_LOADER_MIN_MS;
  if (typeof shownAt !== "number" || typeof minMs !== "number") return 0;
  return Math.max(0, minMs - (performance.now() - shownAt));
}

function App() {
  const path = useRoute();
  const m = matchRoute(path);
  const [, force] = React.useReducer(x => x + 1, 0);
  // Stay in the loading state until GET /me has answered — otherwise the guard effect below would
  // flash-redirect to /login before the entry call ever resolves.
  const [ready, setReady] = React.useState(() => !MP.needsHydrate());
  // evolve-suite#1302 — true until the arrival loader has had its minimum, counted from when it was DISPLAYED (see
  // arrivalLoaderRemainingMs). A hold, not a delay: an app slower than the minimum is never held further, and it only
  // applies where the loader draws.
  const [arrivalHeld, setArrivalHeld] = React.useState(() => arrivalLoaderRemainingMs() > 0);
  // EG-#003 — index.html's live region announced "Opening Evolve Edge..." once; empty it when the app takes over.
  React.useEffect(() => {
    if (!ready || arrivalHeld) return;
    const status = document.getElementById("mp-arrival-status");
    if (status) status.textContent = "";
  }, [ready, arrivalHeld]);
  React.useEffect(() => {
    if (!arrivalHeld) return undefined;
    const timer = setTimeout(() => setArrivalHeld(false), arrivalLoaderRemainingMs());
    return () => clearTimeout(timer);
  }, []);

  // Hydrate from the API on first load, and re-render whenever the store changes (brief created, etc).
  // GET /me is the entry: the shared platform cookie says whether anyone is signed in, so "already
  // signed into Coach" carries over into Edge without a visible login step. Signed out, the visitor
  // falls through to the guard effect below and lands on /login as normal.
  React.useEffect(() => {
    if (!ready) {
      (async () => {
        // The Suite URL is awaited only where it is needed — a signed-out page (the redirect is decided
        // on it, C6) and the refusal pages (their link) — so a signed-in visitor never waits on it.
        // api.js started ONE read at load and every reader gets it, so asking here costs no request and
        // the entry call and the config have been in flight together since the page began.
        await MP.hydrate();

        // #1297: a member denied Edge, and api.js is already LEAVING for the Suite app chooser. Keep the
        // loader (no setReady, like the Suite redirect below); there is no sign-in decision to make —
        // they are signed in there.
        const denied = accessDeniedNow();
        if (denied && denied.leaving) return;

        // A settled refusal (pitchiq#224) or a denial that is not leaving: render the message instead
        // of the login form, with the Suite link. A reload asks GET /me again and gets the same page.
        if (denied || refusalNow()) {
          suiteConfiguredUrl = await settledSuiteUrl();
          setReady(true);
          return;
        }

        if (!MP.isReturning()) {
          // Nobody is signed in. pitchiq#345: LoginPage's "Sign in at Evolve Suite" link needs the
          // Suite URL whatever the reason, so it is settled before the page is drawn.
          suiteConfiguredUrl = await settledSuiteUrl();
          // pitchiq#489: a first load that FAILED (a 500, a dropped connection, a platform outage)
          // decided nothing about the session, so it is never the Suite sign-in redirect.
          if (!firstLoadFailedNow()) {
            // pitchiq#348 code review: the 401 body's own word on the cookie. Only "absent" (the cookie
            // NAME was missing) is positive proof of no shared session; #675's one-way redirect may
            // only open on it. Guarded: a missing accessor must fail closed to "not proof".
            suiteSharedSession = typeof PSA.auth.sharedSessionState === "function"
              ? PSA.auth.sharedSessionState()
              : "unknown";
            // Settle-then-decide (C6): every input has settled — decide once, inside this same effect,
            // before setReady, so a redirect never lets LoginPage flash first.
            if (decideSuiteRedirectOnce(path).redirect) {
              // Deliberately no setReady(true): the browser is on its way to another origin, and the
              // last thing this visitor should see on the way out is the login form we just decided
              // to take from them.
              return;
            }
          }
        }
        setReady(true);
      })();
    }
    const on = () => force();
    window.addEventListener("mp-state", on);
    return () => window.removeEventListener("mp-state", on);
  }, []);

  const authed = MP.isReturning();

  // route guards
  //
  // /auth/verify MUST be in the open set. It is the landing path of the emailed
  // magic link, and the user is BY DEFINITION unauthenticated when they arrive —
  // that is the entire point of the link. Omitting it made the guard bounce
  // every magic link straight back to /login, so no one could ever sign in.
  React.useEffect(() => {
    if (!ready) return;
    // #1297: a member denied Edge stays on the loader (the plain loading screen only when the shell global is absent) or the message — no route to guard, and
    // no signed-out snapshot to feed the Suite sign-in decision. pitchiq#224: an explained refusal is
    // the same situation — pushing /login under the message would put the form back on a reload.
    if (accessDeniedNow() || refusalNow()) return;
    // pitchiq#489: nor when the first load failed for a reason that decided nothing about the session — the
    // token is kept, so this visitor is not signed out and must not be sent to /login or to the Suite. pitchiq#399 /
    // pitchiq#511: that includes GET /me's platform outage (nobody is signed in on this page, but nobody was signed OUT).
    if (firstLoadFailedNow() && !authed) return;
    // #675, redirect site 1 of 4 — the only one that is a navigation rather
    // than a render. Before sending anyone to Edge's own /login, consult the
    // ONE decision.
    //
    // This effect re-runs on every [path, authed, ready] change, which is
    // exactly why `decideSuiteRedirectOnce` latches: a sign-out flips `authed`
    // with the rest of the snapshot untouched, and a re-decision on that
    // render is what fired the off-host redirect on the sign-out click in an earlier
    // draft. Here the call is a read of an answer already given.
    if (decideSuiteRedirectOnce(path).redirect) return;
    // /onboarding is NOT open (QA-#001, pitchiq#522): it is an authenticated page. A tab left on it after the
    // shared session ended elsewhere must go to the signed-out page (or the Suite sign-in), never draw Quick setup.
    const open = path === "/login" || path === "/auth/verify";
    if (!authed && !open) { navigate("/login"); return; }
    // An authenticated visitor must not linger on the login / magic-link-verify
    // screens. This is the SSO landing — a fresh hashless arrival defaults to
    // /login, then the shared platform session hydrates (authed flips true) —
    // and also anyone who opens /login with a live token. Route them into the
    // app, mirroring the magic-link path's onboarded check so a not-yet-
    // onboarded user still reaches /onboarding rather than a blank dashboard.
    if (authed && (path === "/login" || path === "/auth/verify")) {
      navigate(MP.isOnboarded() ? "/dashboard" : "/onboarding");
    }
  }, [path, authed, ready]);

  // pitchiq#312 — a page that is ready with NOBODY signed in needs to know where the Suite is: it is
  // LoginPage's only button and the refusal page's only link. The paths above read it one by one
  // (#345, #342, the entry call), and the ones that did not left a page with nothing to click: a load
  // that arrives on `#/login` with no token (`ready` starts true, so the effect body above never runs —
  // what a reload of a refusal used to be, and what a bookmarked `/app#/login` still is), and a
  // sign-out inside a tab that loaded with a token (a hash change, no reload).
  //
  // IT DECIDES NOTHING. Declared after the route guard, whose effect has already latched
  // `decideSuiteRedirectOnce` for this render (G7) before this one's read can resolve; and every path
  // that CAN open that door settles the URL itself before deciding (C6). This only gives an already
  // settled page its link — the same value those paths read, from the same single-flight request.
  React.useEffect(() => {
    if (!ready || authed || suiteConfiguredUrl) return;
    settledSuiteUrl().then((url) => {
      if (!url || suiteConfiguredUrl) return;
      suiteConfiguredUrl = url;
      force();
    });
  }, [ready, authed]);

  // pitchiq#471 — Back into a refusal restored from the bfcache. A member refused in an open tab
  // follows the link to the Suite, fixes their workspace and presses Back: the browser restores THIS
  // page without re-running a single script, so it would go on showing the refusal to a session that
  // is fine again. A restored page that is showing THAT refusal reloads, and the reload asks the door
  // again. Only that one: the other refusals and the #1297 no-access page are left as they were (a
  // reload of the no-access page re-asks a door that leaves for the Suite, so Back would bounce the
  // member straight out again). Only on `persisted` — an ordinary load never reloads itself, so this
  // cannot loop. Same `pageshow` shape as pages-newbrief.jsx and switch-loader-host.jsx.
  React.useEffect(() => {
    const onShow = (e) => {
      const refused = refusalNow();
      if (e.persisted && refused && refused.code === SESSION_HAS_NO_WORKSPACE_REFUSAL_CODE) window.location.reload();
    };
    window.addEventListener("pageshow", onShow);
    return () => window.removeEventListener("pageshow", onShow);
  }, []);

  // #675, redirect sites 2-4 of 4, all at once. `location.replace` does not stop
  // JavaScript — the page keeps rendering until the new document commits — so
  // "we are leaving" has to be a state this render respects, not just a call we
  // made. Returning the loader here (the plain loading screen only when the shell global is absent) structurally dominates all three
  // `return <LoginPage />` sites below (/auth/verify, /login, and the !authed
  // fallback), so none of them can put a login form in front of a visitor we
  // have already decided to send away. /auth/verify is the one that matters
  // most: it renders LoginPage BEFORE the auth check (#83), and an old emailed
  // magic link is exactly how a signed-out visitor arrives there.
  const accessDenied = accessDeniedNow();
  // evolve-suite#1302 (flow 15) — the Suite hands the visitor over behind its Product loader, so this wait is the SAME
  // loader ("Opening Evolve Edge...", gold) instead of a different screen flashing between the two; index.html paints
  // its static copy before this code has even compiled. Without the shell global (its script failed to load) the
  // plain loading screen below still draws rather than a blank page.
  //
  // pitchiq#429 — the SAME element stays mounted while LEAVING Edge (the Suite redirect, a refused member): swapping in the
  // plain "Loading your workspace…" screen there showed a second, old screen for the whole hop out. Its label is the one
  // below either way; whether it should name something else once Edge knows it is leaving is an open design question
  // (Edge does not know the destination workspace's name at that point), and this is the one place to change it.
  const leavingEdge = suiteRedirecting() || (accessDenied && accessDenied.leaving);
  const arrivalLoader = window.EvolveShell && window.EvolveShell.SwitchingLoader && window.SwitchLoader;
  const showsLoader = !ready || leavingEdge || (arrivalLoader && arrivalHeld);
  // evolve-suite#1332 review finding 5 — the workspace marker belongs to the ARRIVAL. Once a real page has been drawn the
  // visitor can switch workspace inside Edge without a reload (the shell's workspace switcher), so a loader drawn later
  // on this page load (leaving Edge) is the one a load with no marker draws, never "Opening <the workspace they arrived
  // with>...". Forgotten in an effect: only once that page has really been drawn.
  React.useEffect(() => {
    if (!showsLoader) window.__mpWorkspaceArrival = null;
  }, [showsLoader]);
  if (showsLoader) {
    if (arrivalLoader) {
      // evolve-suite#1332 — a workspace picked in the Suite landed here: the Suite showed "Opening <workspace>...", the
      // server validated its marker and index.html already painted that workspace's loader, so this is the SAME loader
      // (look and label) until the app is ready, never "Opening Evolve Edge..." after it. The server set
      // `__mpHandoffArrival` with it, so there is no second hold either. Every other load has no marker.
      // ⚠️ NOT quite the same phase throughout: React mounts fresh here (no hydration), and the shell's SwitchingLoader
      // draws its first frame at `startOffsetMs + 300 ms` and only moves onto the page's clock on its next animation
      // frame, so the placeholder look's tile is off by one to two painted frames when React takes over (the photo look
      // re-renders at once). The repair belongs in the shell (evolve-suite#1332 follow-up), not here.
      const workspaceArrival = window.__mpWorkspaceArrival;
      const spec = workspaceArrival
        ? window.SwitchLoader.workspaceArrivalSpec(workspaceArrival)
        : window.SwitchLoader.productSpec(window.EvolveShell, { key: "edge", name: "Evolve Edge" });
      // It never completes (`ready` false): the app replaces it once ready AND the minimum has passed. No live region
      // here: index.html's static one (empty at first, filled after paint) already announced the label once.
      return <window.EvolveShell.SwitchingLoader {...spec} ready={false} />;
    }
    return (
      <div className="mp-onb" style={{ display: "grid", placeItems: "center", minHeight: "100vh" }}>
        <div className="mp-fade" style={{ textAlign: "center", color: "var(--fg-2)" }}>
          <span className="mp-spinner" style={{ display: "inline-block", width: 28, height: 28 }}></span>
          <p style={{ marginTop: 14 }}>Loading your workspace…</p>
        </div>
      </div>
    );
  }

  // #1297: denied and NOT leaving — the plain no-access page, in the loading screen's own frame (no
  // new design). The copy is the server's, so it changes in one place.
  //
  // evolve-suite#1030 review E2: when the Suite App Selector's URL is known (the loop breaker held,
  // api.js `leaveForSuiteAppChooser`), offer it — Coach's label and Edge's own Suite CTA style
  // (pages-auth.jsx). No URL (SUITE_URL unset) → no link, never a dead one.
  if (accessDenied) {
    return (
      <div className="mp-onb" data-access-denied="true" style={{ display: "grid", placeItems: "center", minHeight: "100vh" }}>
        <div className="mp-fade" style={{ textAlign: "center", color: "var(--fg-2)" }}>
          <p>{accessDenied.message}</p>
          {accessDenied.chooseAppUrl && (
            <a className="tm-btn tm-btn--primary" href={accessDenied.chooseAppUrl}>{GO_TO_SUITE_LABEL}</a>
          )}
        </div>
      </div>
    );
  }

  // pitchiq#224: an entry door refused and explained itself — the server's own message, in the same
  // frame as the no-access page above. Placed BEFORE every `return <LoginPage />` below for the same
  // structural reason those are dominated by the loader: a member who is signed in to the
  // Suite must never be shown Edge's "Sign in at Evolve Suite" form because of a server refusal.
  //
  // evolve-suite#1030 review E2: "Enable it in Evolve Suite" with no way there was the PM's dev
  // finding. So the refusal links to the Suite's Apps page (final review Ed-a), with the same label
  // and style as the no-access page above.
  //
  // pitchiq#312: EVERY refusal, whatever its code. Ed-b had kept the link to the not-enabled refusal
  // alone, on the reasoning that the Apps page fixes nothing for the others — which left a member
  // with no workspace on their session, a refused workspace or a mismatched identity on one grey
  // sentence and nothing to click (pitchiq#471's removed member is the first of those). The link is
  // the way OUT, not the fix. No Suite URL → no link, never a dead one.
  const refusal = refusalNow();
  if (refusal) {
    const suiteAppsHref = suiteConfiguredUrl
      ? String(suiteConfiguredUrl).replace(/\/+$/, "") + SUITE_APPS_PATH
      : null;
    return (
      <div className="mp-onb" data-entry-refused="true" style={{ display: "grid", placeItems: "center", minHeight: "100vh" }}>
        <div className="mp-fade" style={{ textAlign: "center", color: "var(--fg-2)" }}>
          <p>{refusal.message}</p>
          {suiteAppsHref && (
            <a className="tm-btn tm-btn--primary" href={suiteAppsHref}>{GO_TO_SUITE_LABEL}</a>
          )}
        </div>
      </div>
    );
  }

  // pitchiq#489 — the first load failed for a reason that decided nothing about the session (store.js
  // `MP.firstLoadFailed`): the Detailed Error Slot (spec DwabVFr7XU rev 8 L14, L24) where the content would be, in the
  // refusal page's frame, and BEFORE every `return <LoginPage />` below — this member is still signed in. "Try again"
  // re-runs hydrate and RETURNS its promise, so the slot stays busy until it settles (L30); on success the member is
  // authed and the app renders, on failure hydrate sets the flag again and this same slot stays (L32). Without the shell
  // script the same frame draws the message and a button, never a blank page.
  if (firstLoadFailedNow() && !authed) {
    const retry = () => MP.hydrate();
    const ErrorSlot = window.EvolveShell && window.EvolveShell.ErrorSlot;
    return (
      <div className="mp-onb" data-first-load-failed="true" style={{ display: "grid", placeItems: "center", minHeight: "100vh" }}>
        {ErrorSlot ? (
          <ErrorSlot variant="detailed" onRetry={retry} />
        ) : (
          <div className="mp-fade" style={{ textAlign: "center", color: "var(--fg-2)" }}>
            <p>{FIRST_LOAD_FAILED_TEXT}</p>
            <button type="button" className="tm-btn tm-btn--primary" onClick={retry}>{FIRST_LOAD_RETRY_LABEL}</button>
          </div>
        )}
      </div>
    );
  }

  // login
  //
  // /auth/verify is matched BEFORE the `!authed` bail-out below: the whole job
  // of this route is to turn an unauthenticated visitor into an authenticated
  // one, so it cannot sit behind an auth check.
  //
  // #83: VerifyPage was deleted with the magic-link flow, but this route
  // outlived it and threw ReferenceError on every hit, killing the whole SPA
  // render. Old emailed links and bookmarks still point here, so send them to
  // the login screen instead of 404ing or crashing.
  // #120 — `suiteUrl` is passed, NOT re-read inside the page. `suiteConfiguredUrl` is the exact
  // value `suite-redirect.js` used to decide this visitor stays, and a second read of the same
  // fact could disagree with the decision that produced this render — offering a Suite link to
  // someone who is here BECAUSE `!suiteUrl` was one of the eight `stay()` conditions. Passing it
  // makes the two the same read by construction.
  if (m.is("/auth/verify")) return <LoginPage suiteUrl={suiteConfiguredUrl} />;
  if (m.is("/login") && !authed) return <LoginPage suiteUrl={suiteConfiguredUrl} />;
  // pitchiq#208 — Quick Setup had zero navigation chrome (no Shell at all): the old spec
  // revision PROHIBITED it, superseded by Eli's 2026-09-24 ruling (ELI-045), which the current
  // Edge Onboarding + Edge Navigation specs now require. OnboardingShell (nav.jsx) is not
  // <Shell> — see its own comment for why the dashboard's padded/max-width content frame is the
  // wrong composition for onboarding's own full-height layout.
  // Only for a signed-in person (QA-#001): a signed-out visitor falls through to LoginPage below.
  if (authed && m.is("/onboarding")) return <OnboardingShell path={path}><OnboardingPage /></OnboardingShell>;

  if (!authed) return <LoginPage suiteUrl={suiteConfiguredUrl} />;

  if (m.is("/dashboard")) return <DashboardPage path={path} />;
  if (m.is("/prepare/new")) return <PrepareNewPage path={path} />;
  if (m.is("/briefs")) return <BriefsPage path={path} />;
  let pm = m.is("/prepare/:id");
  if (pm) return <PrepViewPage id={pm.id} path={path} />;
  let cm = m.is("/post-call/:id");
  if (cm) return <PostCallPage id={cm.id} path={path} />;
  if (m.is("/settings")) return <SettingsPage path={path} />;
  if (m.is("/upgrade")) return <UpgradePage path={path} />;

  // default
  return <DashboardPage path={path} />;
}

Object.assign(window, { App });
const root = ReactDOM.createRoot(document.getElementById("root"));
// evolve-suite#1302 G1 — the Switching Loader host lives HERE, at the root, gated only on the shell global: nothing under
// App (a shell-context reset, a page change) can unmount it in the middle of a switch. See switch-loader-host.jsx.
const switchLoaderHost = window.EvolveShell && window.EvolveShell.useSwitchingLoader && window.SwitchLoaderHost;
root.render(<ToastProvider><App />{switchLoaderHost && <window.SwitchLoaderHost />}<Tweaks /></ToastProvider>);
