import {
  Children,
  type ComponentType,
  createContext,
  type ForwardedRef,
  isValidElement,
  type PropsWithChildren,
  type RefObject,
  useContext,
  useCallback,
  useMemo,
  useRef,
} from 'react'
import {
  type AccessibilityProps,
  StyleSheet,
  TextInput,
  type TextInputProps,
  type TextStyle,
  View,
  type ViewStyle,
} from 'react-native'

import {HITSLOP_20} from '#/lib/constants'
import {mergeRefs} from '#/lib/merge-refs'
import {
  android,
  applyFonts,
  atoms as a,
  platform,
  type TextStyleProp,
  tokens,
  useAlf,
  useTheme,
  utils,
  web,
} from '#/alf'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {type Props as SVGIconProps} from '#/components/icons/common'
import {Text} from '#/components/Typography'
import {IS_WEB} from '#/env'

function getTabbableElements(container: HTMLElement) {
  const nodes: HTMLElement[] = []
  const walker = document.createTreeWalker(
    container,
    NodeFilter.SHOW_ELEMENT,
    {
      acceptNode: node => {
        if (!(node instanceof HTMLElement)) return NodeFilter.FILTER_SKIP
        if (
          node.tagName === 'INPUT' &&
          (node as HTMLInputElement).type === 'hidden'
        ) {
          return NodeFilter.FILTER_SKIP
        }
        if (node.disabled || node.hidden) return NodeFilter.FILTER_SKIP
        return node.tabIndex >= 0
          ? NodeFilter.FILTER_ACCEPT
          : NodeFilter.FILTER_SKIP
      },
    },
  )
  while (walker.nextNode()) nodes.push(walker.currentNode as HTMLElement)
  return nodes
}

function focusTabSibling(from: HTMLElement, reverse: boolean) {
  const root =
    (from.closest('[role="dialog"]') as HTMLElement | null) ??
    document.body
  const tabbables = getTabbableElements(root)
  const active = document.activeElement as HTMLElement | null
  let index = active ? tabbables.indexOf(active) : -1
  if (index === -1) index = tabbables.indexOf(from)
  if (index === -1) return
  const nextIndex = reverse
    ? (index - 1 + tabbables.length) % tabbables.length
    : (index + 1) % tabbables.length
  tabbables[nextIndex]?.focus()
}

const Context = createContext<{
  inputRef: RefObject<TextInput | null> | null
  isInvalid: boolean
  hovered: boolean
  onHoverIn: () => void
  onHoverOut: () => void
  focused: boolean
  onFocus: () => void
  onBlur: () => void
}>({
  inputRef: null,
  isInvalid: false,
  hovered: false,
  onHoverIn: () => {},
  onHoverOut: () => {},
  focused: false,
  onFocus: () => {},
  onBlur: () => {},
})
Context.displayName = 'TextFieldContext'

export type RootProps = PropsWithChildren<{isInvalid?: boolean} & TextStyleProp>

export function Root({children, isInvalid = false, style}: RootProps) {
  const inputRef = useRef<TextInput>(null)
  const {
    state: hovered,
    onIn: onHoverIn,
    onOut: onHoverOut,
  } = useInteractionState()
  const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()

  const context = useMemo(
    () => ({
      inputRef,
      hovered,
      onHoverIn,
      onHoverOut,
      focused,
      onFocus,
      onBlur,
      isInvalid,
    }),
    [
      inputRef,
      hovered,
      onHoverIn,
      onHoverOut,
      focused,
      onFocus,
      onBlur,
      isInvalid,
    ],
  )

  // Check if any child has multiline prop
  const hasMultiline = useMemo(() => {
    let found = false
    Children.forEach(children, child => {
      if (
        isValidElement(child) &&
        (child.props as {multiline?: boolean})?.multiline
      ) {
        found = true
      }
    })
    return found
  }, [children])

  return (
    <Context.Provider value={context}>
      <View
        style={[
          a.flex_row,
          a.align_center,
          a.relative,
          a.w_full,
          !(hasMultiline && IS_WEB) && a.px_md,
          style,
        ]}
        {...web({
          onClick: () => inputRef.current?.focus(),
          onMouseOver: onHoverIn,
          onMouseOut: onHoverOut,
        })}>
        {children}
      </View>
    </Context.Provider>
  )
}

