'use client'

import { useEffect, useLayoutEffect, useMemo, useRef, useState, type ComponentProps } from 'react'
import { DirectionProvider } from '@base-ui/react/direction-provider'
import { Tooltip } from '@base-ui/react/tooltip'

import { ButtonOrLink, type ButtonOrLinkConfigProps } from './components/base/ButtonOrLink.tsx'
import { Pane } from './components/Pane.tsx'
import { GDSContext } from './GDSContext.ts'
import { cn } from './utils/index.ts'

type Theme = 'dark' | 'light' | 'system'
type ResolvedTheme = Exclude<Theme, 'system'>

const DEFAULT_THEME = 'dark' satisfies Theme

const THEME_CLASSES: Record<Theme, string> = {
  dark: 'gds-dark',
  light: 'gds-light',
  system: 'gds-system',
}

const THEME_SSR_CSS: Record<Theme, string> = {
  dark: ':root { color-scheme: only dark; --gds-color-scheme: dark; }',
  light: ':root { color-scheme: only light; --gds-color-scheme: light; }',
  system:
    ':root { color-scheme: light dark; @media (prefers-color-scheme: dark) { --gds-color-scheme: dark; } @media (prefers-color-scheme: light) { --gds-color-scheme: light; } }',
}

function resolveTheme(theme: Theme): ResolvedTheme {
  if (theme === 'system') {
    if (typeof window === 'undefined') {
      return DEFAULT_THEME
    }
    return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
  }
  return theme
}

export interface GDSProviderProps extends Omit<ComponentProps<'div'>, 'dir'> {
  /** @default 'dark' */
  theme?: Theme | undefined
  /** @default 'ltr' */
  direction?: ComponentProps<typeof DirectionProvider>['direction'] | undefined
  /** @default 'en' // or if `direction` is `rtl`, 'ar' */
  language?: string | undefined
  buttonOrLinkConfig?: Omit<ButtonOrLinkConfigProps, 'children'> | undefined
}

export type { GDSContextValue } from './GDSContext.ts'

/**
 * Provides global configuration and enhancements for GDS components.
 *
 * Note: Only one GDSProvider should be used per app, typically at the root. If you need different
 * themes for different parts of the app, use the `gds-dark` / `gds-light` utility classes instead.
 *
 * Currently implements:
 *
 * - An easy way to configure the root theme (dark/light/system), text direction, and `ButtonOrLink`
 *   config.
 * - Space key handler for improved `active` state behavior.
 * - Shared delay for multiple tooltips (once a tooltip is visible, adjacent tooltips show instantly).
 *
 * @example
 *
 * ```tsx
 * import { GDSProvider } from '@graphprotocol/gds-react'
 *
 * function App() {
 *   return (
 *     <GDSProvider theme="dark">
 *       <YourApp />
 *     </GDSProvider>
 *   )
 * }
 * ```
 */
