import 'symbol-observable';
import 'regenerator-runtime/runtime';

import 'whatwg-fetch'; // fetch polyfill needed for PhantomJs rendering
import 'file-saver';
import 'jquery';

import { createElement } from 'react';
import { createRoot } from 'react-dom/client';

import {
  locationUtil,
  monacoLanguageRegistry,
  setLocale,
  setTimeZoneResolver,
  setWeekStart,
  standardEditorsRegistry,
  standardFieldConfigEditorRegistry,
  standardTransformersRegistry,
} from '@grafana/data';
import { DEFAULT_LANGUAGE } from '@grafana/i18n';
import { initializeI18n, loadNamespacedResources } from '@grafana/i18n/internal';
import {
  locationService,
  setBackendSrv,
  setDataSourceSrv,
  setLocationSrv,
  setQueryRunnerFactory,
  setRunRequest,
  setPluginImportUtils,
  setEmbeddedDashboard,
  setAppEvents,
  setReturnToPreviousHook,
  setPluginComponentHook,
  setPluginComponentsHook,
  setCurrentUser,
  setChromeHeaderHeightHook,
  setPluginLinksHook,
  setHelpNavItemHook,
  setFolderPicker,
  setCorrelationsService,
  setPluginFunctionsHook,
  setMegaMenuOpenHook,
} from '@grafana/runtime';
import {
  initOpenFeature,
  setGetObservablePluginComponents,
  setGetObservablePluginLinks,
  setPanelDataErrorView,
  setPanelRenderer,
  setPluginPage,
} from '@grafana/runtime/internal';
import { loadResources as loadScenesResources, sceneUtils } from '@grafana/scenes';
import config, { updateConfig } from 'app/core/config';
import { getStandardTransformers } from 'app/features/transformers/standardTransformers';

import getDefaultMonacoLanguages from '../lib/monaco-languages';

import { AppWrapper } from './AppWrapper';
import { appEvents } from './core/app_events';
import { AppChromeService } from './core/components/AppChrome/AppChromeService';
import { useChromeHeaderHeight } from './core/components/AppChrome/TopBar/useChromeHeaderHeight';
import { useHelpNode } from './core/components/AppChrome/TopBar/useHelpNode';
import { LazyFolderPicker } from './core/components/NestedFolderPicker/LazyFolderPicker';
import { getAllOptionEditors, getAllStandardFieldConfigs } from './core/components/OptionsUI/registry';
import { PluginPage } from './core/components/Page/PluginPage';
import {
  GrafanaContextType,
  useMegaMenuOpenInternal,
  useReturnToPreviousInternal,
} from './core/context/GrafanaContext';
import { initializeCrashDetection } from './core/crash';
import { NAMESPACES, GRAFANA_NAMESPACE } from './core/internationalization/constants';
import { loadTranslations } from './core/internationalization/loadTranslations';
import { postInitTasks, preInitTasks } from './core/lifecycle-hooks';
import { setMonacoEnv } from './core/monacoEnv';
import { interceptLinkClicks } from './core/navigation/patch/interceptLinkClicks';
import { CorrelationsService } from './core/services/CorrelationsService';
import { NewFrontendAssetsChecker } from './core/services/NewFrontendAssetsChecker';
import { backendSrv } from './core/services/backend_srv';
import { contextSrv, RedirectToUrlKey } from './core/services/context_srv';
import { initEchoSrv } from './core/services/echo/init';
import { KeybindingSrv } from './core/services/keybindingSrv';
import { startMeasure, stopMeasure } from './core/utils/metrics';
import { initAlerting } from './features/alerting/unified/initAlerting';
import { getTimeSrv } from './features/dashboard/services/TimeSrv';
import { EmbeddedDashboardLazy } from './features/dashboard-scene/embedding/EmbeddedDashboardLazy';
import { DashboardLevelTimeMacro } from './features/dashboard-scene/scene/DashboardLevelTimeMacro';
import { initGrafanaLive } from './features/live';
import { PanelDataErrorView } from './features/panel/components/PanelDataErrorView';
import { PanelRenderer } from './features/panel/components/PanelRenderer';
import { DatasourceSrv } from './features/plugins/datasource_srv';
import {
  getObservablePluginComponents,
  getObservablePluginLinks,
} from './features/plugins/extensions/getPluginExtensions';
import { getPluginExtensionRegistries } from './features/plugins/extensions/registry/setup';
import { usePluginComponent } from './features/plugins/extensions/usePluginComponent';
import { usePluginComponents } from './features/plugins/extensions/usePluginComponents';
import { usePluginFunctions } from './features/plugins/extensions/usePluginFunctions';
import { usePluginLinks } from './features/plugins/extensions/usePluginLinks';
import { getAppPluginsToPreload } from './features/plugins/extensions/utils';
import { importPanelPlugin, syncGetPanelPlugin } from './features/plugins/importPanelPlugin';
import { initSystemJSHooks } from './features/plugins/loader/systemjsHooks';
import { preloadPlugins } from './features/plugins/pluginPreloader';
import { QueryRunner } from './features/query/state/QueryRunner';
import { runRequest } from './features/query/state/runRequest';
import { initWindowRuntime } from './features/runtime/init';
import { cleanupOldExpandedFolders } from './features/search/utils';
import { variableAdapters } from './features/variables/adapters';
import { createAdHocVariableAdapter } from './features/variables/adhoc/adapter';
import { createConstantVariableAdapter } from './features/variables/constant/adapter';
import { createCustomVariableAdapter } from './features/variables/custom/adapter';
import { createDataSourceVariableAdapter } from './features/variables/datasource/adapter';
import { getVariablesUrlParams } from './features/variables/getAllVariableValuesForUrl';
import { createIntervalVariableAdapter } from './features/variables/interval/adapter';
import { setVariableQueryRunner, VariableQueryRunner } from './features/variables/query/VariableQueryRunner';
import { createQueryVariableAdapter } from './features/variables/query/adapter';
import { createSwitchVariableAdapter } from './features/variables/switch/adapter';
import { createSystemVariableAdapter } from './features/variables/system/adapter';
import { createTextBoxVariableAdapter } from './features/variables/textbox/adapter';
import { configureStore } from './store/configureStore';