export function useSharedInputStyles() {
  const t = useTheme()
  return useMemo(() => {
    const hover: ViewStyle[] = [
      {
        borderColor: t.palette.contrast_100,
      },
    ]
    const focus: ViewStyle[] = [
      {
        backgroundColor: t.palette.primary_25,
        borderColor: t.palette.primary_500,
      },
    ]
    const error: ViewStyle[] = [
      {
        backgroundColor: t.palette.negative_25,
        borderColor: t.palette.negative_300,
      },
    ]
    const errorHover: ViewStyle[] = [
      {
        backgroundColor: t.palette.negative_25,
        borderColor: t.palette.negative_500,
      },
    ]

    return {
      chromeHover: StyleSheet.flatten(hover),
      chromeFocus: StyleSheet.flatten(focus),
      chromeError: StyleSheet.flatten(error),
      chromeErrorHover: StyleSheet.flatten(errorHover),
    }
  }, [t])
}

export type InputProps = Omit<
  TextInputProps,
  'value' | 'onChangeText' | 'placeholder'
> & {
  label: string
  /**
   * @deprecated Controlled inputs are *strongly* discouraged. Use `defaultValue` instead where possible.
   *
   * See https://github.com/facebook/react-native-website/pull/4247
   *
   * Note: This guidance no longer applies once we migrate to the New Architecture!
   */
  value?: string
  onChangeText?: (value: string) => void
  isInvalid?: boolean
  inputRef?: RefObject<TextInput | null> | ForwardedRef<TextInput>
  /**
   * Note: this currently falls back to the label if not specified. However,
   * most new designs have no placeholder. We should eventually remove this fallback
   * behaviour, but for now just pass `null` if you want no placeholder -sfn
   */
  placeholder?: string | null | undefined
}

export function createInput(Component: typeof TextInput) {
  return function Input({
    label,
    placeholder,
    value,
    onChangeText,
    onFocus,
    onBlur,
    isInvalid,
    inputRef,
    style,
    ...rest
  }: InputProps) {
    const t = useTheme()
    const {fonts} = useAlf()
    const ctx = useContext(Context)
    const withinRoot = Boolean(ctx.inputRef)

    const {chromeHover, chromeFocus, chromeError, chromeErrorHover} =
      useSharedInputStyles()

    if (!withinRoot) {
      return (
        <Root isInvalid={isInvalid}>
          <Input
            label={label}
            placeholder={placeholder}
            value={value}
            onChangeText={onChangeText}
            isInvalid={isInvalid}
            {...rest}
          />
        </Root>
      )
    }

    const refs = mergeRefs([ctx.inputRef, inputRef!].filter(Boolean))

    const tabListenerRef = useRef<{
      el: HTMLElement
      handler: (e: KeyboardEvent) => void
    } | null>(null)

    const attachTabHandler = useCallback(
      (node: TextInput | null) => {
        if (tabListenerRef.current) {
          tabListenerRef.current.el.removeEventListener(
            'keydown',
            tabListenerRef.current.handler,
          )
          tabListenerRef.current = null
        }
        if (!IS_WEB || !rest.multiline || !node) return

        const el = node as unknown as HTMLElement
        const handler = (e: KeyboardEvent) => {
          if (e.key !== 'Tab') return
          e.preventDefault()
          focusTabSibling(el, e.shiftKey)
        }
        el.addEventListener('keydown', handler)
        tabListenerRef.current = {el, handler}
      },
      [rest.multiline],
    )

    const inputRefs = mergeRefs([refs, attachTabHandler])

    const flattened = StyleSheet.flatten([
      a.relative,
      a.z_20,
      a.flex_1,
      a.text_md,
      t.atoms.text,
      a.px_xs,
      {
        // paddingVertical doesn't work w/multiline - esb
        lineHeight: a.text_md.fontSize * 1.2,
        textAlignVertical: rest.multiline ? 'top' : undefined,
        minHeight: rest.multiline ? 80 : undefined,
        minWidth: 0,
        paddingTop: 13,
        paddingBottom: 13,
      },
      android({
        paddingTop: 8,
        paddingBottom: 9,
      }),
      /*
       * Margins are needed here to avoid autofill background overlapping the
       * top and bottom borders - esb
       */
      web({
        paddingTop: 11,
        paddingBottom: 11,
        marginTop: 2,
        marginBottom: 2,
      }),
      rest.multiline &&
        web({
          resize: 'vertical',
          fieldSizing: 'content',
          paddingLeft: 16,
          paddingRight: 16,
        }),
      style,
    ])

    applyFonts(flattened, fonts.family)

    // should always be defined on `typography`
    // @ts-ignore
    if (flattened.fontSize) {
      // @ts-ignore
      flattened.fontSize = Math.round(
        // @ts-ignore
        flattened.fontSize * fonts.scaleMultiplier,
      )
    }

    return (
      <>
        <Component
          accessibilityHint={undefined}
          hitSlop={HITSLOP_20}
          selectionColor={utils.alpha(t.palette.primary_500, 0.4)}
          cursorColor={t.palette.primary_500}
          selectionHandleColor={t.palette.primary_500}
          {...rest}
          accessibilityLabel={label}
          ref={inputRefs}
          value={value}
          onChangeText={onChangeText}
          onFocus={e => {
            ctx.onFocus()
            onFocus?.(e)
          }}
          onBlur={e => {
            ctx.onBlur()
            onBlur?.(e)
          }}
          placeholder={placeholder === null ? undefined : placeholder || label}
          placeholderTextColor={t.palette.contrast_500}
          keyboardAppearance={t.name === 'light' ? 'light' : 'dark'}
          style={flattened}
        />

        <View
          style={[
            a.z_10,
            a.absolute,
            a.inset_0,
            {borderRadius: 10},
            t.atoms.bg_contrast_50,
            {borderColor: 'transparent', borderWidth: 1},
            ctx.hovered ? chromeHover : {},
            ctx.focused ? chromeFocus : {},
            ctx.isInvalid || isInvalid ? chromeError : {},
            (ctx.isInvalid || isInvalid) && (ctx.hovered || ctx.focused)
              ? chromeErrorHover
              : {},
          ]}
        />
      </>
    )
  }
}