export function GDSProvider({
  theme = DEFAULT_THEME,
  direction = 'ltr',
  language = direction === 'rtl' ? 'ar' : 'en',
  buttonOrLinkConfig,
  children,
  className,
  ...props
}: GDSProviderProps) {
  /**
   * Check for duplicate `GDSProvider` instances. We do the detection in an effect (after all
   * providers have rendered), but throw in the next render so error boundaries can catch it.
   */
  const foundDuplicateProvider = useRef(false)

  useLayoutEffect(() => {
    const existingProviders = document.querySelectorAll('.gds-provider')
    if (existingProviders.length > 1) {
      foundDuplicateProvider.current = true
    }
  })

  if (foundDuplicateProvider.current) {
    throw new Error('[GDSProvider] Rendering more than one `GDSProvider` is not allowed')
  }

  /**
   * Handle the `theme` prop:
   *
   * - Apply the correct class (`gds-dark`, `gds-light`, or `gds-system`) to the `<html>` element.
   * - Track `activeTheme` which resolves 'system' to the actual 'dark' or 'light' value based on
   *   `prefers-color-scheme`, and updates reactively when the user's system preference changes.
   */

  const themeClass = THEME_CLASSES[theme]

  if (!themeClass) {
    throw new Error(
      `[GDSProvider] Invalid theme "${theme}" provided. Expected one of: ${Object.keys(
        THEME_CLASSES,
      ).join(', ')}`,
    )
  }

  useLayoutEffect(() => {
    const html = document.documentElement
    const remove = () => html.classList.remove(...Object.values(THEME_CLASSES))
    remove()
    html.classList.add(themeClass)
    return remove
  }, [themeClass])

  const [activeTheme, setActiveTheme] = useState(() => resolveTheme(theme))

  useEffect(() => {
    setActiveTheme(resolveTheme(theme))

    if (theme !== 'system') return

    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
    const handleChange = (event: MediaQueryListEvent) => {
      setActiveTheme(event.matches ? 'dark' : 'light')
    }

    mediaQuery.addEventListener('change', handleChange)
    return () => mediaQuery.removeEventListener('change', handleChange)
  }, [theme])

  /**
   * Add the `data-gds-active` attribute to the `html` element when the Space key is pressed to
   * improve Tailwind's `active` state.
   *
   * By default, `:active` stays applied when dragging off pressed elements (e.g. buttons), which
   * makes it look like releasing the mouse button will activate the button. We work around that by
   * using `:hover:active` instead of just `:active`, but that won't work when pressing buttons with
   * Space. This hook fixes it with `:is(:hover:active, [data-gds-active] :active)`.
   */
  const isSpaceDown = useRef(false)

  useLayoutEffect(() => {
    const addAttribute = () => {
      if (isSpaceDown.current) return
      isSpaceDown.current = true
      document.documentElement.setAttribute('data-gds-active', '')
    }

    const removeAttribute = () => {
      if (!isSpaceDown.current) return
      isSpaceDown.current = false
      document.documentElement.removeAttribute('data-gds-active')
    }

    const onKeyDown = (event: KeyboardEvent) => {
      if (event.code === 'Space') addAttribute()
    }

    const onKeyUp = (event: KeyboardEvent) => {
      if (event.code === 'Space') removeAttribute()
    }

    const onWindowBlur = () => {
      removeAttribute()
    }

    const onVisibilityChange = () => {
      if (document.visibilityState === 'hidden') removeAttribute()
    }

    window.addEventListener('keydown', onKeyDown)
    window.addEventListener('keyup', onKeyUp)
    window.addEventListener('blur', onWindowBlur)
    document.addEventListener('visibilitychange', onVisibilityChange)

    return () => {
      window.removeEventListener('keydown', onKeyDown)
      window.removeEventListener('keyup', onKeyUp)
      window.removeEventListener('blur', onWindowBlur)
      document.removeEventListener('visibilitychange', onVisibilityChange)
    }
  }, [])

  const gdsContextValue = useMemo(
    () => ({
      theme,
      activeTheme,
      direction,
      language,
    }),
    [theme, activeTheme, direction, language],
  )

  return (
    <GDSContext.Provider value={gdsContextValue}>
      <DirectionProvider direction={direction}>
        <Pane.Provider>
          <Tooltip.Provider timeout={300}>
            <ButtonOrLink.Config {...buttonOrLinkConfig}>
              {/* Prevent a flash of incorrect theme in server-side rendered apps (e.g. Next.js) */}
              <PreHydrationStyle>{THEME_SSR_CSS[theme]}</PreHydrationStyle>
              <div
                dir={direction}
                lang={language}
                className={cn('gds-provider isolate', themeClass, className)}
                {...props}
              >
                {children}
              </div>
            </ButtonOrLink.Config>
          </Tooltip.Provider>
        </Pane.Provider>
      </DirectionProvider>
    </GDSContext.Provider>
  )
}

interface PreHydrationStyleProps extends ComponentProps<'style'> {}

/**
 * Renders a `<style>` element that exists only until React hydration completes.
 *
 * This component outputs its `<style>` tag during server-side rendering and during the client’s
 * initial render, ensuring the markup matches and avoiding hydration mismatches.
 *
 * Once hydration completes, the style tag is removed automatically.
 */
export function PreHydrationStyle(props: PreHydrationStyleProps) {
  const [hydrated, setHydrated] = useState(false)

  useEffect(() => {
    setHydrated(true)
  }, [])

  if (hydrated) return null

  return <style {...props} />
}
