import { createApp, defineAsyncComponent, defineComponent, h, reactive } from 'vue';
import { createPinia } from 'pinia';
import {
  browserTracingIntegration,
  init as SentryInit,
  setTag as sentrySetTag,
  setUser as sentrySetUser,
} from '@sentry/vue';
import dayjs from 'dayjs';
import i18next from 'i18next';
import i18nFetchBackend from 'i18next-fetch-backend';
import I18NextVue from 'i18next-vue';
import { setupAndSyncLTIContext } from 'lti/initiate';
import { getQueryClient } from 'network-layer/plugin/getQueryClient';
import { initializeCSR } from 'network-layer/plugin/initializerCSR';
import SupportedLocaleForDayJS from 'shared-domains/config/DayjsConfig';
import { isEnvironmentProd } from 'shared-library/utils/detectEnvironment';
import { mark, measure } from 'shared-library/utils/performanceMarks';
import AxiosPlugin, { updateURLs } from 'shared-services/Axios/AxiosPlugin';
import $axios from 'shared-services/Axios/AxiosService';
import { initConsentDefaults } from 'shared-services/CookieConsentService';
import EventBusPlugin from 'shared-services/EventBus/EventBusPlugin';
import { isLangCodePTBR, syncDocumentDirection } from 'shared-services/LocaleUtils';
import { getLTIContext } from 'shared-services/LtiCommonUtils';
import RenderService from 'shared-services/RenderService';
import useRootStore from 'shared-services/rootStore';
import QRouterPlugin from 'shared-services/Router/QRouterPlugin';
import { useServerDataStore } from 'shared-services/serverData';
import { flushAtIdle, startScheduler } from 'shared-services/DeferredTaskScheduler';
import useCurrentUserStore from 'shared-services/User/currentUser';
import UserActionsService from 'shared-services/User/UserActionsService';
import UserPlugin from 'shared-services/User/UserPlugin';
import useUserStore from 'shared-services/User/userStore';
import UtilitiesPlugin from 'shared-services/UtilitiesPlugin';
import clickOutsidePlugin from 'sphagetti/plugins/v-click-outside';
import getCurrentDeviceProperties from 'utilities/DeviceConfigUtils';
import {
  isDev,
  isE2ETest,
  isJenkinsTest,
  isLocal,
  isSentryEnabled,
  setGoogleApiKey,
} from 'utilities/EnvUtils';
import { substituteLocalizedKeyword } from 'utilities/i18nUtils';
import sentryConfig from 'utilities/sentryConfig';
import App from './App.vue';
import { resolveCurrentUser, seedCurrentUserQueryCache } from './earlyCurrentUser';

// services
import getRouter from './router';
import { warmFiltersChunk, warmReportsRouteChunk } from './utils/warmReportsChunks';

import type { ICommonStoreProps, TPlatform } from 'analytics-client/types';
import type { IMetaData } from 'shared-domains/types/metaData';
import type UserDomain from 'shared-services/User/UserService';

// deferred css
import('./style.css');
import('./assets/css/tailwind.css');

import ('shared-icons/join-icons.css');
import ('shared-icons/math-kit-icons.css');



const FA = defineAsyncComponent(() => import('shared-components/FA.vue'));
const VImage = defineAsyncComponent(() => import('sphagetti/components/VImage.vue'));

document.title = 'Wayground';

mark('admin:bootstrap:start');

// core ships prebuilt, so it cannot read this app's env at its own build time.
// Register the key before anything Google Drive/Picker/Slides runs.
const googleApiKey = import.meta.env.VITE_GOOGLE_API_KEY ?? '';
setGoogleApiKey(googleApiKey);

const app = createApp(App);
// Same key, for core's MediaVideo/MediaVideoPreview.
app.provide('googleApiKey', googleApiKey);

function getFontAwesomeClass(icon: unknown) {
  if (Array.isArray(icon)) {
    return `${icon[0]} fa-${icon[1]}`;
  }
  if (typeof icon === 'string') {
    return icon;
  }
  if (icon && typeof icon === 'object' && 'prefix' in icon && 'iconName' in icon) {
    const { prefix, iconName } = icon as { prefix: string; iconName: string };
    return `${prefix} fa-${iconName}`;
  }
  return '';
}

