import { useEffect, useMemo, useRef, useState, type KeyboardEvent, type Ref } from 'react'
import { useMergedRefs } from '@base-ui/utils/useMergedRefs'

import {
  bigIntToNumber,
  formatBigInt,
  numberToBigInt,
  parseBigInt,
  type BigIntFormatOptions,
} from '@graphprotocol/gds-utils'

import type { InputProps } from '../components/Input.tsx'
import { useForceRerender } from './useForceRerender.ts'
import { usePrevious } from './usePrevious.ts'

export interface UseNumberInputOptions<
  UseBigInt extends boolean = false,
  T = UseBigInt extends true ? bigint : number,
> {
  ref?: Ref<HTMLInputElement> | undefined
  /** @default false */
  useBigInt?: UseBigInt | undefined
  /** @default 18n */
  precision?: bigint | undefined
  /** @default null */
  defaultValue?: T | null | undefined
  value?: T | null | undefined
  onChange?: ((value: T | null) => void) | undefined
  /** @default false */
  required?: boolean | undefined
  /** @default null */
  min?: T | null | undefined
  /** @default null */
  max?: T | null | undefined
  /**
   * @default {
   *   minimumFractionDigits: 0
   *   maximumFractionDigits: 20
   *   trailingZeroDisplay: 'auto'
   * }
   */
  format?: BigIntFormatOptions | undefined
}

export type UseNumberInputError = 'NOT_A_NUMBER' | 'EMPTY' | 'BELOW_MIN' | 'ABOVE_MAX'

export interface UseNumberInputReturnedObject<T = number | bigint> {
  /**
   * `null` means the input is either empty or invalid (or both, if `required` is true); check
   * `error` for more info.
   */
  value: T | null
  setValue: (value: T | null) => void
  error: UseNumberInputError | null
  nonValidatedValue: bigint | null
  nearestValidValue: T
  props: {
    ref: Ref<HTMLInputElement>
    type: 'number'
    value: InputProps['value']
    onKeyDown: InputProps['onKeyDown']
    onChange: InputProps['onChange']
    onBlur: InputProps['onBlur']
    required?: InputProps['required'] | undefined
    min?: InputProps['min'] | undefined
    max?: InputProps['max'] | undefined
    status?: InputProps['status'] | undefined
    message?: InputProps['message'] | undefined
  }
}

export function useNumberInput<
  UseBigInt extends boolean = false,
  T = UseBigInt extends true ? bigint : number,
