import { useCallback, useEffect, useState } from 'preact/hooks';
import {
  ConsentManagerLanguageKey,
  LanguageKey,
  TranslatedMessages,
  Translations,
} from '@transcend-io/internationalization';
import { settings } from '../settings';
import { substituteHtml } from '../utils/substitute-html';
import { invertSafe } from '@transcend-io/type-utils';

export const loadedTranslations: Translations = Object.create(null);

/**
 * Mapping of browser locale to AWS base translation key
 *
 * TODO: https://transcend.height.app/T-39777
 * This is here for a quick fix of our CM UI translation logic. Copied from the monorepo's extract_intl.ts
 * This should be removed ASAP (it should be in the intl repo and imported, not in here or extract_intl.ts)
 */
export const TRANSLATE_LOCALE = {
  [LanguageKey.EsEs]: 'es',
  [LanguageKey.NlNl]: 'nl',
  [LanguageKey.NlBe]: 'nl',
  [LanguageKey.Es419]: 'es-MX',
  // This is a hack to fix the fact that we don't have a LanguageKey -> BrowserLanguageKey mapping
  'es-MX': 'es-MX',
  [LanguageKey.ZhHk]: 'zh-TW',
  [LanguageKey.AfZz]: 'af',
  [LanguageKey.Ar]: 'ar',
  [LanguageKey.En]: 'en',
  [LanguageKey.Fr]: 'fr',
  [LanguageKey.Es]: 'es',
  [LanguageKey.De]: 'de',
  [LanguageKey.It]: 'it',
  [LanguageKey.Ja]: 'ja',
  [LanguageKey.Ru]: 'ru',
  [LanguageKey.ArAe]: 'ar',
  [LanguageKey.FrFr]: 'fr',
  [LanguageKey.DeDe]: 'de',
  [LanguageKey.ItIt]: 'it',
  [LanguageKey.BgBg]: 'bg',
  [LanguageKey.ZhCn]: 'zh',
  [LanguageKey.HrHr]: 'hr',
  [LanguageKey.CsCz]: 'cs',
  [LanguageKey.DaDk]: 'da',
  [LanguageKey.EnGb]: 'en',
  [LanguageKey.FiFi]: 'fi',
  [LanguageKey.ElGr]: 'el',
  [LanguageKey.HiIn]: 'hi',
  [LanguageKey.HuHu]: 'hu',
  [LanguageKey.IdId]: 'id',
  [LanguageKey.JaJp]: 'ja',
  [LanguageKey.KoKr]: 'ko',
  [LanguageKey.LtLt]: 'lt',
  [LanguageKey.MsMy]: 'ms',
  [LanguageKey.NbNi]: 'no',
  [LanguageKey.PlPl]: 'pl',
  [LanguageKey.PtBr]: 'pt',
  [LanguageKey.PtPt]: 'pt',
  [LanguageKey.RoRo]: 'ro',
  [LanguageKey.RuRu]: 'ru',
  [LanguageKey.SrLatnRs]: 'sr',
  [LanguageKey.SvSe]: 'sv',
  [LanguageKey.TaIn]: 'ta',
  [LanguageKey.ThTh]: 'th',
  [LanguageKey.TrTr]: 'tr',
  [LanguageKey.UkUa]: 'uk',
  [LanguageKey.ViVn]: 'vi',
  [LanguageKey.EnUS]: 'en',
  [LanguageKey.EnAu]: 'en',
  [LanguageKey.FrBe]: 'fr',
  [LanguageKey.EnIe]: 'en',
  [LanguageKey.EnCa]: 'en',
  [LanguageKey.EnAe]: 'en',
  [LanguageKey.DeAt]: 'de',
  [LanguageKey.DeCh]: 'de',
  [LanguageKey.ItCh]: 'it',
  [LanguageKey.FrCh]: 'fr',
  [LanguageKey.HeIl]: 'he',
  [LanguageKey.EnNz]: 'en',
  [LanguageKey.EtEe]: 'et',
  [LanguageKey.IsIs]: 'is',
  [LanguageKey.LvLv]: 'lv',
  [LanguageKey.MtMt]: 'mt',
  [LanguageKey.SkSk]: 'sk',
  [LanguageKey.SlSl]: 'sl',
  [LanguageKey.MrIn]: 'mr',
  [LanguageKey.ZuZa]: 'en',
} as unknown as { [k in LanguageKey]: string };