// setup pinia store
const piniaStore = createPinia();
app.use(piniaStore);

// setup router
const router = getRouter(app);
window.hack_router = router || window.hack_router;

if (isSentryEnabled()) {
  SentryInit({
    app,
    dsn: 'https://575cba3d393ef5368c3939eb47190fcb@o984659.ingest.us.sentry.io/4511297922334720',
    tracesSampleRate: 0.05,
    sampleRate: 1,
    ignoreErrors: sentryConfig.ignoredErrors,
    beforeBreadcrumb: sentryConfig.beforeBreadcrumb,
    beforeSend(event) {
      const exception = event.exception?.values?.[0];
      const msg = exception?.value || '';
      const frames = exception?.stacktrace?.frames || [];
      const hasVueDirectiveFrame = frames.some((f) => f.function === 'invokeDirectiveHook');
      const hasVueUnmountFrame = frames.some((f) => f.function === 'unmountComponent');

      // FE-SESSION-B: Vue 3.5.13 directive-patching crash — only suppress when
      // invokeDirectiveHook is in the stack (not legitimate ref.value errors).
      if (
        (msg.includes("reading 'value'") || msg.includes('.value\'') || msg.includes('.value"')) &&
        hasVueDirectiveFrame
      ) {
        return null;
      }

      // FE-SESSION-F: Safari wording for Vue 3.5.13 unmountComponent crash
      // (destructuring null instance). Only suppress when unmountComponent is in the stack.
      if (
        msg.includes('Right side of assignment cannot be destructured') &&
        hasVueUnmountFrame
      ) {
        return null;
      }

      return event;
    },
    maxBreadcrumbs: 30,
    integrations: [
      browserTracingIntegration({
        router,
        enableHTTPTimings: false,
      }),
    ],
    denyUrls: sentryConfig.denyUrls,
  });
}

// setupDebuggers(app);

i18next.use(i18nFetchBackend);
// Mirror the loaded locale onto <html>/<body> lang + dir (RTL for Arabic). Registered before
// init so the initial load and every later changeLanguage() are covered.
syncDocumentDirection(i18next);

app.use(I18NextVue, { i18next });

app.use(EventBusPlugin);
app.use(AxiosPlugin);
app.use(UtilitiesPlugin);

app.component(
  'InjectedFontAwesomeIcon',
  defineComponent({
    props: {
      icon: {
        type: [Array, Object, String],
        required: true,
      },
    },
    setup(props) {
      return () => h('i', { class: getFontAwesomeClass(props.icon) });
    },
  }),
);
// Stable, reactive target for the FontAwesome SVG definitions.
//
// `registerFA` runs in an idle job *after* `app.mount()` (see the fontawesome flushAtIdle
// below). `app.provide()` only assigns into `appContext.provides` — it does not reach
// component instances that already ran `setup()`. quizizz-ui's `Icon.vue` does a one-shot
// `inject('InjectedFontAwesomeDefinitions')` into a plain const, so every Icon created
// before the idle flush would hold the empty placeholder object forever and permanently
// fall through to the CSS-class `<i>` branch.
//
// Providing one object up front and having `registerFA` mutate it in place fixes both
// halves: the reference stays valid, and `reactive` makes Icon's `hasInjectedDefinition`
// computed re-evaluate once the definitions land.
const faDefinitions = reactive<Record<'far' | 'fas' | 'fab', Record<string, unknown>>>({
  far: {},
  fas: {},
  fab: {},
});
app.provide('InjectedFontAwesomeDefinitions', faDefinitions);