// import symlinked extensions
const extensionsIndex = require.context('.', true, /extensions\/index.ts/);
const extensionsExports = extensionsIndex.keys().map((key) => {
  return extensionsIndex(key);
});

export class GrafanaApp {
  context!: GrafanaContextType;

  async init() {
    try {
      await preInitTasks();

      // Let iframe container know grafana has started loading
      window.parent.postMessage('GrafanaAppInit', '*');

      initSystemJSHooks();

      // Currently the OpenFeature API requires a signed in user. This means feature flags cannot be used
      // on the login page.
      if (contextSrv.user.isSignedIn) {
        try {
          await initOpenFeature();
        } catch (err) {
          console.error('Failed to initialize OpenFeature provider', err);
        }
      }

      const regionalFormat = config.featureToggles.localeFormatPreference
        ? config.regionalFormat
        : contextSrv.user.language;

      const initI18nPromise = initializeI18n(
        {
          language: contextSrv.user.language,
          ns: NAMESPACES,
          module: loadTranslations,
        },
        regionalFormat
      );

      // This is a placeholder so we can put a 'comment' in the message json files.
      // Starts with an underscore so it's sorted to the top of the file. Even though it is in a comment the following line is still extracted
      // t('_comment', 'The code is the source of truth for English phrases. They should be updated in the components directly, and additional plurals specified in this file.');
      initI18nPromise.then(async ({ language }) => {
        updateConfig({ language });

        // Initialise scenes translations into the Grafana namespace. Must finish before any scenes UI is rendered.
        return loadNamespacedResources(GRAFANA_NAMESPACE, language ?? DEFAULT_LANGUAGE, [loadScenesResources]);
      });

      setBackendSrv(backendSrv);
      await initEchoSrv();
      // This needs to be done after the `initEchoSrv` since it is being used under the hood.
      startMeasure('frontend_app_init');

      setLocale(config.regionalFormat);
      setWeekStart(contextSrv.user.weekStart);
      setPanelRenderer(PanelRenderer);
      setPluginPage(PluginPage);
      setFolderPicker(LazyFolderPicker);
      setPanelDataErrorView(PanelDataErrorView);
      setLocationSrv(locationService);
      setCorrelationsService(new CorrelationsService());
      setEmbeddedDashboard(EmbeddedDashboardLazy);
      setTimeZoneResolver(() => contextSrv.user.timezone);
      initGrafanaLive();
      setCurrentUser(contextSrv.user);

      // Expose the app-wide eventbus
      setAppEvents(appEvents);

      // We must wait for translations to load because some preloaded store state requires translating
      await initI18nPromise;

      // Important that extension reducers are initialized before store
      addExtensionReducers();
      configureStore();
      initExtensions();

      initAlerting();

      standardEditorsRegistry.setInit(getAllOptionEditors);
      standardFieldConfigEditorRegistry.setInit(getAllStandardFieldConfigs);
      standardTransformersRegistry.setInit(getStandardTransformers);
      variableAdapters.setInit(() => [
        createQueryVariableAdapter(),
        createCustomVariableAdapter(),
        createTextBoxVariableAdapter(),
        createConstantVariableAdapter(),
        createDataSourceVariableAdapter(),
        createIntervalVariableAdapter(),
        createAdHocVariableAdapter(),
        createSystemVariableAdapter(),
        createSwitchVariableAdapter(),
      ]);

      monacoLanguageRegistry.setInit(getDefaultMonacoLanguages);
      setMonacoEnv();

      setQueryRunnerFactory(() => new QueryRunner());
      setVariableQueryRunner(new VariableQueryRunner());

      // Provide runRequest implementation to packages, @grafana/scenes in particular
      setRunRequest(runRequest);

      // Privide plugin import utils to packages, @grafana/scenes in particular
      setPluginImportUtils({
        importPanelPlugin,
        getPanelPluginFromCache: syncGetPanelPlugin,
      });

      // Login redirect requires locationUtil to be initialized
      locationUtil.initialize({
        config: window.grafanaBootData.settings,
        getTimeRangeForUrl: getTimeSrv().timeRangeForUrl,
        getVariablesUrlParams: getVariablesUrlParams,
      });

      if (config.featureToggles.useSessionStorageForRedirection) {
        handleRedirectTo();
      }

      // intercept anchor clicks and forward it to custom history instead of relying on browser's history
      document.addEventListener('click', interceptLinkClicks);

      // Init DataSourceSrv
      const dataSourceSrv = new DatasourceSrv();
      dataSourceSrv.init(config.datasources, config.defaultDatasource);
      setDataSourceSrv(dataSourceSrv);
      initWindowRuntime();

      // Do not pre-load apps if rendererDisableAppPluginsPreload is true and the request comes from the image renderer
      const skipAppPluginsPreload =
        config.featureToggles.rendererDisableAppPluginsPreload && contextSrv.user.authenticatedBy === 'render';
      if (contextSrv.user.orgRole !== '' && !skipAppPluginsPreload) {
        preloadPlugins(await getAppPluginsToPreload());
      }

      getPluginExtensionRegistries();

      setHelpNavItemHook(useHelpNode);
      setPluginLinksHook(usePluginLinks);
      setPluginComponentHook(usePluginComponent);
      setPluginComponentsHook(usePluginComponents);
      setPluginFunctionsHook(usePluginFunctions);
      setGetObservablePluginLinks(getObservablePluginLinks);
      setGetObservablePluginComponents(getObservablePluginComponents);

      // initialize chrome service
      const queryParams = locationService.getSearchObject();
      const chromeService = new AppChromeService();
      const keybindingsService = new KeybindingSrv(locationService, chromeService);
      const newAssetsChecker = new NewFrontendAssetsChecker();
      newAssetsChecker.start();

      // Read initial kiosk mode from url at app startup
      chromeService.setKioskModeFromUrl(queryParams.kiosk);

      // Clean up old search local storage values
      try {
        cleanupOldExpandedFolders();
      } catch (err) {
        console.warn('Failed to clean up old expanded folders', err);
      }

      this.context = {
        backend: backendSrv,
        location: locationService,
        chrome: chromeService,
        keybindings: keybindingsService,
        newAssetsChecker,
        config,
      };

      setReturnToPreviousHook(useReturnToPreviousInternal);
      setMegaMenuOpenHook(useMegaMenuOpenInternal);
      setChromeHeaderHeightHook(useChromeHeaderHeight);

      if (config.featureToggles.crashDetection) {
        initializeCrashDetection();
      }

      if (config.featureToggles.dashboardLevelTimeMacros) {
        sceneUtils.registerVariableMacro('__from', DashboardLevelTimeMacro, true);
        sceneUtils.registerVariableMacro('__to', DashboardLevelTimeMacro, true);
      }

      const root = createRoot(document.getElementById('reactRoot')!);
      root.render(createElement(AppWrapper, { context: this.context }));

      await postInitTasks();
    } catch (error) {
      console.error('Failed to start Grafana', error);
      window.__grafana_load_failed();
    } finally {
      stopMeasure('frontend_app_init');
    }
  }
}