/** Mapping of AWS base translation keys to list of browser locales that should use them */
export const INVERTED_TRANSLATE_LOCALE = invertSafe(TRANSLATE_LOCALE);

const getDuplicativeLocalizations = (lang: LanguageKey): LanguageKey[] =>
  INVERTED_TRANSLATE_LOCALE[TRANSLATE_LOCALE[lang]];

/**
 * Detect user-preferred languages from the user agent
 *
 * @returns an ordered list of preferred languages in BCP47 format
 */
export function getUserLanguages(): readonly string[] {
  return navigator.languages && navigator.languages.length
    ? navigator.languages
    : [navigator.language];
}

/**
 * Get all potential sub-languages from a given ISO 639-1 or BCP47 language code
 *
 * @param langCode - ISO 639-1 or BCP47 format language code
 * @returns Array of potential sub-languages, sorted by most to least specific
 */
const getAllSubLanguages = (langCode: ConsentManagerLanguageKey): string[] =>
  langCode
    .toLowerCase()
    .split('-')
    .flatMap((_, i, tags) => tags.slice(0, i + 1).join('-'))
    .reverse();

/**
 * Match preferred language list against a supported languages list
 *
 * @param preferred - Sorted language list in order of most preferable to least preferable
 * @param supported - List of supported languages to match from
 * @returns Preferred languages (including sub-language matches) that match supported languages list
 */
export const matchLanguages = (
  preferred: ConsentManagerLanguageKey[],
  supported: ConsentManagerLanguageKey[],
): string[] => {
  const matches = new Set<string>();
  const allSupportedLanguages = supported.flatMap(getAllSubLanguages);
  return preferred.flatMap(getAllSubLanguages).filter((language) => {
    if (!allSupportedLanguages.includes(language)) {
      return false;
    }
    const unique = !matches.has(language);
    if (unique) {
      matches.add(language);
    }
    return unique;
  });
};

/**
 * Get nearest matching language from a list of supported languages
 *
 * @param preferred - Sorted language list in order of most preferable to least preferable
 * @param supported - List of supported languages to match from
 * @returns Nearest supported language, sorted by preferred language list
 */
export const getNearestSupportedLanguage = (
  preferred: readonly string[],
  supported: ConsentManagerLanguageKey[],
): ConsentManagerLanguageKey | undefined =>
  supported.find((language) =>
    getAllSubLanguages(language).some((lang) =>
      preferred.some((preferredLang) => preferredLang.toLowerCase() === lang),
    ),
  );

/**
 * Sorts the supported languages by the user's preferences
 *
 * @param languages - an object of translations
 * @returns a list of language keys, sorted
 */
export const sortSupportedLanguagesByPreference = (
  languages: ConsentManagerLanguageKey[],
): ConsentManagerLanguageKey[] =>
  languages.sort((a, b) => {
    const preferredLanguagesFull = getUserLanguages();

    // Only use first token e.g. fr-CA => fr, and remove dupes e.g. [en-US, en] => [en], by casting to set
    const preferredLanguagesShort = [
      ...new Set(preferredLanguagesFull.map((l) => l.split('-')[0])),
    ];
    const rank = (l: string): number =>
      preferredLanguagesFull.includes(l)
        ? preferredLanguagesFull.indexOf(l)
        : preferredLanguagesShort.includes(l)
        ? preferredLanguagesShort.indexOf(l)
        : Infinity;
    return rank(a) - rank(b);
  });

/**
 * Picks a default language for the user
 *
 * @param supportedLanguages - Set of supported languages
 * @returns the language key of the best default language for this user
 */