app.component('FA', FA);
app.component('VImage', VImage);
// ─── Google Consent Mode v2 ──────────────────────────────────────────────────
// initConsentDefaults() MUST be called before installGTag / installGTM.
// It pushes 'consent default' commands to window.dataLayer so that GTM honours
// the user's stored preferences from the first moment any tag fires.
//
// How third-party tags are controlled (all via GTM consent triggers):
//
//   • Meta Pixel
//       GTM trigger condition: ad_storage = granted
//       When the user rejects non-essential cookies, ad_storage is set to 'denied'
//       and GTM suppresses the Meta Pixel tag automatically.
//
//   • LinkedIn Insight Tag
//       GTM trigger condition: ad_storage = granted
//       Same as Meta Pixel — suppressed by GTM when ad_storage = 'denied'.
//
//   • Google Ads (_gcl_au, _gcl_aw — Conversion Linker)
//       GTM trigger conditions: ad_storage = granted AND ad_user_data = granted
//       The Conversion Linker tag is suppressed when either of these is 'denied',
//       preventing first-party ad cookies (_gcl_*) from being written.
//
//   • GA4 Analytics
//       GTM trigger condition: analytics_storage = granted
//       (Note: GA4 analytics consent scope is not controlled by the cookie banner
//       in this PR — analytics_storage remains 'granted' regardless of banner choice.
//       GA4 consent management will be addressed in a follow-up.)
//
//   • Braze SDK
//       Controlled directly in code via BrazePlugin (not GTM) — gated on the
//       `marketing` consent flag read from CookieConsentService.readConsentState().
//
// GTM setup required (one-time, performed by the GTM admin — not this PR):
//   - Meta Pixel tag: add trigger condition  → ad_storage = granted
//   - LinkedIn Insight Tag: add trigger condition → ad_storage = granted
//   - Google Ads Conversion Linker: add trigger conditions → ad_storage = granted
//                                                            ad_user_data = granted
//   - GA4 config tag: add trigger condition → analytics_storage = granted
// ─────────────────────────────────────────────────────────────────────────────
initConsentDefaults();
// Add VueQueryPlugin to the app and pass in the queryClient
initializeCSR(app);

// floating-vue (global `v-tooltip` directive) is installed synchronously before
// app.mount() inside bootstrap(), not lazily here — see the comment there.

app.directive('click-outside', clickOutsidePlugin);
/**
 * Single source of truth for locale resolution.
 * Avoids repeated getCurrentLanguageToUse + isLangCodePTBR derivation scattered across boot.
 */
async function resolveLocaleContext(
  getCurrentLanguageToUse: (userLang?: string) => Promise<string>,
  userLang?: string,
  requestCountry?: string,
) {
  let locale = await getCurrentLanguageToUse(userLang);

  // Taiwan override when user has no lang preference
  if (!userLang && requestCountry === 'TW') {
    locale = window.location.href.includes('lang=en') ? 'en' : 'zh-TW';
  }

  const i18nextLocale = isLangCodePTBR(locale) ? 'pt-BR' : locale;
  return { locale, i18nextLocale };
}

function loadDayJSLocale(
  language:
    | 'es'
    | 'en'
    | 'fr'
    | 'pl'
    | 'ru'
    | 'id'
    | 'ms'
    | 'tl'
    | 'de'
    | 'vi'
    | 'th'
    | 'az'
    | 'pt-br'
    | 'zh-CN'
    | 'zh-TW'
    | 'tr'
    | 'it'
    | 'ja'
    | 'kk',
) {
  if (SupportedLocaleForDayJS[language]) {
    SupportedLocaleForDayJS[language]().then(() => dayjs.locale(language));
  } else {
    dayjs.locale('en');
  }
}

