/**
 * React bindings for Multideal i18n + prefs.
 *
 * Exports:
 *  - LocaleProvider   - wraps the app; syncs lang/dir/fontScale/contrast to <html>
 *  - useLocale()      - { locale, setLocale }
 *  - useT(namespace)  - memoized translator for a namespace
 *  - <T />            - inline JSX translator
 *  - usePrefs()       - { fontScale, setFontScale, contrast, setContrast }
 */

'use client';

import {
  createContext,
  useContext,
  useEffect,
  useMemo,
  useSyncExternalStore,
  type ReactNode,
} from 'react';
import { DirectionProvider } from '@radix-ui/react-direction';
import { usePrefsStore } from './store';
import { getRequestLocale } from './request-locale';
import { getT, dirForLocale } from './index';
import type { Locale } from './index';
import type { StringBundle } from './types';
import type { FontScale, ContrastMode } from './store';

// ─── Locale context ───────────────────────────────────────────────────────────

interface LocaleContextValue {
  locale: Locale;
  setLocale: (locale: Locale) => void;
}

const LocaleContext = createContext<LocaleContextValue | null>(null);

/**
 * The request locale during SSR; `undefined` in the browser, where the store
 * already seeds from `data-server-locale` and so needs no ambient fallback.
 */
function ambientLocale(): Locale | undefined {
  return typeof document === 'undefined' ? getRequestLocale() : undefined;
}

// ─── LocaleProvider ───────────────────────────────────────────────────────────

/**
 * Wrap your React tree with this provider. It reads from the zustand store and
 * applies side effects to <html> (lang, dir, --font-scale, data-contrast).
 *
 * Pass `locale` from the server (e.g. `Astro.locals.locale`) to make the SSR
 * render and the first client hydration render use the same dictionary — this
 * prevents React error #418 ("text content did not match") when the request
 * locale differs from the store default.
 *
 * When `locale` is provided, it is the source of truth for the first render
 * pass on both server and client; the zustand store is seeded with it before
 * any `useT` consumer runs. After hydration, the store takes over so user
 * actions (locale toggle, font-scale, contrast) propagate normally.
 *
 * Passing `locale` is optional. When omitted, the provider inherits an
 * enclosing provider's locale, then the ambient request locale (server) or the
 * store's `<html lang>` seed (client) — all of which resolve to the request's
 * locale, so an island that passes nothing still renders identically on both
 * sides of the hydration boundary.
 */
