import fs from 'fs';
import { join } from 'path';
import acceptLanguage from 'accept-language';

type MessageDictionary = { [key: string]: string };

export class PageLocalizationManager {
  static readonly POLYFILL_PREFIX: 'Intl.~locale.' = 'Intl.~locale.';
  static readonly DEFAULT_LANGUAGE_TAG: 'en-us' = 'en-us';

  /**
   * The langauge tag is the full tag as defined by BCP-47.It might be: language-script-country, language, or language-country.
   */
  public readonly languageTag: string;
  /**
   * The language is just the first part of the language tag, e.g. `en` for `en-US`.
   */
  public readonly language: string;

  constructor(
    acceptLanguageHeader: string | undefined,
    supportedLanguages: string[] = PageLocalizationManager.getLanguagesOnFile(),
  ) {
    acceptLanguage.languages(this.finalizeLanguageList(supportedLanguages));

    // The language is used to do things like fetch from disk, but all the filenames are kept lowercase. So, we need to
    // downcase the language and region to meet the expectations downstream.
    this.languageTag =
      (acceptLanguageHeader &&
        acceptLanguage.get(
          this.augmentHeader(acceptLanguageHeader.toLowerCase()),
        )) ||
      PageLocalizationManager.DEFAULT_LANGUAGE_TAG;

    // If the language is not regionalized then an acceptable region will need to be assigned.
    if (this.languageTag && !this.languageTag.includes('-')) {
      this.languageTag =
        this.inferAcceptableRegion(supportedLanguages, this.languageTag) ||
        PageLocalizationManager.DEFAULT_LANGUAGE_TAG;
    }

    this.language = this.languageTag.split('-')[0];
  }

  static getLanguagesOnFile(): string[] {
    const languages = fs
      .readdirSync(join(__dirname, './translations'))
      .filter(dir => !dir.startsWith('whitelist_') && dir.endsWith('.json'))
      .map(dir => dir.split('.')[0]);

    return languages;
  }

  localePolyfillString(): string {
    return `${PageLocalizationManager.POLYFILL_PREFIX}${this.language}`;
  }

  messages(): MessageDictionary {
    return JSON.parse(
      fs
        .readFileSync(
          join(__dirname, './translations', `${this.languageTag}.json`),
        )
        .toString(),
    );
  }

  reactIntlCode(): string {
    return require(`raw-loader!react-intl/locale-data/${this.language}`);
  }

  /**
   * Perform proper casing on the language list and prepend default.
   *
   * @param supportedLanguages List of languages supported.
   */
  private finalizeLanguageList(supportedLanguages: string[]): string[] {
    return [
      PageLocalizationManager.DEFAULT_LANGUAGE_TAG,
      ...supportedLanguages,
      ...this.deregionalizeOptions(supportedLanguages),
    ].map(language => language.toLowerCase());
  }

  /**
   * Returns a unique list of acceptable languages with the region removed. Order is not guaranteed. For example:
   * [en-US, en-GB, es-es] => [en, es]
   * @param supportedLanguages List of supported language-region sets
   */
  private deregionalizeOptions(supportedLanguages: string[]): string[] {
    return Array.from(
      new Set(supportedLanguages.map(lang => lang.split('-')[0])),
    );
  }

  /**
   * This method attempts to find an acceptable `language-region` for a match on just `language`. This special
   * cases Chinese and Mexican to handle them the same way that www does.
   * @param supportedLanguages languages that are current supported
   * @param language the requested language
   */
  private inferAcceptableRegion(
    supportedLanguages: string[],
    language: string,
  ): string | undefined {
    if (language === 'es') {
      return 'es-es';
    } else if (language === 'zh') {
      return 'zh-cn';
    }
    return supportedLanguages.find(lang =>
      lang.startsWith(`${this.languageTag}-`),
    );
  }

  private augmentHeader(languageHeader: string): string {
    // Safari only sends one option and that option isn't necesarily the one you'd expect.
    // For example, it'll send es-xl for Spanish localised to Mexico. This intentionally
    // expands Safari-style accept language headers to also allow for any version of the language
    // regardless of locale if the specific language/locale is not available.
    if (/\A\w{2}-\w{2}\Z/) {
      const language = languageHeader.split('-')[0];
      return `${languageHeader}, ${language};q=0.9`;
    } else {
      return languageHeader;
    }
  }
}
