import {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useState,
  type ReactNode,
} from 'react'

import {
  applyTheme,
  readThemePreference,
  resolveTheme,
  storeThemePreference,
  type ResolvedTheme,
  type ThemePreference,
} from '@/lib/theme'

interface ThemeContextValue {
  preference: ThemePreference
  theme: ResolvedTheme
  setPreference: (preference: ThemePreference) => void
  /** Flips between light and dark, leaving `system` behind deliberately. */
  toggle: () => void
}

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

export function ThemeProvider({ children }: { children: ReactNode }) {
  const [preference, setPreferenceState] = useState<ThemePreference>(() => readThemePreference())
  const [theme, setTheme] = useState<ResolvedTheme>(() => resolveTheme(readThemePreference()))

  const setPreference = useCallback((next: ThemePreference) => {
    setPreferenceState(next)
    storeThemePreference(next)

    const resolved = resolveTheme(next)
    setTheme(resolved)
    applyTheme(resolved)
  }, [])

  const toggle = useCallback(() => {
    setPreference(resolveTheme(readThemePreference()) === 'dark' ? 'light' : 'dark')
  }, [setPreference])

  /*
   * While the preference is `system`, keep following the OS. Someone whose
   * machine switches to dark at sunset expects the app to follow without a
   * reload.
   */
  useEffect(() => {
    if (preference !== 'system') {
      return
    }

    const query = window.matchMedia('(prefers-color-scheme: dark)')

    const onChange = (event: MediaQueryListEvent) => {
      const resolved: ResolvedTheme = event.matches ? 'dark' : 'light'
      setTheme(resolved)
      applyTheme(resolved)
    }

    query.addEventListener('change', onChange)

    return () => query.removeEventListener('change', onChange)
  }, [preference])

  const value = useMemo<ThemeContextValue>(
    () => ({ preference, theme, setPreference, toggle }),
    [preference, theme, setPreference, toggle],
  )

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

export function useTheme(): ThemeContextValue {
  const context = useContext(ThemeContext)

  if (context === null) {
    throw new Error('useTheme must be used inside a ThemeProvider.')
  }

  return context
}
