/*
Copyright 2022-2024 New Vector Ltd.

SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
*/

import i18n, {
  type BackendModule,
  type ReadCallback,
  type ResourceKey,
} from "i18next";
import { initReactI18next } from "react-i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import * as Sentry from "@sentry/react";
import { logger } from "matrix-js-sdk/src/logger";
import { shouldPolyfill as shouldPolyfillSegmenter } from "@formatjs/intl-segmenter/should-polyfill";
import { shouldPolyfill as shouldPolyfillDurationFormat } from "@formatjs/intl-durationformat/should-polyfill";

import { getUrlParams } from "./UrlParams";
import { Config } from "./config/Config";
import { ElementCallOpenTelemetry } from "./otel/otel";
import { platform } from "./Platform";

// This generates a map of locale names to their URL (based on import.meta.url), which looks like this:
// {
//   "../locales/en/app.json": "/whatever/assets/root/locales/en-aabbcc.json",
//   ...
// }
const locales = import.meta.glob<string>("../locales/*/*.json", {
  query: "?url",
  import: "default",
  eager: true,
});

const getLocaleUrl = (
  language: string,
  namespace: string,
): string | undefined => locales[`../locales/${language}/${namespace}.json`];

const supportedLngs = [
  ...new Set(
    Object.keys(locales).map((url) => {
      // The URLs are of the form ../locales/en/app.json
      // This extracts the language code from the URL
      const lang = url.match(/\/([^/]+)\/[^/]+\.json$/)?.[1];
      if (!lang) {
        throw new Error(`Could not parse locale URL ${url}`);
      }
      return lang;
    }),
  ),
];

// A backend that fetches the locale files from the URLs generated by the glob above
const Backend = {
  type: "backend",
  init(): void {},
  read(language: string, namespace: string, callback: ReadCallback): void {
    (async (): Promise<ResourceKey> => {
      const url = getLocaleUrl(language, namespace);
      if (!url) {
        throw new Error(
          `Namespace ${namespace} for locale ${language} not found`,
        );
      }

      const response = await fetch(url, {
        credentials: "omit",
        headers: {
          Accept: "application/json",
        },
      });

      if (!response.ok) {
        throw Error(`Failed to fetch ${url}`);
      }

      return await response.json();
    })().then(
      (data) => callback(null, data),
      (error) => callback(error, null),
    );
  },
} satisfies BackendModule;

enum LoadState {
  None,
  Loading,
  Loaded,
}

class DependencyLoadStates {
  public config: LoadState = LoadState.None;
  public sentry: LoadState = LoadState.None;
  public openTelemetry: LoadState = LoadState.None;

  public allDepsAreLoaded(): boolean {
    return !Object.values(this).some((s) => s !== LoadState.Loaded);
  }
}

export class Initializer {
  private static internalInstance: Initializer;
  private isInitialized = false;

  public static isInitialized(): boolean {
    return Initializer.internalInstance?.isInitialized;
  }

  public static async initBeforeReact(): Promise<void> {
    const polyfills: Promise<unknown>[] = [];
    if (shouldPolyfillSegmenter()) {
      polyfills.push(import("@formatjs/intl-segmenter/polyfill-force"));
    }

    if (shouldPolyfillDurationFormat()) {
      polyfills.push(import("@formatjs/intl-durationformat/polyfill-force"));
    }

    await Promise.all(polyfills);

    //i18n
    const languageDetector = new LanguageDetector();
    languageDetector.addDetector({
      name: "urlFragment",
      // Look for a language code in the URL's fragment
      lookup: () => getUrlParams().lang ?? undefined,
    });

    await i18n
      .use(Backend)
      .use(languageDetector)
      .use(initReactI18next)
      .init({
        fallbackLng: "en",
        defaultNS: "app",
        keySeparator: ".",
        nsSeparator: false,
        pluralSeparator: "_",
        contextSeparator: "|",
        supportedLngs,
        interpolation: {
          escapeValue: false, // React has built-in XSS protections
        },
        detection: {
          // No localStorage detectors or caching here, since we don't have any way
          // of letting the user manually select a language
          order: ["urlFragment", "navigator"],
          caches: [],
        },
      });

    // Custom Themeing
    if (import.meta.env.VITE_CUSTOM_CSS) {
      const style = document.createElement("style");
      style.textContent = import.meta.env.VITE_CUSTOM_CSS;
      document.head.appendChild(style);
    }

    // Custom fonts
    const { fonts, fontScale } = getUrlParams();
    if (fontScale !== null) {
      document.documentElement.style.setProperty(
        "--font-scale",
        fontScale.toString(),
      );
    }
    if (fonts.length > 0) {
      document.documentElement.style.setProperty(
        "--font-family",
        fonts.map((f) => `"${f}"`).join(", "),
      );
    }

    // Add the platform to the DOM, so CSS can query it
    document.body.setAttribute("data-platform", platform);
  }

  public static init(): Promise<void> | null {
    if (Initializer?.internalInstance?.initPromise) {
      return null;
    }
    Initializer.internalInstance = new Initializer();
    Initializer.internalInstance.initPromise = new Promise<void>((resolve) => {
      // initStep calls itself recursively until everything is initialized in the correct order.
      // Then the promise gets resolved.
      Initializer.internalInstance.initStep(resolve);
    });
    return Initializer.internalInstance.initPromise;
  }

  private loadStates = new DependencyLoadStates();

  private initStep(resolve: (value: void | PromiseLike<void>) => void): void {
    // config
    if (this.loadStates.config === LoadState.None) {
      this.loadStates.config = LoadState.Loading;
      Config.init().then(
        () => {
          this.loadStates.config = LoadState.Loaded;
          this.initStep(resolve);
        },
        (e) => {
          logger.error("Failed to load config", e);
        },
      );
    }

    //sentry (only initialize after the config is ready)
    if (
      this.loadStates.sentry === LoadState.None &&
      this.loadStates.config === LoadState.Loaded
    ) {
      if (Config.get().sentry?.DSN && Config.get().sentry?.environment) {
        Sentry.init({
          dsn: Config.get().sentry?.DSN,
          environment: Config.get().sentry?.environment,
          integrations: [
            Sentry.reactRouterV5BrowserTracingIntegration({ history }),
          ],
          tracesSampleRate: 1.0,
        });
      }
      // Sentry is now 'loadeed' (even if we actually skipped starting
      // it due to to not being configured)
      this.loadStates.sentry = LoadState.Loaded;
    }

    // OpenTelemetry (also only after config loaded)
    if (
      this.loadStates.openTelemetry === LoadState.None &&
      this.loadStates.config === LoadState.Loaded
    ) {
      ElementCallOpenTelemetry.globalInit();
      this.loadStates.openTelemetry = LoadState.Loaded;
    }

    if (this.loadStates.allDepsAreLoaded()) {
      // resolve if there is no dependency that is not loaded
      resolve();
      this.isInitialized = true;
    }
  }

  private initPromise?: Promise<void>;
}
