import { createContext, useEffect, useMemo, useState } from 'react'
import useLocalStorageState from 'use-local-storage-state'

import { createUseContext } from '@lib/react'

export type Theme = 'dark' | 'light' | 'system'
type ThemeLightDark = 'dark' | 'light'

type ThemeProviderProps = {
  children: React.ReactNode
  defaultTheme?: Theme
  storageKey?: string
}

type ThemeProviderState = {
  theme: Theme
  setTheme: (theme: Theme) => void
  isThemeReady: boolean
}

const initialState: ThemeProviderState = {
  theme: 'system',
  setTheme: () => null,
  isThemeReady: false,
}

const ThemeContext = createContext<ThemeProviderState>(initialState)
const STORAGE_KEY = 'usp-theme-mode'

function nextFrame(): Promise<void> {
  return new Promise((r) => requestAnimationFrame(() => r()))
}

/**
 * here we wait until the css var '--theme-ready-flag' is set, so we know the css is loaded
 * we need this 'hack' because we set the values of the DataTable with js and read the values from the css file.
 * to make sure these values are not undefined, we need to wait until the css is loaded.
 */
async function waitForThemeApplied(expected: Theme, timeoutMs = 500): Promise<boolean> {
  const start = performance.now()
  // Give the browser a paint opportunity
  await nextFrame()

  while (performance.now() - start < timeoutMs) {
    const value = getComputedStyle(document.documentElement)
      .getPropertyValue('--theme-ready-flag')
      .replaceAll(/['"\s]/g, '')
    if (value === expected) {
      return true
    }
    await nextFrame()
  }
  return false
}

export function ThemeProvider({ children, defaultTheme = 'system', ...props }: Readonly<ThemeProviderProps>) {
  const [theme, setTheme] = useLocalStorageState(STORAGE_KEY, {
    defaultValue: defaultTheme,
  })
  const [isThemeReady, setIsThemeReady] = useState(false)

  useEffect(() => {
    const root = globalThis.document.documentElement

    root.classList.remove('light', 'dark')

    let applied: ThemeLightDark
    if (theme === 'system') {
      applied = globalThis.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
      root.classList.add(applied)
      return
    }
    applied = theme
    root.classList.add(theme)

    let cancelled = false

    void (async () => {
      // Option A: wait for computed variable
      const ok = await waitForThemeApplied(applied)
      if (!cancelled) {
        setIsThemeReady(ok)
      }
    })()

    return () => {
      cancelled = true
    }
  }, [theme])

  const value = useMemo(
    () => ({
      theme,
      setTheme,
      isThemeReady,
    }),
    [isThemeReady, setTheme, theme],
  )

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

export const useTheme = createUseContext(ThemeContext, 'ThemeProviderContext')
