import {
  createContext,
  useCallback,
  useContext,
  useMemo,
  type ReactElement,
} from 'react'
import {
  Pressable,
  type AccessibilityProps,
  type GestureResponderEvent,
  type PressableProps,
  type StyleProp,
  type TextStyle,
  type ViewStyle,
} from 'react-native'

import {useInteractionState} from '@/hooks/use-interaction-state'
import {
  atoms as a,
  pressed as pressedFeedback,
  useTheme,
  type Palette,
} from '@/sprinkles'

import {Text, type TextProps} from './typography'

export type ButtonVariant = 'solid' | 'ghost'
export type ButtonColor =
  | 'primary'
  | 'secondary'
  | 'negative'
  | 'primary_subtle'
  | 'negative_subtle'
export type ButtonSize = 'tiny' | 'small' | 'large'
export type ButtonShape =
  | 'round'
  | 'square'
  | 'rectangular'
  | 'flush'
  | 'default'

export type ButtonState = {
  pressed: boolean
  disabled: boolean
}

type NonTextElements =
  | ReactElement
  | Iterable<ReactElement | null | undefined | boolean>

export type ButtonProps = Pick<
  PressableProps,
  | 'disabled'
  | 'onPress'
  | 'testID'
  | 'onLongPress'
  | 'hitSlop'
  | 'onPressIn'
  | 'onPressOut'
> &
  AccessibilityProps & {
    label: string
    variant?: ButtonVariant
    color?: ButtonColor
    size?: ButtonSize
    shape?: ButtonShape
    style?: StyleProp<ViewStyle>
    children?: NonTextElements
  }

type Theme = ReturnType<typeof useTheme>

type ButtonContextValue = {
  variant?: ButtonVariant
  color?: ButtonColor
  size: ButtonSize
  disabled: boolean
}

const ButtonContext = createContext<ButtonContextValue>({
  size: 'small',
  disabled: false,
})

export const useButtonContext = () => useContext(ButtonContext)

const PADDING_BY_SIZE: Record<ButtonSize, ViewStyle> = {
  large: {paddingVertical: 13, paddingHorizontal: 20, gap: 6},
  small: {paddingVertical: 9, paddingHorizontal: 14, gap: 4},
  tiny: {paddingVertical: 5, paddingHorizontal: 10, gap: 3},
}

const DIM_BY_SIZE: Record<ButtonSize, number> = {
  large: 54,
  small: 44,
  tiny: 34,
}

const RECT_RADIUS_BY_SIZE: Record<ButtonSize, number> = {
  large: 8,
  small: 6,
  tiny: 6,
}

// pad the sub-44pt sizes so every size×shape hits a 44pt effective
// target (mirrors Chip's compensation); tightly packed rows may overlap
// slop, which ios resolves to the nearest target
const HITSLOP_BY_SIZE: Record<ButtonSize, number | undefined> = {
  large: undefined,
  small: 4,
  tiny: 10,
}

type Shade = keyof Palette

const SOLID_BG_BY_COLOR: Record<
  ButtonColor,
  {base: Shade; pressed: Shade; disabled?: Shade}
> = {
  primary: {
    base: 'primary_500',
    pressed: 'primary_600',
    disabled: 'primary_200',
  },
  negative: {
    base: 'negative_500',
    pressed: 'negative_600',
    disabled: 'negative_200',
  },
  primary_subtle: {base: 'primary_50', pressed: 'primary_100'},
  negative_subtle: {base: 'negative_50', pressed: 'negative_100'},
  secondary: {base: 'contrast_50', pressed: 'contrast_100'},
}

const solidColorStyle = (
  color: ButtonColor | undefined,
  disabled: boolean,
  t: Theme,
): {base: ViewStyle; pressed?: ViewStyle} => {
  const shades = SOLID_BG_BY_COLOR[color ?? 'secondary']
  if (disabled && shades.disabled)
    return {base: {backgroundColor: t.palette[shades.disabled]}}
  return {
    base: {backgroundColor: t.palette[shades.base]},
    pressed: {backgroundColor: t.palette[shades.pressed]},
  }
}

const shapeStyle = (
  shape: ButtonShape,
  size: ButtonSize,
): ViewStyle[] => {
  switch (shape) {
    case 'round':
      return [
        a.rounded_full,
        {width: DIM_BY_SIZE[size], height: DIM_BY_SIZE[size]},
      ]
    case 'square':
      return [
        a.rounded_md,
        {width: DIM_BY_SIZE[size], height: DIM_BY_SIZE[size]},
      ]
    case 'rectangular':
      return [
        {borderRadius: RECT_RADIUS_BY_SIZE[size]},
        PADDING_BY_SIZE[size],
      ]
    case 'flush': {
      const {paddingVertical, gap} = PADDING_BY_SIZE[size]
      return [a.align_self_start, {paddingVertical, gap}]
    }
    case 'default':
    default:
      return [a.rounded_full, PADDING_BY_SIZE[size]]
  }
}

