import {createInstance as createI18nInstance, type i18n, type InitOptions} from 'i18next'
import {initReactI18next} from 'react-i18next'

import {type SourceOptions} from '../config'
import {localeBundlesReducer, localeDefReducer} from '../config/configPropertyReducers'
import {resolveConfigProperty} from '../config/resolveConfigProperty'
import {createSanityI18nBackend} from './backend'
import {DEBUG_I18N, maybeWrapT} from './debug'
import {studioLocaleNamespace} from './localeNamespaces'
import {defaultLocale} from './locales'
import {getPreferredLocale} from './localeStore'
import {
  type Locale,
  type LocaleDefinition,
  type LocaleResourceBundle,
  type LocaleSource,
} from './types'

/**
 * @internal
 * @hidden
 */
export function prepareI18n(source: SourceOptions): {source: LocaleSource; i18next: i18n} {
  const {projectId, dataset, name: sourceName} = source
  const context = {projectId: projectId, dataset}

  const locales = resolveConfigProperty({
    config: source,
    context,
    propertyName: 'i18n.locales',
    reducer: localeDefReducer,
    initialValue: [defaultLocale],
  })

  const bundles = resolveConfigProperty({
    config: source,
    context,
    propertyName: 'i18n.bundles',
    reducer: localeBundlesReducer,
    initialValue: normalizeResourceBundles(locales),
  })

  return createI18nApi({
    locales,
    bundles,
    projectId,
    sourceName,
  })
}

function createI18nApi({
  locales,
  bundles,
  projectId,
  sourceName,
}: {
  locales: LocaleDefinition[]
  bundles: LocaleResourceBundle[]
  projectId: string
  sourceName: string
}): {source: LocaleSource; i18next: i18n} {
  const namespaceNames = new Set(bundles.map((bundle) => bundle.namespace))
  const options = getI18NextOptions(projectId, sourceName, locales, namespaceNames)
  const i18nInstance = createI18nInstance()
    .use(createSanityI18nBackend({bundles}))
    .use(initReactI18next)

  i18nInstance.init(options).catch((err) => {
    console.error('Failed to initialize i18n backend: %s', err)
  })

  const reducedLocales = locales.map(reduceLocaleDefinition)

  return {
    /** @public */
    source: {
      get currentLocale() {
        return reducedLocales.find((locale) => locale.id === i18nInstance.language) ?? defaultLocale
      },
      loadNamespaces(namespaces: string[]): Promise<void> {
        const missing = namespaces.filter((ns) => !i18nInstance.hasLoadedNamespace(ns))
        return missing.length === 0 ? Promise.resolve() : i18nInstance.loadNamespaces(missing)
      },
      locales: reducedLocales,
      t: maybeWrapT(i18nInstance.t),
    },

    /** @internal */
    i18next: i18nInstance,
  }
}

/**
 * Takes the locales config and returns a normalized array of bundles from the defined locales.
 *
 * @param locales - The locale bundles defined in configuration/plugins
 * @returns An array of normalized bundles
 * @internal
 */
function normalizeResourceBundles(locales: LocaleDefinition[]): LocaleResourceBundle[] {
  const normalized: LocaleResourceBundle[] = []

  for (const lang of locales) {
    if (lang.bundles && !Array.isArray(lang.bundles)) {
      throw new Error(`Resource bundle for locale ${lang.id} is not an array`)
    }

    if (!lang.bundles) {
      continue
    }

    for (const bundle of lang.bundles) {
      if ('locale' in bundle && bundle.locale !== lang.id) {
        throw new Error(`Resource bundle inside locale ${lang.id} has mismatching locale id`)
      }

      const ns = bundle.namespace
      if (!ns) {
        throw new Error(`Resource bundle for locale ${lang.id} is missing namespace`)
      }

      normalized.push('locale' in bundle ? bundle : {...bundle, locale: lang.id})
    }
  }

  return normalized
}

const defaultOptions: InitOptions = {
  /**
   * Even though we're only defining the studio namespace, i18next will still load requested
   * namespaces through the backend. The reason why we're defining the namespace at all is to
   * prevent i18next from (trying) to load the i18next default `translation` namespace.
   */
  ns: [studioLocaleNamespace],
  defaultNS: studioLocaleNamespace,
  partialBundledLanguages: true,

  // Fall back to English (US) locale
  fallbackLng: defaultLocale.id,

  // This will be overriden with the users detected/preferred locale before initing,
  // but to satisfy the init options and prevent mistakes, we include a default here.
  lng: defaultLocale.id,

  // In rare cases we'll want to be able to debug i18next - there is a `debug` option
  // in the studio i18n configuration for that, which will override this value.
  debug: DEBUG_I18N,

  // When specifying language 'en-US', do not load 'en-US', 'en', 'dev' - only `en-US`.
  load: 'currentOnly',

  // We always use our "backend" for loading translations, allowing us to handle i18n resources
  // in a single place with a single approach. This means we shouldn't need to wait for the init,
  // as any missing translations will be loaded async (through react suspense).
  initImmediate: true,

  // Because we use i18next-react, we do not need to escale values
  interpolation: {
    escapeValue: false,
  },

  // Theoretically, if the framework somehow gets new translations added, re-render.
  // Note that this shouldn't actually happen, as we only use the Sanity backend
  react: {
    bindI18nStore: 'added',
  },
}

function getI18NextOptions(
  projectId: string,
  sourceName: string,
  locales: LocaleDefinition[],
  namespaces: Set<string>,
): InitOptions & {lng: string} {
  const preferredLocaleId = getPreferredLocale(projectId, sourceName)
  const preferredLocale = locales.find((l) => l.id === preferredLocaleId)
  const lastLocale = locales[locales.length - 1]
  const locale = preferredLocale?.id ?? lastLocale.id ?? defaultOptions.lng
  return {
    ...defaultOptions,
    ns: Array.from(namespaces), // For now, let us load all namespaces. We can optimize later.
    lng: locale,
    supportedLngs: locales.map((def) => def.id),
  }
}

/**
 * Reduce a locale definition to a Locale instance
 *
 * @param definition - The locale definition to reduce
 * @returns A Locale instance
 * @internal
 */
function reduceLocaleDefinition(definition: LocaleDefinition): Locale {
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  const {bundles, ...locale} = definition
  return locale
}