export function pickDefaultLanguage(
  supportedLanguages: ConsentManagerLanguageKey[],
): ConsentManagerLanguageKey {
  if (settings.locale && supportedLanguages.includes(settings.locale)) {
    return settings.locale;
  }

  const preferredLanguages = getUserLanguages();
  /* We should refactor this ASAP TODO: https://transcend.height.app/T-39777
   * Extend supportedLanguages to include locales that we consider equivalent
   * e.g. instead of just having en, include en-US, en-GB, en-AU, etc
   */
  const extendedSupportedLanguages = supportedLanguages
    .map((lang: ConsentManagerLanguageKey) => getDuplicativeLocalizations(lang))
    .flat() as ConsentManagerLanguageKey[];
  const nearestExtendedLanguage =
    getNearestSupportedLanguage(
      preferredLanguages,
      sortSupportedLanguagesByPreference(extendedSupportedLanguages),
    ) || ConsentManagerLanguageKey.En;

  let nearestTranslation = nearestExtendedLanguage;
  if (!supportedLanguages.includes(nearestTranslation)) {
    nearestTranslation = getDuplicativeLocalizations(nearestTranslation).find(
      (lang) => supportedLanguages.includes(lang as ConsentManagerLanguageKey),
    ) as ConsentManagerLanguageKey;
  }
  return nearestTranslation;
}

/**
 * Fetch message translations
 *
 * @param translationsLocation - Base path to fetching messages
 * @param language - Language to fetch
 * @returns The translations
 */
export const getTranslations = async (
  translationsLocation: string,
  language: ConsentManagerLanguageKey,
): Promise<TranslatedMessages> => {
  loadedTranslations[language] ??= await (async () => {
    const pathToFetch = `${translationsLocation}/${language}.json`;
    const response = await fetch(pathToFetch);
    if (!response.ok) {
      throw new Error(`Failed to load translations for language ${language}`);
    }
    return response.json();
  })();

  return loadedTranslations[language];
};

/**
 * Sets the language to use in translator
 *
 * @param options - Options
 * @returns the language and a change language callback
 */
export function useLanguage({
  supportedLanguages,
  translationsLocation,
}: {
  /** Set of supported languages */
  supportedLanguages: ConsentManagerLanguageKey[];
  /** Base path to fetching messages */
  translationsLocation: string;
}): {
  /** The language in use */
  language: ConsentManagerLanguageKey;
  /** A change language callback */
  handleChangeLanguage: (language: ConsentManagerLanguageKey) => void;
  /** Message translations */
  messages: TranslatedMessages | undefined;
  /** HTML opening/closing tab variables */
  htmlTagVariables: Record<string, string>;
} {
  // The current language
  const [language, setLanguage] = useState<ConsentManagerLanguageKey>(() =>
    // choose a default language based on the browser selected
    pickDefaultLanguage(supportedLanguages),
  );

  // Hold the translations for that language (fetched async)
  const [messages, setMessages] = useState<TranslatedMessages | undefined>();

  // Store the HTML opening/closing tags we need to replace our tag variables with
  const [htmlTagVariables, setHtmlTagVariables] = useState<
    Record<string, string>
  >({});

  // Load the default translations
  useEffect(() => {
    getTranslations(translationsLocation, language).then((messages) => {
      // Replace raw HTML tags with variables bc raw HTML causes parsing errors
      const { substitutedMessages, tagVariables } = substituteHtml(messages);
      setHtmlTagVariables(tagVariables);
      setMessages(substitutedMessages);
    });
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const handleChangeLanguage = useCallback(
    async (language: ConsentManagerLanguageKey) => {
      const newMessages = await getTranslations(translationsLocation, language);

      // Replace raw HTML tags with variables bc raw HTML causes parsing errors
      const { substitutedMessages, tagVariables } = substituteHtml(newMessages);
      setMessages(substitutedMessages);
      setHtmlTagVariables(tagVariables);
      setLanguage(language);
    },
    [setLanguage, translationsLocation],
  );

  return { language, handleChangeLanguage, messages, htmlTagVariables };
}