function addExtensionReducers() {
  if (extensionsExports.length > 0) {
    extensionsExports[0].addExtensionReducers();
  }
}

function initExtensions() {
  if (extensionsExports.length > 0) {
    extensionsExports[0].init();
  }
}

function handleRedirectTo(): void {
  const queryParams = locationService.getSearch();
  const redirectToParamKey = 'redirectTo';

  if (queryParams.has('auth_token')) {
    // URL Login should not be redirected
    window.sessionStorage.removeItem(RedirectToUrlKey);
    return;
  }

  if (queryParams.has(redirectToParamKey) && window.location.pathname !== '/') {
    const rawRedirectTo = queryParams.get(redirectToParamKey)!;
    window.sessionStorage.setItem(RedirectToUrlKey, encodeURIComponent(rawRedirectTo));
    queryParams.delete(redirectToParamKey);
    window.history.replaceState({}, '', `${window.location.pathname}${queryParams.size > 0 ? `?${queryParams}` : ''}`);
    return;
  }

  if (!contextSrv.user.isSignedIn) {
    return;
  }

  const redirectTo = window.sessionStorage.getItem(RedirectToUrlKey);
  if (!redirectTo) {
    return;
  }

  window.sessionStorage.removeItem(RedirectToUrlKey);
  let decodedRedirectTo = decodeURIComponent(redirectTo);
  if (decodedRedirectTo.startsWith('/goto/')) {
    // In this case there should be a request to the backend
    const urlToRedirectTo = locationUtil.assureBaseUrl(decodedRedirectTo);
    window.location.replace(urlToRedirectTo);
    return;
  }
  // Ensure that the appsuburl is stripped from the redirect to in case of a frontend redirect
  const stripped = locationUtil.stripBaseFromUrl(decodedRedirectTo);
  locationService.replace(stripped);
}

export default new GrafanaApp();
