import React, {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useState,
  type ReactNode,
} from 'react';
import i18n from '../i18n';
import { useQueryClient } from '@tanstack/react-query';

// ─────────────────────────────────────────────────────────────────────────────
// Types
// ─────────────────────────────────────────────────────────────────────────────

/**
 * Language code — any string is allowed so consumers can extend with their own
 * locales (e.g. `'fr'`, `'de'`, `'ja'`). The library ships with built-in
 * support for `'pt-BR'`, `'en'`, and `'es'`, but those are recommendations not
 * restrictions.
 */
export type Language = string;

/**
 * Descriptor for a single language available in the picker.
 */
export interface LanguageDefinition {
  /** ISO/BCP-47 code stored in localStorage and passed to i18n.changeLanguage() */
  code: Language;
  /** Full display label shown in the LanguageSelector dropdown */
  label: string;
  /** Short label shown in `variant="minimal"` (e.g. "PT", "EN") */
  shortLabel?: string;
  /** Optional translation JSON. When provided, it is registered with i18next
   *  on mount so dynamically added locales are loaded automatically.
   *  Use a plain object matching your `pt-BR.json` shape. */
  resources?: Record<string, unknown>;
}

interface LanguageContextType {
  /** Currently active language code */
  language: Language;
  /** Change the active language (persists + updates i18n + invalidates queries) */
  setLanguage: (language: Language) => void;
  /** All languages registered for this app */
  availableLanguages: LanguageDefinition[];
  /** True when only a single language is configured — UI can hide selectors */
  isMonolingual: boolean;
}

// ─────────────────────────────────────────────────────────────────────────────
// Defaults
// ─────────────────────────────────────────────────────────────────────────────

/**
 * Default language set when no `availableLanguages` prop is provided.
 * These are the locales the library ships with out-of-the-box.
 */
export const DEFAULT_LANGUAGES: LanguageDefinition[] = [
  { code: 'pt-BR', label: 'Português (BR)', shortLabel: 'PT' },
  { code: 'en', label: 'English', shortLabel: 'EN' },
  { code: 'es', label: 'Español', shortLabel: 'ES' },
];

const STORAGE_KEY = 'xertica_language';

// Maps legacy localStorage values (from older versions) to canonical codes.
const LEGACY_LANGUAGE_MAP: Record<string, string> = {
  PT: 'pt-BR',
  pt: 'pt-BR',
  EN: 'en',
  'en-US': 'en',
  ES: 'es',
};

// ─────────────────────────────────────────────────────────────────────────────
// Context
// ─────────────────────────────────────────────────────────────────────────────

const LanguageContext = createContext<LanguageContextType | undefined>(undefined);

// ─────────────────────────────────────────────────────────────────────────────
// Provider
// ─────────────────────────────────────────────────────────────────────────────

export interface LanguageProviderProps {
  children: ReactNode;
  /**
   * List of languages available to this app. The first item is the default
   * when no saved preference exists. Pass a single-item array to lock the
   * app to one language (the LanguageSelector will hide itself automatically).
   * Defaults to `DEFAULT_LANGUAGES` (pt-BR, en, es) when omitted.
   *
   * @example
   * ```tsx
   * // Monolingual English app
   * <LanguageProvider availableLanguages={[{ code: 'en', label: 'English' }]}>
   * ```
   *
   * @example
   * ```tsx
   * // Add French alongside the built-in languages
   * import fr from './locales/fr.json';
   *
   * <LanguageProvider availableLanguages={[
   *   ...DEFAULT_LANGUAGES,
   *   { code: 'fr', label: 'Français', shortLabel: 'FR', resources: fr },
   * ]}>
   * ```
   */
  availableLanguages?: LanguageDefinition[];
  /**
   * Override the default language. Falls back to the first item in
   * `availableLanguages` when omitted.
   */
  defaultLanguage?: Language;
}