async function initI18n(initialLocale: string) {
  const defaultLocale = 'en';
  let locale = initialLocale;
  let localeForI18next = isLangCodePTBR(locale) ? 'pt-BR' : locale;

  i18next.use({
    type: 'postProcessor',
    name: 'LocalizedKeywordSubstitution',
    process: substituteLocalizedKeyword,
  });

  // Fast path for English — skip dynamic import of LocaleConstants and remote fetch
  if (locale === defaultLocale) {
    await i18next.init({
      lng: defaultLocale,
      debug: false,
      interpolation: { prefix: '{$', suffix: '}' },
      fallbackLng: defaultLocale,
      resources: { [defaultLocale]: { translation: {} } },
      partialBundledLanguages: true,
      backend: {
        loadPath: `${import.meta.env.VITE_TRANSLATION_SOURCE_FILE_URL}/{$lng}.json`,
        crossDomain: true,
      },
      postProcess: ['pseudo', 'LocalizedKeywordSubstitution'],
      nsSeparator: ':Q:',
    });
    dayjs.locale('en');
    useRootStore().setTranslations({});
    useRootStore().setEnglishMap({});
    return;
  }

  const localeConstantsModule = await import('shared-domains/utils/LocaleConstants');
  const supportedLngs = localeConstantsModule.TranslatedLanguages.map((lang) => {
    if (isLangCodePTBR(lang.code)) {
      return 'pt-BR';
    }
    return lang.code;
  });

  // If already initialized (English fast-path ran), use changeLanguage instead of init
  if (i18next.isInitialized) {
    i18next.options.supportedLngs = supportedLngs;

    let loaded = false;
    try {
      await i18next.loadLanguages(localeForI18next);
      loaded = i18next.hasResourceBundle(localeForI18next, 'translation');
    } catch (error) {
      // oxlint-disable-next-line no-console
      console.error('Failed to load translation', error);
    }

    if (!loaded) {
      try {
        await i18next.loadLanguages(localeForI18next);
        loaded = i18next.hasResourceBundle(localeForI18next, 'translation');
      } catch {
        // silent — fallback below
      }
    }

    if (loaded) {
      await i18next.changeLanguage(localeForI18next);
    } else {
      // oxlint-disable-next-line no-console
      console.error(`Translation load failed for ${localeForI18next}, falling back to English`);
      locale = defaultLocale;
      localeForI18next = defaultLocale;
    }
  } else {
    await i18next.init({
      lng: localeForI18next,
      debug: false,
      interpolation: {
        prefix: '{$',
        suffix: '}',
      },
      supportedLngs,
      fallbackLng: defaultLocale,
      resources: { [defaultLocale]: { translation: {} } },
      partialBundledLanguages: true,
      backend: {
        loadPath: `${import.meta.env.VITE_TRANSLATION_SOURCE_FILE_URL}/{$lng}.json`,
        crossDomain: true,
      },
      postProcess: ['pseudo', 'LocalizedKeywordSubstitution'],
      nsSeparator: ':Q:',
    });

    if (!i18next.hasResourceBundle(localeForI18next, 'translation')) {
      let loaded = false;

      try {
        await i18next.loadLanguages(localeForI18next);
        loaded = i18next.hasResourceBundle(localeForI18next, 'translation');
      } catch (error) {
        // oxlint-disable-next-line no-console
        console.error('Failed to load translation', error);
      }

      if (!loaded) {
        try {
          await i18next.loadLanguages(localeForI18next);
          loaded = i18next.hasResourceBundle(localeForI18next, 'translation');
        } catch {
          // silent — fallback below
        }
      }

      if (!loaded) {
        // oxlint-disable-next-line no-console
        console.error(`Translation load failed for ${localeForI18next}, falling back to English`);
        await i18next.changeLanguage(defaultLocale);
        locale = defaultLocale;
        localeForI18next = defaultLocale;
      }
    }
  }

  loadDayJSLocale(locale as Parameters<typeof loadDayJSLocale>[0]);

  let translationMap;
  let englishMap: Record<string, any> = {};
  try {
    translationMap = await import(`../translations/${locale}.json`);
    if (!translationMap) {
      translationMap = await import(`../translations/${defaultLocale}.json`);
    }
    englishMap = await import(`../translations/${defaultLocale}.json`);
  } catch {
    translationMap = await import(`../translations/${defaultLocale}.json`);
  }
  useRootStore().setTranslations(translationMap?.default ?? {});
  useRootStore().setEnglishMap(englishMap.default ?? {});
}

function removeInitialLoader() {
  const spinner: HTMLElement | null = document.querySelector('#initial-loader-container');

  if (spinner) {
    spinner.style.display = 'none';
    spinner.remove();
  }
}

