import type { Ref } from 'react'
import { objectKeys } from 'ts-extras'

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

import { useMapWithInit } from './useMapWithInit.ts'
import { useRefWithInit } from './useRefWithInit.ts'
import { useStyleObserver } from './useStyleObserver.ts'

export type PassedValues<C extends GDSComponent> = {
  [K in keyof ComponentCSSPropsConfig<C>]?: CSSPropType<ComponentCSSPropsConfig<C>[K]> | undefined
}

export type PropValues<P extends PassedValues<GDSComponent>> = {
  [K in keyof P]: NonNullable<P[K]>
}

/**
 * Hook to observe and track CSS prop values from the DOM. Returns a callback ref, a map of prop
 * values that updates when CSS properties change, and the array of CSS props.
 */
export function useCSSProps<C extends GDSComponent, P extends PassedValues<C>>(
  component: C,
  passedValues: P,
  options: {
    ref?: Ref<HTMLElement> | undefined
    disabled?: boolean | undefined
  } = {},
) {
  const { ref, disabled = false } = options

  const cssProps = useRefWithInit(() =>
    objectKeys(passedValues).map((propName) => component.getCSSPropByName(propName)),
  ).current

  const propValues = useMapWithInit(() =>
    cssProps.map((cssProp) => {
      // Using the initial value for missing passed values could be wrong because it can be overridden in CSS, but it's just for the first render
      const passedValue = passedValues[cssProp.name]
      const initialValue = passedValue !== undefined ? passedValue : cssProp.initialValue
      return [cssProp.name, initialValue]
    }),
  )

  const cssProperties = useRefWithInit(() => cssProps.map((cssProp) => cssProp.cssProperty)).current

  const callbackRef = useStyleObserver(
    cssProperties,
    (records) => {
      for (const record of records) {
        const prop = cssProps.find((prop) => prop.cssProperty === record.property)!
        const value = parseCSSPropValue(prop, record.value)
        if (propValues.get(prop.name) !== value) {
          propValues.set(prop.name, value)
        }
      }
    },
    [],
    { ...(ref !== undefined && { ref }), immediate: true, disabled },
  )

  return [callbackRef, Object.fromEntries(propValues) as PropValues<P>, cssProps] as const
}