export function LanguageProvider({
  children,
  availableLanguages = DEFAULT_LANGUAGES,
  defaultLanguage,
}: LanguageProviderProps) {
  // Hard guard: empty array is almost always a consumer mistake. Throwing here
  // surfaces the bug at provider mount instead of letting the app silently fall
  // back to DEFAULT_LANGUAGES (which would, in turn, surprise monolingual apps
  // that meant to pass `[{code:'en',...}]` but accidentally passed `[]`).
  if (availableLanguages.length === 0) {
    throw new Error(
      'LanguageProvider: `availableLanguages` cannot be empty. Pass at least one ' +
        'LanguageDefinition or omit the prop to use DEFAULT_LANGUAGES.'
    );
  }
  const languages = availableLanguages;

  // useQueryClient() works because LanguageProvider is always rendered inside
  // <QueryClientProvider> (both in App.tsx and via XerticaProvider in templates).
  const queryClient = useQueryClient();

  // Stable "signature" of the configured languages — string of comma-joined
  // codes. This prevents the effect below (and downstream `useMemo`) from
  // firing every render when a consumer passes `availableLanguages={[...]}`
  // inline (a fresh array reference on every render).
  const languageCodesKey = useMemo(() => languages.map(l => l.code).join(','), [languages]);

  // Register any locale `resources` carried by language definitions. This
  // allows consumers to ship new locales without editing `i18n.ts`.
  useEffect(() => {
    for (const lang of languages) {
      if (lang.resources && !i18n.hasResourceBundle(lang.code, 'translation')) {
        i18n.addResourceBundle(lang.code, 'translation', lang.resources, true, true);
      }
    }
    // `languageCodesKey` is a stable string signature; only re-runs when the
    // set of configured language codes actually changes.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [languageCodesKey]);

  const resolvedDefault = defaultLanguage ?? languages[0].code;

  const [language, setLanguageState] = useState<Language>(() => {
    const saved = typeof window !== 'undefined' ? window.localStorage?.getItem(STORAGE_KEY) : null;

    // Direct match against configured languages
    if (saved && languages.some(l => l.code === saved)) {
      return saved;
    }

    // Legacy migration
    if (saved && LEGACY_LANGUAGE_MAP[saved]) {
      const migrated = LEGACY_LANGUAGE_MAP[saved];
      if (languages.some(l => l.code === migrated)) {
        return migrated;
      }
    }

    return resolvedDefault;
  });

  /** Change language: persists to localStorage, updates i18next, and invalidates
   *  the React Query cache so any query whose result contains translated strings
   *  is refetched in the new language on next render.
   *
   *  Components using `useTranslation()` already update instantly (i18next notifies
   *  them). Queries using `useFeatureCards()` / `useAssistantConfig()` etc. also
   *  update instantly because language is included in their queryKey — switching
   *  language means a new key → cache miss → immediate refetch.
   *
   *  Wrapped in `useCallback` so that consumers destructuring `setLanguage` and
   *  passing it into effect dependency arrays get a stable reference. */
  const setLanguage = useCallback(
    (lang: Language) => {
      // Silently no-op if the requested language is not registered.
      if (!languages.some(l => l.code === lang)) {
        if (typeof console !== 'undefined') {
          console.warn(
            `[LanguageProvider] Language "${lang}" is not in availableLanguages — ignoring.`
          );
        }
        return;
      }
      setLanguageState(lang);
      window.localStorage?.setItem(STORAGE_KEY, lang);
      i18n.changeLanguage(lang);
      // Defensive invalidation: refetches any query that didn't include language
      // in its queryKey and would otherwise serve stale translated strings.
      queryClient.invalidateQueries();
    },
    // Depend on the stable codes signature instead of the `languages` array
    // reference so the callback identity remains stable across inline-prop renders.
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [languageCodesKey, queryClient]
  );

  // Sync i18next on mount in case the page refreshed with a saved language
  useEffect(() => {
    if (i18n.language !== language) {
      i18n.changeLanguage(language);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []); // mount-only — i18n.ts already reads localStorage for the initial lng

  const value = useMemo<LanguageContextType>(
    () => ({
      language,
      setLanguage,
      availableLanguages: languages,
      isMonolingual: languages.length <= 1,
    }),
    // `setLanguage` is stable thanks to useCallback above; `languages` is gated
    // by `languageCodesKey` so that inline-array consumers don't thrash the memo.
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [language, setLanguage, languageCodesKey]
  );

  return <LanguageContext.Provider value={value}>{children}</LanguageContext.Provider>;
}

// ─────────────────────────────────────────────────────────────────────────────
// Hook
// ─────────────────────────────────────────────────────────────────────────────

export function useLanguage() {
  const context = useContext(LanguageContext);
  if (context === undefined) {
    throw new Error('useLanguage must be used within a LanguageProvider');
  }
  return context;
}