function setMetaData(metaData?: Partial<IMetaData>) {
  (window as any).metaData = {
    ...metaData,
    googleClientId: isEnvironmentProd()
      ? import.meta.env.VITE_GOOGLE_CLIENT_ID_PROD
      : import.meta.env.VITE_GOOGLE_CLIENT_ID_DEV,
    //
    stripePublishableKey: isEnvironmentProd()
      ? import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY_PROD
      : import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY_DEV,
  };
}

async function hydrateUser() {
  mark('admin:hydrate-user:start');
  const userStore = useUserStore();
  const serverDataStore = useServerDataStore();
  try {
    serverDataStore.localClientInit();
    useRootStore().setDeviceInfoFromClient();
    updateURLs();

    // Fire-and-forget: GA cookies setup is non-critical and should not block mount
    UserActionsService.setGACookies().catch(() => {});

    // Fetch user via raw axios (preserves fetchAllTags=true from authMain endpoint)
    const [user, geoData] = await Promise.all([
      resolveCurrentUser(() => UserActionsService.getCurrentUser($axios)),
      RenderService.getGeoData(),
    ]);

    // Seed Tanstack Query cache so downstream composables calling
    // `services/users/getCurrentUser({}).query()` reuse this data instead of
    // firing duplicate `/_api/schoolMain/user` requests.
    seedCurrentUserQueryCache(user, getQueryClient());

    if (user) {
      // core's `setUser` takes only the user; the old second `_isInitialLoad` arg was
      // already unused. core's store also drops `initializeThirdPartyProperties`, so the
      // third-party fan-out is called explicitly in the idle job below.
      userStore.setUser(user);
      useCurrentUserStore().setInitialUserTags(user.tags ? user.tags : []);
    }

    if (geoData && Object.keys(geoData).length > 0) {
      serverDataStore.setRequestCountry(geoData.requestCountry ?? '');
      serverDataStore.setRegion(geoData.region ?? '');
    }

    setMetaData({
      ...geoData,
      user,
    });

    serverDataStore.setCredentials({
      googleClientId: (window as any).metaData?.googleClientId || '',
      stripePublishableKey: (window as any).metaData?.stripePublishableKey || '',
    });

    // core's UserPlugin also attaches Sentry user/tag context when given these two
    // callbacks — the workspace copy it replaces only set `$user`, so this is new behaviour
    // core brings, wired up rather than stubbed out.
    app.use(UserPlugin, { user, setUser: sentrySetUser, setTag: sentrySetTag });

    return { user, geoData, userStore, serverDataStore };
  } finally {
    mark('admin:hydrate-user:end');
    measure('admin:hydrate-user:measure', 'admin:hydrate-user:start', 'admin:hydrate-user:end');
  }
}

async function initialLoad(
  { user, userStore, serverDataStore }: Awaited<ReturnType<typeof hydrateUser>>,
  getCurrentLanguageToUse: (userLang?: string) => Promise<string>,
) {
  mark('admin:initial-load:start');
  try {
    const { locale: correctedLocale, i18nextLocale: correctedLocaleForI18next } =
      await resolveLocaleContext(
        getCurrentLanguageToUse,
        user?.preferences?.lang,
        serverDataStore.requestCountry,
      );

    import('shared-services/AnalyticsV2').then(
      async ({ AnalyticsV2, attachCommonStoreUpdateListener }) => {
        attachCommonStoreUpdateListener();
        const { default: detectEnvironment } = await import('shared-library/utils/detectEnvironment');
        const commonProps = {
          environment: detectEnvironment(),
          screenType: getCurrentDeviceProperties({}).type,
          locale: correctedLocale,
          country: serverDataStore.requestCountry,
          platform: 'web' as TPlatform,
          featureFlags: {},
          userId: (userStore.user as UserDomain)?.id,
          subPlanId:
            userStore.userService?.subscription?.planId || userStore.userService?.subscription?.id,
          region: serverDataStore.region,
          appName: 'session',
        };
        const ltiContext = getLTIContext();
        if (ltiContext) {
          commonProps.screenType = `lti-${ltiContext.platform}`;
        }
        AnalyticsV2.init({ commonStoreProps: commonProps as ICommonStoreProps });
      },
    );

    if (correctedLocaleForI18next !== i18next.language) {
      await i18next.changeLanguage(correctedLocaleForI18next);
      loadDayJSLocale(correctedLocale as Parameters<typeof loadDayJSLocale>[0]);
    }

    flushAtIdle(
      async () => {
        const { default: BrazePlugin } = await import('sphagetti/plugins/BrazePlugin');
        app.use(BrazePlugin, { user });
      },
      { name: 'braze-plugin' },
    );

    if (
      import.meta.env.MODE !== 'production' &&
      (isE2ETest() || isJenkinsTest() || isLocal() || isDev())
    ) {
      await import('./assets/css/fontawesome.all.min.css');
    }
  } finally {
    mark('admin:initial-load:end');
    measure('admin:initial-load:measure', 'admin:initial-load:start', 'admin:initial-load:end');
  }
}

