'use client';

import { LocaleProvider, useLocale } from '@tale/ui/i18n/locale-provider';
import { LocaleSync } from '@tale/ui/i18n/sync';
import type { i18n as I18nInstance } from 'i18next';
import type { ReactNode } from 'react';
import { I18nextProvider } from 'react-i18next';

import { type Theme, ThemeProvider } from './theme';

interface ClientLocaleConfig {
  /**
   * Use client-side detection: `<LocaleProvider>` reads localStorage →
   * `window.__ACCEPT_LANGUAGE__` → `navigator.languages` and exposes
   * `useLocale()` for the language picker. URL-driven services (web, docs)
   * omit `locale` entirely and mount `<LocaleSync>` from the root route
   * with `useCurrentLocale()`.
   */
  mode: 'client';
  /**
   * Side-effect fired on mount with the detected locale and after every
   * `setLocale()`. Platform passes `loadDayjsLocale` here so the matching
   * dayjs locale is fetched lazily without each consumer having its own
   * effect. Errors are not caught — the callback owns its failure modes.
   */
  onChange?: (locale: string) => void | Promise<void>;
  /** Falls back to the package default ('en-US') if omitted. */
  defaultLocale?: string;
}

interface ThemeConfig {
  /**
   * Overrides the package default (`'system'`). Don't set unless this
   * service has a specific reason to override OS preference — the canonical
   * setup is to leave it unset so the user's OS choice wins.
   */
  defaultTheme?: Theme;
}

type ThemeOption = boolean | ThemeConfig;

interface AppShellProps {
  /** The service's i18n instance, returned from `initServiceI18n`. */
  i18n: I18nInstance;
  /**
   * Locale source. Provide `{ mode: 'client', ... }` for services that drive
   * locale from a saved preference (platform, scaffolded services). Omit for
   * URL-driven services (web, docs) — those mount `<LocaleSync>` from their
   * own root route with the value read from the URL.
   */
  locale?: ClientLocaleConfig;
  /**
   * Set to `true` (`<AppShell theme>`) to mount `<ThemeProvider>` from
   * `@tale/ui/theme` with the canonical `'system'` default. Pass a config
   * object only when a service needs to override `defaultTheme`. Omit
   * entirely for services that intentionally don't toggle the `.dark` class
   * on `<html>` (currently the marketing site, until its dark styles ship).
   */
  theme?: ThemeOption;
  children: ReactNode;
}

function ClientLocaleBridge() {
  const { locale } = useLocale();
  return <LocaleSync locale={locale} />;
}

/**
 * Standardized provider shell every Tale frontend service uses. Owns the
 * cross-cutting theme + locale + i18n stack in the order that matters:
 *
 *   `<ThemeProvider>` → `<LocaleProvider>` → `<I18nextProvider>`
 *     → `<LocaleSync>` → children
 *
 * The ordering inside the i18n+locale block is load-bearing:
 * `<I18nextProvider>` contains a bridge that reads `useLocale()`, so
 * `<LocaleProvider>` must sit above it. Bundling the providers prevents
 * services from reinventing the bridge — every service used to ship a
 * per-service `lib/i18n/i18n-provider.tsx` doing exactly this.
 *
 * Query client, auth, branding, and router providers are not bundled
 * because they vary per service. Compose them around `<AppShell>`:
 *
 *   ```tsx
 *   <AppShell
 *     i18n={i18n}
 *     locale={{ mode: 'client', onChange: loadDayjsLocale }}
 *     theme
 *   >
 *     <RouterProvider router={router} />
 *   </AppShell>
 *   ```
 *
 * Services with extra outer providers (Convex auth, site-URL context, …)
 * wrap them above `<AppShell>`; extra inner providers (query client,
 * dialog providers, …) nest between `<AppShell>` and `<RouterProvider>`.
 */
export function AppShell({ i18n, locale, theme, children }: AppShellProps) {
  let tree: ReactNode = (
    <I18nextProvider i18n={i18n}>
      {locale ? <ClientLocaleBridge /> : null}
      {children}
    </I18nextProvider>
  );

  if (locale) {
    tree = (
      <LocaleProvider
        defaultLocale={locale.defaultLocale}
        onLocaleChange={locale.onChange}
      >
        {tree}
      </LocaleProvider>
    );
  }

  if (theme) {
    const themeConfig = theme === true ? undefined : theme;
    tree = (
      <ThemeProvider defaultTheme={themeConfig?.defaultTheme}>
        {tree}
      </ThemeProvider>
    );
  }

  return tree;
}