>({
  ref: passedRef,
  useBigInt = false as UseBigInt,
  precision = 18n,
  defaultValue = null,
  value: controlledValue,
  onChange: controlledOnChange,
  required = false,
  min: passedMin = null,
  max: passedMax = null,
  format = {},
}: UseNumberInputOptions<UseBigInt> = {}): UseNumberInputReturnedObject<T> {
  if (passedMin !== null && passedMax !== null && passedMin > passedMax) {
    throw new Error('[useNumberInput] `min` cannot be greater than `max`')
  }

  const inputRef = useRef<HTMLInputElement>(null)
  const inputPassedRef = useMergedRefs(inputRef, passedRef)

  const normalizeValue = <TT extends number | bigint | null>(
    value: TT,
  ): TT extends null ? null : bigint => {
    if (typeof value === 'number') {
      return numberToBigInt(value, { precision }) as TT extends null ? null : bigint
    } else if (typeof value === 'bigint') {
      return value as TT extends null ? null : typeof value
    } else {
      return null as TT extends null ? null : bigint
    }
  }

  const denormalizeValue = <TT extends bigint | null>(value: TT): TT extends null ? null : T => {
    if (value === null) {
      return null as TT extends null ? null : T
    } else if (useBigInt) {
      return value as TT extends null ? null : T
    } else {
      return bigIntToNumber(value, { precision }) as TT extends null ? null : T
    }
  }

  const formatValue = (value: bigint | null, overrides?: BigIntFormatOptions) => {
    if (value === null) return ''
    return formatBigInt(value, { precision, ...format, ...overrides })
  }

  const defaultMin = useBigInt ? null : Number.MIN_SAFE_INTEGER
  const defaultMax = useBigInt ? null : Number.MAX_SAFE_INTEGER
  const min = normalizeValue(passedMin ?? defaultMin)
  const max = normalizeValue(passedMax ?? defaultMax)
  const allowNegative = min === null || min < 0n
  const allowDecimals = precision > 0n

  const validateValue = (value: bigint | null): UseNumberInputError | null => {
    if (value === null) {
      if (required) {
        return 'EMPTY'
      }
      return null
    }
    if (min !== null && value < min) {
      return 'BELOW_MIN'
    }
    if (max !== null && value > max) {
      return 'ABOVE_MAX'
    }
    return null
  }

  const derivedFromProps = (() => {
    const normalizedValue = normalizeValue(
      controlledValue !== undefined ? controlledValue : defaultValue,
    )
    const error = validateValue(normalizedValue)
    const validatedValue = !error ? normalizedValue : null
    return {
      normalizedValue,
      error,
      validatedValue,
    }
  })()

  const [typing, setTyping] = useState(false)
  const [inputValue, setInputValue] = useState(() => formatValue(derivedFromProps.normalizedValue))
  const [validatedValue, setValidatedValue] = useState<bigint | null>(
    derivedFromProps.validatedValue,
  )
  const [error, setError] = useState<UseNumberInputError | null>(
    derivedFromProps.error !== 'EMPTY' ? derivedFromProps.error : null,
  )
  const [errorOnBlur, setErrorOnBlur] = useState<UseNumberInputError | null>(error)
  const [nonValidatedValue, setNonValidatedValue] = useState<bigint | null>(
    derivedFromProps.normalizedValue,
  )
  let nearestValidValue = nonValidatedValue ?? 0n
  if (min !== null && (nonValidatedValue ?? 0n) < min) {
    nearestValidValue = min
  }
  if (max !== null && (nonValidatedValue ?? 0n) > max) {
    nearestValidValue = max
  }

  const setValue = (
    newValue: number | bigint | null,
    {
      updateInputValue = true,
      callOnChange = false,
    }: {
      updateInputValue?: boolean
      callOnChange?: boolean
    } = {},
  ) => {
    const normalizedValue = normalizeValue(newValue)
    const formattedValue = formatValue(normalizedValue)
    const newError = validateValue(normalizedValue)

    if (updateInputValue) setInputValue(formattedValue)
    setNonValidatedValue(normalizedValue)

    if (normalizedValue === null || newError) {
      setValidatedValue(null)
      if (callOnChange) controlledOnChange?.(null)
      setError(newError)
      return
    }

    // The passed value may have more decimals than we allow, so we parse the formatted value to truncate those
    const parsedValue = parseBigInt(formattedValue, { precision })
    setValidatedValue(parsedValue)
    if (callOnChange) {
      ;(controlledOnChange as ((value: T | null) => void) | undefined)?.(
        denormalizeValue(parsedValue),
      )
    }
    setError(null)
  }

  // Update internal values when a prop changes externally that might affect the value or the error
  const previousControlledValue = usePrevious(controlledValue)
  const previousRequired = usePrevious(required)
  const previousMin = usePrevious(passedMin)
  const previousMax = usePrevious(passedMax)
  if (
    (previousControlledValue.hasChanged &&
      (controlledValue === undefined || normalizeValue(controlledValue) !== validatedValue)) ||
    previousRequired.hasChanged ||
    previousMin.hasChanged ||
    previousMax.hasChanged
  ) {
    if (previousControlledValue.hasChanged && controlledValue !== undefined) {
      setValue(controlledValue)
    } else {
      setValue(denormalizeValue(nonValidatedValue) as number | bigint | null)
    }
  }

  const getSignificantCaretPositionMap = (string: string) => {
    const significantCaretPositionMap: number[] = []
    let significantCaretPosition = 0
    let hasDecimalPoint = false
    let hasSignificantDigit = false
    significantCaretPositionMap.push(significantCaretPosition)
    for (let i = 0; i < string.length; i++) {
      const character = string[i]!
      if (character === '-' && significantCaretPosition === 0) {
        significantCaretPosition++
      } else if (character === '.' && !hasDecimalPoint) {
        significantCaretPosition++
        hasDecimalPoint = true
        // Handle typing `.5` (instead of `0.5`)
        if (string[i - 1] !== '0') {
          significantCaretPosition++
        }
      } else if (/\d/.test(character)) {
        if (character === '0' && !hasSignificantDigit) {
          if (string[i - 1] === '.' || string[i + 1] === '.') {
            significantCaretPosition++
            hasSignificantDigit = true
          }
        } else {
          significantCaretPosition++
          hasSignificantDigit = true
        }
      }
      significantCaretPositionMap.push(significantCaretPosition)
    }
    return significantCaretPositionMap
  }

  const getSignificantCaretPosition = (input: HTMLInputElement) => {
    const significantCaretPositionMap = getSignificantCaretPositionMap(input.value)
    return significantCaretPositionMap[
      input.selectionStart !== null && input.selectionStart < significantCaretPositionMap.length
        ? input.selectionStart
        : significantCaretPositionMap.length
    ]!
  }

  const setSignificantCaretPosition = (input: HTMLInputElement, position: number) => {
    const significantCaretPositionMap = getSignificantCaretPositionMap(input.value)
    let actualCaretPosition = significantCaretPositionMap.indexOf(position)
    if (actualCaretPosition === -1) {
      actualCaretPosition = input.value.length
    }
    input.setSelectionRange(actualCaretPosition, actualCaretPosition)
  }

  const previousSignificantCaretPosition = useRef<number | null>(null)
  const [forceRerender] = useForceRerender()
  const charsToRemove = useMemo(
    () => new RegExp(`[^\\d,${allowNegative ? '-' : ''}${allowDecimals ? '.' : ''}]`, 'g'),
    [allowNegative, allowDecimals],
  )

  const onKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
    const allowKeyPress =
      event.ctrlKey ||
      event.metaKey ||
      /\d/.test(event.key) ||
      (allowNegative && event.key === '-') ||
      (allowDecimals && event.key === '.') ||
      // TODO: Fix stuck caret when pressing Delete to the left of a comma
      // TODO: Fix caret sometimes not ending up in the correct position when pressing Delete to the left of the decimal point
      ['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Tab'].includes(
        event.key,
      )
    if (!allowKeyPress) {
      event.preventDefault()
    }
  }

  const onChange = (newInputValue: string) => {
    setTyping(true)

    if (inputRef.current) {
      previousSignificantCaretPosition.current = getSignificantCaretPosition(inputRef.current)
    }

    // Remove all characters except digits, commas, minus signs (if allowed), and decimal points (if allowed)
    let cleanedValue = newInputValue.replace(charsToRemove, '')

    // Remove minus signs that are not at the beginning of the string
    if (allowNegative) {
      cleanedValue = cleanedValue.replace(/(?!^)-/g, '')
    }

    // Remove decimal points except the first one
    if (allowDecimals) {
      const cleanedValueParts = cleanedValue.split('.')
      cleanedValue =
        cleanedValueParts.length > 1
          ? `${cleanedValueParts.shift()}.${cleanedValueParts.join('')}`
          : cleanedValueParts[0]!
    }

    setInputValue(cleanedValue)

    if (cleanedValue === '') {
      setValue(null, {
        updateInputValue: false,
        callOnChange: true,
      })
      return
    }

    const parsedValue = parseBigInt(cleanedValue, { precision })

    if (parsedValue === null) {
      setValidatedValue(null)
      controlledOnChange?.(null)
      setError('NOT_A_NUMBER')
      return
    }

    setValue(parsedValue, {
      updateInputValue: false,
      callOnChange: true,
    })

    // Don't format potentially intermediate values like `-0` (in case we're typing `-0.1`), `1.` (for `1.5`), or `1.50` (for `1.501`)
    // Those cases will be formatted on blur instead
    if (!/^-0+$/.test(cleanedValue) && !/\.((.*)0+)?$/.test(cleanedValue)) {
      setInputValue(
        formatValue(parsedValue, { minimumFractionDigits: 0, maximumFractionDigits: 20 }),
      )
    }

    // Ensure the caret position is updated correctly when pressing backspace on a comma
    forceRerender()
  }

  // Ensure the caret is in the correct position after the value changes
  useEffect(() => {
    if (!inputRef.current || previousSignificantCaretPosition.current === null) return
    const significantCaretPosition = getSignificantCaretPosition(inputRef.current)
    if (significantCaretPosition !== previousSignificantCaretPosition.current) {
      setSignificantCaretPosition(inputRef.current, previousSignificantCaretPosition.current)
    }
    previousSignificantCaretPosition.current = null
  })

  const onBlur = () => {
    setTyping(false)
    setErrorOnBlur(error)
    const parsedValue = parseBigInt(inputValue, { precision })
    if (parsedValue !== null) {
      setInputValue(formatValue(parsedValue))
    }
  }

  const status: InputProps['status'] = error ? 'error' : undefined
  let message: InputProps['message']
  if (error) {
    switch (error) {
      case 'NOT_A_NUMBER':
        message = 'Please enter a valid number.'
        break
      case 'EMPTY':
        message = 'This field is required.'
        break
      case 'BELOW_MIN':
        message = `This field's value cannot be less than ${formatValue(min)}.`
        break
      case 'ABOVE_MAX':
        message = `This field's value cannot be greater than ${formatValue(max)}.`
        break
    }
  }

  // Only show the error message when not typing, or while typing until the input is valid again
  if (typing && error !== errorOnBlur) {
    message = undefined
  }

  return {
    value: denormalizeValue(validatedValue),
    setValue: setValue as (value: T | null) => void,
    error,
    nonValidatedValue,
    nearestValidValue: denormalizeValue(nearestValidValue),
    props: {
      ref: inputPassedRef,
      type: 'number',
      value: inputValue,
      onKeyDown,
      onChange,
      onBlur,
      required,
      min: min !== null ? (bigIntToNumber(min, { precision }) ?? undefined) : undefined,
      max: max !== null ? (bigIntToNumber(max, { precision }) ?? undefined) : undefined,
      status,
      message,
    },
  }
}
