/**
 * Theme.
 *
 * Accent colours are written onto the root element as CSS variables at
 * runtime, sourced from the palette in `@pig/core`. That means one definition
 * of each colour rather than a TypeScript copy and a CSS copy that drift apart
 * the first time someone retunes a shade.
 *
 * Preferences are persisted server-side and mirrored into localStorage. The
 * server copy is authoritative and follows a person between devices; the local
 * copy exists only so the inline script in index.html can apply the right
 * theme before first paint, avoiding a white flash for dark-mode users.
 */
import {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useState,
  type ReactNode,
} from 'react';
import { ACCENTS, DEFAULT_ACCENT, getAccent, type ThemeMode } from '@pig/core';

interface ThemeContextValue {
  mode: ThemeMode;
  accent: string;
  /** The mode actually in effect once `system` is resolved. */
  resolved: 'light' | 'dark';
  setMode: (mode: ThemeMode) => void;
  setAccent: (accent: string) => void;
  accents: typeof ACCENTS;
}

const ThemeContext = createContext<ThemeContextValue | null>(null);

const STORAGE_MODE = 'pig.themeMode';
const STORAGE_ACCENT = 'pig.accent';

function readStored(key: string, fallback: string): string {
  try {
    return localStorage.getItem(key) ?? fallback;
  } catch {
    // Private browsing can throw on access. A default is fine.
    return fallback;
  }
}

function store(key: string, value: string): void {
  try {
    localStorage.setItem(key, value);
  } catch {
    /* Non-fatal: the server copy is authoritative anyway. */
  }
}

export function ThemeProvider({
  children,
  onPersist,
}: {
  children: ReactNode;
  /** Called when a preference changes, to save it server-side. */
  onPersist?: (prefs: { themeMode?: ThemeMode; accentColor?: string }) => void;
}) {
  const [mode, setModeState] = useState<ThemeMode>(
    () => readStored(STORAGE_MODE, 'system') as ThemeMode,
  );
  const [accent, setAccentState] = useState<string>(() =>
    readStored(STORAGE_ACCENT, DEFAULT_ACCENT),
  );
  const [systemDark, setSystemDark] = useState(
    () => window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false,
  );

  // Follow the OS while the user has chosen `system`, including live changes
  // — macOS and iOS both switch at sunset if configured to.
  useEffect(() => {
    const query = window.matchMedia?.('(prefers-color-scheme: dark)');
    if (!query) return;
    const listener = (event: MediaQueryListEvent) => setSystemDark(event.matches);
    query.addEventListener('change', listener);
    return () => query.removeEventListener('change', listener);
  }, []);

  const resolved: 'light' | 'dark' =
    mode === 'system' ? (systemDark ? 'dark' : 'light') : mode;

  // Paint the accent variables. Running on every change of either input keeps
  // the two halves of the palette (light and dark tunings) correctly paired.
  useEffect(() => {
    const root = document.documentElement;
    const definition = getAccent(accent);
    const values = resolved === 'dark' ? definition.dark : definition.light;

    root.setAttribute('data-theme', resolved);
    root.setAttribute('data-accent', accent);
    root.style.setProperty('--accent', values.accent);
    root.style.setProperty('--accent-fg', values.fg);
    root.style.setProperty('--accent-on', values.on);
    root.style.setProperty('--accent-subtle', values.subtle);

    // Keep Safari's toolbar in step with the app, so the chrome does not band
    // against the page when scrolled to the top.
    const meta = document.querySelector<HTMLMetaElement>('meta[name="theme-color"]:not([media])');
    const surface = resolved === 'dark' ? '#09090b' : '#ffffff';
    if (meta) meta.content = surface;
  }, [accent, resolved]);

  const setMode = useCallback(
    (next: ThemeMode) => {
      setModeState(next);
      store(STORAGE_MODE, next);
      onPersist?.({ themeMode: next });
    },
    [onPersist],
  );

  const setAccent = useCallback(
    (next: string) => {
      setAccentState(next);
      store(STORAGE_ACCENT, next);
      onPersist?.({ accentColor: next });
    },
    [onPersist],
  );

  /**
   * Adopt preferences loaded from the server without echoing them straight
   * back as a save. Exposed through the window rather than context because it
   * is called once, by the auth layer, before the tree has mounted its
   * consumers.
   */
  useEffect(() => {
    const adopt = (prefs: { themeMode?: string; accentColor?: string }) => {
      if (prefs.themeMode) {
        setModeState(prefs.themeMode as ThemeMode);
        store(STORAGE_MODE, prefs.themeMode);
      }
      if (prefs.accentColor) {
        setAccentState(prefs.accentColor);
        store(STORAGE_ACCENT, prefs.accentColor);
      }
    };
    (window as unknown as { __pigAdoptTheme?: typeof adopt }).__pigAdoptTheme = adopt;
  }, []);

  const value = useMemo(
    () => ({ mode, accent, resolved, setMode, setAccent, accents: ACCENTS }),
    [mode, accent, resolved, setMode, setAccent],
  );

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

export function useTheme(): ThemeContextValue {
  const context = useContext(ThemeContext);
  if (!context) throw new Error('useTheme must be used inside a ThemeProvider.');
  return context;
}
