import type { Ref, RefCallback } from 'react'

import {
  getCSSPropRawValue,
  type ComponentCSSPropsConfig,
  type CSSPropsConfig,
  type CSSPropType,
  type GDSComponent,
} from '@graphprotocol/gds-css'

import { useCSSProps, type PassedValues, type PropValues } from './useCSSProps.ts'
import { useFirstRender } from './useFirstRender.ts'

type ExtractPolyfillValues<P extends CSSPropsConfig> = {
  [K in keyof P as P[K]['type'] extends 'values' ? K : never]: CSSPropType<P[K]> | undefined
}

type PassedPolyfillValues<C extends GDSComponent> = keyof ExtractPolyfillValues<
  ComponentCSSPropsConfig<C>
> extends never
  ? never
  : ExtractPolyfillValues<ComponentCSSPropsConfig<C>>

type ReturnedPropValues<C extends GDSComponent, R> = R extends true
  ? PropValues<PassedPolyfillValues<C>>
  : R extends PassedValues<C>
    ? PropValues<R>
    : never

/**
 * This hook should be called by components with CSS props of type `values`, for the `prop-*`
 * utility classes to work in browsers that don't support style queries (Firefox).
 */
export function useCSSPropsPolyfill<C extends GDSComponent>(
  component: C,
  passedValues: PassedPolyfillValues<C>,
  options?: {
    ref?: Ref<HTMLElement> | undefined
  },
): readonly [RefCallback<HTMLElement>, Record<string, string>]

export function useCSSPropsPolyfill<C extends GDSComponent, R extends PassedValues<C> | true>(
  component: C,
  passedValues: PassedPolyfillValues<C>,
  options: {
    ref?: Ref<HTMLElement> | undefined
    returnPropValues: R
  },
): readonly [RefCallback<HTMLElement>, Record<string, string>, ReturnedPropValues<C, R>]

export function useCSSPropsPolyfill<
  C extends GDSComponent,
  R extends PassedValues<C> | true | undefined = undefined,
>(
  component: C,
  passedValues: PassedPolyfillValues<C>,
  options?: {
    ref?: Ref<HTMLElement> | undefined
    returnPropValues?: R
  },
):
  | readonly [RefCallback<HTMLElement>, Record<string, string>]
  | readonly [RefCallback<HTMLElement>, Record<string, string>, ReturnedPropValues<C, R>] {
  const { ref, returnPropValues } = options ?? {}
  const polyfillEnabled = !getStyleQueriesSupported()

  // Merge `passedValues` with `returnPropValues` if needed
  const allPassedValues = (() => {
    if (!returnPropValues) return passedValues
    if (returnPropValues === true) return passedValues
    const passed = passedValues as Record<string, unknown>
    const merged = { ...passed }
    for (const key of Object.keys(returnPropValues)) {
      const value = returnPropValues[key]
      if (key in passedValues && passed[key] !== value) {
        throw new Error(
          `[useCSSPropsPolyfill] Conflicting values for prop \`${key}\`: ${String(passed[key])} vs ${value}`,
        )
      }
      merged[key] = value
    }
    return merged as typeof passedValues
  })()

  const [callbackRef, propValues, cssProps] = useCSSProps(component, allPassedValues, {
    ref,
    disabled: !polyfillEnabled && !returnPropValues,
  })

  /**
   * Always include the polyfill attributes on the first render (even if the polyfill is disabled)
   * to prevent hydration errors. It would be nice to optimize for the case where the polyfill is
   * not needed instead (by always excluding the attributes on the first render, thereby not having
   * to immediately rerender in browsers that support style queries); unfortunately, that would
   * cause a flash of missing styles in browsers that need the polyfill.
   */
  const firstRender = useFirstRender({ immediatelyRerender: !polyfillEnabled })
  const polyfillAttributes =
    polyfillEnabled || firstRender
      ? Object.fromEntries(
          cssProps
            .filter((prop) => prop.name in passedValues)
            .map((prop) => {
              const value = propValues[prop.name as keyof typeof propValues]
              const rawValue = getCSSPropRawValue(prop, value)
              return [prop.polyfillAttribute, rawValue]
            }),
        )
      : {}

  if (!returnPropValues) {
    return [callbackRef, polyfillAttributes] as const
  }

  const returnedPropValues = (
    returnPropValues === true
      ? propValues
      : Object.fromEntries(Object.entries(propValues).filter(([key]) => key in returnPropValues))
  ) as ReturnedPropValues<C, R>

  return [callbackRef, polyfillAttributes, returnedPropValues] as const
}

let privateStyleQueriesSupported: boolean | null = null

function getStyleQueriesSupported(): boolean {
  if (typeof window === 'undefined' || typeof document === 'undefined') return false
  if (privateStyleQueriesSupported === null) {
    privateStyleQueriesSupported =
      window.getComputedStyle(document.body).getPropertyValue('--gds-style-queries-test') ===
      'success'
  }
  return privateStyleQueriesSupported
}