export function LocaleProvider({
  locale: localeProp,
  children,
}: {
  locale?: Locale;
  children: ReactNode;
}) {
  // NOTE: do NOT call `seedLocale(localeProp)` here. Mutating the module-level
  // zustand store during render is a side effect (React rule violation) and on
  // the Cloudflare Worker server it races across concurrent requests sharing
  // the same isolate (request A's `localeProp='en'` would flip the store, then
  // request B's `localeProp='he'` would flip it back, corrupting any
  // server-side reader). The render path below threads `localeProp` through
  // `LocaleContext`, which `useT` consumes directly — no store mutation
  // needed for SSR/CSR parity.
  // A nested provider with no prop inherits the ancestor's locale rather than
  // dropping to the store default, which on the server is always 'he'.
  const inherited = useContext(LocaleContext);
  const effectiveProp = localeProp ?? inherited?.locale ?? ambientLocale();

  const storeLocale = usePrefsStore((s) => s.locale);
  const fontScale = usePrefsStore((s) => s.fontScale);
  const contrast = usePrefsStore((s) => s.contrast);
  const setLocale = usePrefsStore((s) => s.setLocale);

  // Lock the rendered locale to the prop for the SSR + first hydration pass to
  // guarantee identical output across the server/client boundary. Subsequent
  // updates flow from the store (user toggles locale). We subscribe to
  // zustand's persist hydration signal via useSyncExternalStore — this keeps
  // hydration state in an external store (avoids react-hooks/set-state-in-effect)
  // and yields the SSR-safe `false` snapshot during server render.
  const hasHydrated = useSyncExternalStore(
    (cb) => usePrefsStore.persist.onFinishHydration(cb),
    () => usePrefsStore.persist.hasHydrated(),
    () => false,
  );
  const locale: Locale = !hasHydrated && effectiveProp ? effectiveProp : storeLocale;

  // Trigger rehydration from localStorage after mount. `skipHydration: true`
  // on the persist middleware prevents synchronous rehydration that would
  // cause React 19 hydration mismatches — we kick it explicitly here.
  useEffect(() => {
    usePrefsStore.persist.rehydrate();
  }, []);

  // Reconcile prop locale → store after mount, without forcing a hydration
  // mismatch. If the store rehydrated from localStorage to a different locale
  // than the server chose, the server prop wins (cookie/`?lang=` is a more
  // explicit signal than stale localStorage); push it back into the store.
  useEffect(() => {
    if (!localeProp) return;
    if (usePrefsStore.getState().locale === localeProp) return;
    setLocale(localeProp);
  }, [localeProp, setLocale]);

  // Sync locale → <html lang dir>
  useEffect(() => {
    if (typeof document === 'undefined') return;
    document.documentElement.lang = locale;
    document.documentElement.dir = dirForLocale(locale);
  }, [locale]);

  // Sync fontScale → CSS custom property
  useEffect(() => {
    if (typeof document === 'undefined') return;
    const scaleMap: Record<FontScale, string> = {
      S: '0.9',
      M: '1',
      L: '1.125',
      XL: '1.25',
      XXL: '1.5',
      XXXL: '2',
    };
    document.documentElement.style.setProperty('--font-scale', scaleMap[fontScale]);
  }, [fontScale]);

  // Sync contrast → data-contrast attribute
  useEffect(() => {
    if (typeof document === 'undefined') return;
    if (contrast === 'high') {
      document.documentElement.setAttribute('data-contrast', 'high');
    } else {
      document.documentElement.removeAttribute('data-contrast');
    }
  }, [contrast]);

  const value = useMemo<LocaleContextValue>(() => ({ locale, setLocale }), [locale, setLocale]);

  return (
    <LocaleContext.Provider value={value}>
      <DirectionProvider dir={dirForLocale(locale)}>{children}</DirectionProvider>
    </LocaleContext.Provider>
  );
}

// ─── Hooks ────────────────────────────────────────────────────────────────────

/**
 * Returns `{ locale, setLocale }`.
 */
export function useLocale(): LocaleContextValue {
  const ctx = useContext(LocaleContext);
  if (!ctx) {
    // Fallback to store directly when used outside a LocaleProvider
    // (e.g. in Astro islands where the provider may be at a higher level)
    const locale = ambientLocale() ?? usePrefsStore.getState().locale;
    const setLocale = usePrefsStore.getState().setLocale;
    return { locale, setLocale };
  }
  return ctx;
}

/**
 * Returns a memoized translator function `t(key)` for a given namespace.
 * Re-memoizes only when locale or namespace changes.
 *
 * Reads locale from `LocaleContext` first, then the ambient request locale, and
 * finally the zustand store. The last two agree by construction, so the hook
 * resolves the request's locale even outside a `LocaleProvider`.
 */
export function useT<N extends keyof StringBundle>(namespace: N) {
  const ctx = useContext(LocaleContext);
  const storeLocale = usePrefsStore((s) => s.locale);
  const locale: Locale = ctx?.locale ?? ambientLocale() ?? storeLocale;
  return useMemo(() => getT(locale, namespace), [locale, namespace]);
}

/**
 * Render a single i18n string inline, e.g. `<T namespace="common" k="loading" />`.
 */
export function T<N extends keyof StringBundle>({
  namespace,
  k,
}: {
  namespace: N;
  k: keyof StringBundle[N];
}) {
  const t = useT(namespace);
  return <>{t(k as Parameters<typeof t>[0])}</>;
}

/**
 * Returns `{ fontScale, setFontScale, contrast, setContrast }`.
 */
export function usePrefs(): {
  fontScale: FontScale;
  setFontScale: (fs: FontScale) => void;
  contrast: ContrastMode;
  setContrast: (c: ContrastMode) => void;
} {
  const fontScale = usePrefsStore((s) => s.fontScale);
  const setFontScale = usePrefsStore((s) => s.setFontScale);
  const contrast = usePrefsStore((s) => s.contrast);
  const setContrast = usePrefsStore((s) => s.setContrast);
  return { fontScale, setFontScale, contrast, setContrast };
}