async function bootstrap() {
  const appDOM = document.querySelector('#app') as HTMLDivElement;

  // Warm the reports route chunk and filters while bootstrap is in flight.
  // These fire-and-forget so chunks download in parallel with user hydration.
  warmReportsRouteChunk();
  warmFiltersChunk();

  // Parallelize: i18n init (with optimistic 'en' locale) runs alongside user hydration
  // instead of waiting for hydration to complete first.
  const i18nPromise = (async () => {
    mark('admin:i18n:start');
    try {
      await initI18n('en');
    } finally {
      mark('admin:i18n:end');
      measure('admin:i18n:measure', 'admin:i18n:start', 'admin:i18n:end');
    }
  })();

  const [{ getCurrentLanguageToUse }, hydrated, { default: FloatingVuePlugin }] = await Promise.all([
    import('shared-services/LocaleUtils'),
    hydrateUser(),
    // Downloaded in parallel here but installed synchronously before mount (see
    // below) so the global `tooltip` directive is resolved on the very first
    // render.
    import('floating-vue'),
    i18nPromise,
  ]);

  // If user's actual locale differs from optimistic 'en', correct it now
  const { locale: userLocale, i18nextLocale: userI18nextLocale } = await resolveLocaleContext(
    getCurrentLanguageToUse,
    hydrated.user?.preferences?.lang,
    hydrated.serverDataStore.requestCountry,
  );
  if (userI18nextLocale !== 'en') {
    mark('admin:i18n-correction:start');
    await initI18n(userLocale);
    mark('admin:i18n-correction:end');
    measure('admin:i18n-correction:measure', 'admin:i18n-correction:start', 'admin:i18n-correction:end');
  }

  // The IndexedDB feature-flag warm-up that used to run here is gone: `hydrateFromCache` was
  // added to the workspace FeatureFlagsService locally and does not exist in
  // @wayground/core's copy. `router.ts` still calls `updateFeatureFlags(userId)` on route
  // resolution, so the flags are populated either way — this is a first-paint latency
  // regression only. Restore it by upstreaming the cache warm-up to core rather than
  // re-forking the service here.

  // ┌─ mount with user + translations ready ──────────────────────────────────┐
  app.use(QRouterPlugin(router));

  // Install floating-vue (global `v-tooltip` directive) BEFORE mount. Installing
  // it lazily on idle caused a Vue 3.5.13 directive-patching crash (FE-SESSION-B):
  // the first render happened with `tooltip` unresolved, then once the directive
  // registered a later patch had more dirs than the previous vnode, so
  // invokeDirectiveHook read `oldBindings[i].value` on undefined. Resolving the
  // directive from the first render eliminates that mismatch.
  app.use(FloatingVuePlugin, {
    themes: {
      'tooltip': { delay: { show: 300, hide: 0 } },
      'error-tooltip-sm': { triggers: ['hover'] },
      'error-tooltip': { triggers: ['hover'] },
      'info-tooltip': { triggers: ['hover'], delay: { show: 300, hide: 0 } },
      'mystic-peak-tooltip': { triggers: ['hover'] },
      'info-popper': { triggers: ['hover'], delay: { show: 300, hide: 0 } },
      'warn-popper': { triggers: ['hover'] },
      'danger-popper': { triggers: ['hover'] },
    },
  });

  const resolvedInitial = router.resolve({
    path: window.location.pathname.replace(/^\/session/, '') || '/',
    query: Object.fromEntries(new URLSearchParams(window.location.search)),
    hash: window.location.hash,
  });
  app.provide('initialLayoutName', resolvedInitial.meta?.layout || 'blank');

  mark('admin:mount:start');
  app.mount(appDOM);
  mark('admin:mount:end');
  measure('admin:mount:measure', 'admin:mount:start', 'admin:mount:end');

  // ┌─ deferred task scheduler ────────────────────────────────────────────────┐
  // core's DeferredTaskScheduler has a single idle batch rather than the four ordered
  // tiers (route/idle/late/background) the workspace ThirdPartyScriptScheduler had, so the
  // three per-tier wait budgets collapse into one. 6s is the old `idleMaxWaitMs`, i.e. the
  // budget that gated the earliest jobs — nothing flushes later than it used to.
  startScheduler(router.isReady(), { maxWaitMs: 6_000, quietMs: 2_000 });

  // FontAwesome SVG JS: defer to idle — icon fonts are already preloaded via CSS in index.html.
  // Goes through the scheduler rather than `requestIdleCallback`: index.html boots this file
  // directly, so the polyfill in the unused `main.ts` never runs and Safari < 17.4 has no
  // global to call. The scheduler is setTimeout-based and needs no polyfill.
  flushAtIdle(
    async () => {
      const { default: registerFA } = await import('./fontAwesomeUtils');
      registerFA(faDefinitions);
    },
    { name: 'fontawesome' },
  );

  flushAtIdle(
    () => {
      import('./assets/css/katex.css');
      import('./assets/css/global.css');
      import('./assets/scss/tooltip-themes.scss');
      import('floating-vue/dist/style.css');
    },
    { name: 'deferred-styles' },
  );

  flushAtIdle(
    async () => {
      const { installGTag, installGTM } = await import('shared-services/gtag');
      // core's installers take (app, router) and load their scripts eagerly — the
      // `deferScriptLoad`/`defer` options the workspace package had are gone. Both calls
      // already run inside this idle batch, so the tags still land after first paint.
      installGTag(app, router);
      installGTM(app);
    },
    { name: 'gtm-gtag' },
  );

  flushAtIdle(
    async () => {
      // core's user store has no `initializeThirdPartyProperties`, so the fan-out is called
      // directly. Two deliberate differences from the workspace copy it replaces:
      //   - `skipFeatureFlags: true` — the old fan-out never refreshed feature flags, and
      //     `$featureFlags` is already hydrated above; passing false would add a fetch.
      //   - Braze is not loaded here. The old version did (that was the only use of its
      //     `nonce` argument); core leaves it to the consent-gated `BrazePlugin`, which this
      //     app installs in its own idle job above, so it is still initialised exactly once.
      const user = hydrated.userStore.user;
      // core's user type declares `id` but not `_id`; the `_id` fallback is kept because the
      // hydrated payload can still carry it, matching the guard this replaces.
      const userId = user?.id ?? (user as Record<string, unknown> | undefined)?.['_id'];
      if (userId) {
        const { setUserPropertiesThirdParty } = await import('shared-services/ThirdPartyServices');
        await setUserPropertiesThirdParty(user, true);
      }
    },
    { name: 'third-party-user-properties' },
  );

  flushAtIdle(
    async () => {
      const { default: setupBirdie } = await import('./BirdiePlugin');
      setupBirdie();
    },
    { name: 'birdie' },
  );

  flushAtIdle(
    async () => {
      const { generateSurvey } = await import('sphagetti/services/Survicate');
      generateSurvey();
    },
    { name: 'survicate' },
  );

  mark('admin:router-ready:start');
  router.isReady().then(async () => {
    mark('admin:router-ready:end');
    measure('admin:router-ready:measure', 'admin:router-ready:start', 'admin:router-ready:end');
    setTimeout(removeInitialLoader);
    initialLoad(hydrated, getCurrentLanguageToUse);
    if (getLTIContext()) {
      await setupAndSyncLTIContext(app);
    }
  });
}

bootstrap();
