import {
  ParentComponent,
  createContext,
  createMemo,
  createResource,
  useContext,
} from "solid-js";
import { match } from "@formatjs/intl-localematcher";
import { Accessor, createEffect, createSignal } from "solid-js";
import { $settings } from "../settings/stores";
import { enGB } from "date-fns/locale/en-GB";
import { useStore } from "@nanostores/solid";
import type { Locale } from "date-fns";
import {
  resolveTemplate,
  translator,
  type Template,
} from "@solid-primitives/i18n";

async function synchronised(
  name: string,
  callback: () => Promise<void> | void,
): Promise<void> {
  await navigator.locks.request(name, callback);
}

export const SUPPORTED_LANGS = ["en", "zh-Hans"] as const;

export const SUPPORTED_REGIONS = ["en_US", "en_GB", "zh_CN"] as const;

const DEFAULT_LANG = "en";

/**
 * Decide the using language for the user.
 * @returns the selected language tag
 */
export function autoMatchLangTag() {
  return match(Array.from(navigator.languages), SUPPORTED_LANGS, DEFAULT_LANG);
}

const DateFnLocaleCx = /* __@PURE__ */ createContext<Accessor<Locale>>(
  () => enGB,
);

const cachedDateFnLocale: Record<string, Locale> = {
  enGB,
};

export function autoMatchRegion() {
  const specifiers = navigator.languages.map((x) => x.split("-"));

  for (const s of specifiers) {
    if (s.length === 1) {
      const lang = s[0];
      for (const available of SUPPORTED_REGIONS) {
        if (available.toLowerCase().startsWith(lang.toLowerCase())) {
          return available;
        }
      }
    } else if (s.length === 2) {
      const [lang, region] = s;
      for (const available of SUPPORTED_REGIONS) {
        if (available.toLowerCase() === `${lang}_${region}`.toLowerCase()) {
          return available;
        }
      }
    }
  }

  return "en_GB";
}

export function useRegion() {
  const appSettings = useStore($settings);

  return createMemo(
    () => {
      const settings = appSettings();
      if (typeof settings.region !== "undefined") {
        return settings.region;
      } else {
        return autoMatchRegion();
      }
    },
    "en_GB",
    { name: "region" },
  );
}

async function importDateFnLocale(tag: string): Promise<Locale> {
  switch (tag.toLowerCase()) {
    case "en_us":
      return (await import("date-fns/locale/en-US")).enUS;
    case "en_gb":
      return enGB;
    case "zh_cn":
      return (await import("date-fns/locale/zh-CN")).zhCN;
    default:
      throw new TypeError(`unsupported tag "${tag}"`);
  }
}

/**
 * Provides runtime values and fetch dependencies for date-fns locale
 */
export const DateFnScope: ParentComponent = (props) => {
  const [dateFnLocale, setDateFnLocale] = createSignal(enGB, {
    name: "dateFnLocale",
  });
  const region = useRegion();

  createEffect(() => {
    const dateFnLocaleName = region();

    if (cachedDateFnLocale[dateFnLocaleName]) {
      setDateFnLocale(cachedDateFnLocale[dateFnLocaleName]);
    } else {
      synchronised("i18n-wrapper-load-date-fns-locale", async () => {
        if (cachedDateFnLocale[dateFnLocaleName]) {
          setDateFnLocale(cachedDateFnLocale[dateFnLocaleName]);
          return;
        }
        const target = `date-fns/locale/${dateFnLocaleName}`;
        try {
          const mod = await importDateFnLocale(dateFnLocaleName);
          cachedDateFnLocale[dateFnLocaleName] = mod;
          setDateFnLocale(mod);
        } catch (reason) {
          console.error(
            {
              act: "load-date-fns-locale",
              stat: "failed",
              reason,
              target,
            },
            "failed to load date-fns locale",
          );
        }
      });
    }
  });

  return (
    <DateFnLocaleCx.Provider value={dateFnLocale}>
      {props.children}
    </DateFnLocaleCx.Provider>
  );
};

/**
 * Get the {@link Locale} object for date-fns.
 *
 * This function must be using in {@link DateFnScope}
 *
 * @returns Accessor for Locale
 */
export function useDateFnLocale(): Accessor<Locale> {
  const cx = useContext(DateFnLocaleCx);
  return cx;
}

export function useLanguage() {
  const settings = useStore($settings);
  return () => settings().language || autoMatchLangTag();
}

type ImportFn<T> = (name: string) => Promise<{ default: T }>;

type ImportedModule<F> = F extends ImportFn<infer T> ? T : never;

type MergedImportedModule<T> = T extends []
  ? {}
  : T extends [infer I]
    ? ImportedModule<I>
    : T extends [infer I, ...infer J]
      ? ImportedModule<I> & MergedImportedModule<J>
      : never;

export function createStringResource<
  T extends ImportFn<Record<string, string | Template<any> | undefined>>[],
>(...importFns: T) {
  const language = useLanguage();
  const cache: Record<string, MergedImportedModule<T>> = {};

  return createResource(
    () => [language()] as const,
    async ([nlang]) => {
      if (cache[nlang]) {
        return cache[nlang];
      }

      const results = await Promise.all(
        importFns.map((x) => x(nlang).then((v) => v.default)),
      );

      const merged: MergedImportedModule<T> = Object.assign({}, ...results);

      cache[nlang] = merged;

      return merged;
    },
  );
}

export function createTranslator<
  T extends ImportFn<Record<string, string | Template<any> | undefined>>[],
>(...importFns: T) {
  const res = createStringResource(...importFns);

  return [translator(res[0], resolveTemplate), res] as const;
}