export const Input = createInput(TextInput)

export function LabelText({
  nativeID,
  children,
}: PropsWithChildren<{nativeID?: string}>) {
  const t = useTheme()
  return (
    <Text
      nativeID={nativeID}
      style={[a.text_sm, a.font_medium, t.atoms.text_contrast_medium, a.mb_sm]}>
      {children}
    </Text>
  )
}

export function Icon({icon: Comp}: {icon: ComponentType<SVGIconProps>}) {
  const t = useTheme()
  const ctx = useContext(Context)
  const {hover, focus, errorHover, errorFocus} = useMemo(() => {
    const hover: TextStyle[] = [
      {
        color: t.palette.contrast_800,
      },
    ]
    const focus: TextStyle[] = [
      {
        color: t.palette.primary_500,
      },
    ]
    const errorHover: TextStyle[] = [
      {
        color: t.palette.negative_500,
      },
    ]
    const errorFocus: TextStyle[] = [
      {
        color: t.palette.negative_500,
      },
    ]

    return {
      hover,
      focus,
      errorHover,
      errorFocus,
    }
  }, [t])

  return (
    <View style={[a.z_20, a.pr_xs]}>
      <Comp
        size="md"
        style={[
          {color: t.palette.contrast_500, pointerEvents: 'none', flexShrink: 0},
          ctx.hovered ? hover : {},
          ctx.focused ? focus : {},
          ctx.isInvalid && ctx.hovered ? errorHover : {},
          ctx.isInvalid && ctx.focused ? errorFocus : {},
        ]}
      />
    </View>
  )
}

export function SuffixText({
  children,
  label,
  accessibilityHint,
  style,
}: PropsWithChildren<
  TextStyleProp & {
    label: string
    accessibilityHint?: AccessibilityProps['accessibilityHint']
  }
>) {
  const t = useTheme()
  const ctx = useContext(Context)
  return (
    <Text
      accessibilityLabel={label}
      accessibilityHint={accessibilityHint}
      numberOfLines={1}
      style={[
        a.z_20,
        a.pr_sm,
        a.text_md,
        t.atoms.text_contrast_medium,
        a.pointer_events_none,
        web([{marginTop: -2}, a.leading_snug]),
        (ctx.hovered || ctx.focused) && {color: t.palette.contrast_800},
        style,
      ]}>
      {children}
    </Text>
  )
}

export function GhostText({
  children,
  value,
}: {
  children: string
  value: string
}) {
  const t = useTheme()
  // eslint-disable-next-line bsky-internal/avoid-unwrapped-text
  return (
    <View
      style={[
        a.pointer_events_none,
        a.absolute,
        a.z_10,
        {
          paddingLeft: platform({
            native:
              // input padding
              tokens.space.md +
              // icon
              tokens.space.xl +
              // icon padding
              tokens.space.xs +
              // text input padding
              tokens.space.xs,
            web:
              // icon
              tokens.space.xl +
              // icon padding
              tokens.space.xs +
              // text input padding
              tokens.space.xs,
          }),
        },
        web(a.pr_md),
        a.overflow_hidden,
        a.max_w_full,
      ]}
      aria-hidden={true}
      accessibilityElementsHidden
      importantForAccessibility="no-hide-descendants">
      <Text
        style={[
          {color: 'transparent'},
          a.text_md,
          {lineHeight: a.text_md.fontSize * 1.1875},
          a.w_full,
        ]}
        numberOfLines={1}>
        {children}
        <Text
          style={[
            t.atoms.text_contrast_low,
            a.text_md,
            {lineHeight: a.text_md.fontSize * 1.1875},
          ]}>
          {value}
        </Text>
      </Text>
    </View>
  )
}