const LABEL_SIZE_STYLE: Record<ButtonSize, TextStyle[]> = {
  large: [a.text_md, a.leading_snug, a.font_normal],
  small: [a.text_sm, a.leading_snug, a.font_normal],
  tiny: [a.text_xs, a.leading_snug, a.font_medium],
}

const LABEL_COLOR_BY_COLOR: Record<
  ButtonColor,
  {onFill: Shade; ghost: Shade; disabled?: Shade}
> = {
  primary: {
    onFill: 'white',
    ghost: 'primary_700',
    disabled: 'primary_300',
  },
  negative: {
    onFill: 'white',
    ghost: 'negative_500',
    disabled: 'negative_300',
  },
  primary_subtle: {onFill: 'primary_700', ghost: 'primary_700'},
  negative_subtle: {onFill: 'negative_600', ghost: 'negative_600'},
  secondary: {onFill: 'contrast_950', ghost: 'contrast_950'},
}

const labelTextStyle = (
  color: ButtonColor | undefined,
  size: ButtonSize,
  variant: ButtonVariant | undefined,
  disabled: boolean,
  t: Theme,
): TextStyle[] => {
  const shades = LABEL_COLOR_BY_COLOR[color ?? 'secondary']
  const onFill = variant === 'solid' || variant === undefined
  const shade =
    disabled && shades.disabled
      ? shades.disabled
      : onFill
        ? shades.onFill
        : shades.ghost
  return [...LABEL_SIZE_STYLE[size], {color: t.palette[shade]}]
}

// renders text with the variant style of the enclosing Button; pass a
// style to layer intentional deviations on top
export const ButtonText = ({style, ...rest}: TextProps) => {
  const t = useTheme()
  const {color, size, variant, disabled} = useButtonContext()
  const textStyle = useMemo(
    () => labelTextStyle(color, size, variant, disabled, t),
    [t, color, size, variant, disabled],
  )
  return <Text style={[textStyle, style]} {...rest} />
}

export function Button({
  children,
  variant,
  color,
  size = 'small',
  shape = 'default',
  label,
  disabled,
  style,
  onPress,
  onPressIn,
  onPressOut,
  hitSlop,
  ...rest
}: ButtonProps) {
  const t = useTheme()
  const {state: pressed, onIn, onOut} = useInteractionState()

  const {baseStyles, pressedStyles} = useMemo(() => {
    const base: ViewStyle[] = []
    const pressedStyle: ViewStyle[] = []

    if (variant === 'solid' || variant === undefined) {
      const c = solidColorStyle(color, disabled ?? false, t)
      base.push(c.base)
      if (c.pressed) pressedStyle.push(c.pressed)
    } else {
      pressedStyle.push(pressedFeedback)
    }

    base.push(...shapeStyle(shape, size))

    if (disabled) base.push({opacity: 0.5})

    return {baseStyles: base, pressedStyles: pressedStyle}
  }, [t, variant, color, size, shape, disabled])

  const context = useMemo(
    () => ({variant, color, size, disabled: disabled ?? false}),
    [variant, color, size, disabled],
  )

  const handlePress = useCallback(
    (e: GestureResponderEvent) => {
      if (!disabled) onPress?.(e)
    },
    [disabled, onPress],
  )

  const handlePressIn = useCallback(
    (e: GestureResponderEvent) => {
      onIn()
      onPressIn?.(e)
    },
    [onIn, onPressIn],
  )

  const handlePressOut = useCallback(
    (e: GestureResponderEvent) => {
      onOut()
      onPressOut?.(e)
    },
    [onOut, onPressOut],
  )

  return (
    <Pressable
      accessible
      accessibilityRole="button"
      accessibilityLabel={label}
      disabled={disabled}
      hitSlop={hitSlop ?? HITSLOP_BY_SIZE[size]}
      onPress={handlePress}
      onPressIn={handlePressIn}
      onPressOut={handlePressOut}
      style={[
        a.flex_row,
        a.align_center,
        a.justify_center,
        a.curve_continuous,
        ...baseStyles,
        ...(pressed ? pressedStyles : []),
        style,
      ]}
      {...rest}
    >
      <ButtonContext.Provider value={context}>
        {children ?? <ButtonText>{label}</ButtonText>}
      </ButtonContext.Provider>
    </Pressable>
  )
}

Button.Text = ButtonText
