import React, { use, useCallback, useMemo, useRef, useState } from 'react';
import type { NativeSyntheticEvent } from 'react-native';
import { Platform } from 'react-native';

import GestureHandlerButton, {
  type ButtonEvent,
} from '../../../components/GestureHandlerButton';
import { getTVProps } from '../../../components/utils';
import { getNextHandlerTag } from '../../../handlers/getNextHandlerTag';
import {
  isKeyboardDismissingTap,
  JSResponderContext,
  updateResponderEventValue,
} from '../../scrollViewInterop';
import type { AnimationDuration, TouchableProps } from './TouchableProps';

const isAndroid = Platform.OS === 'android';
const TRANSPARENT_RIPPLE = { rippleColor: 'transparent' as const };
const DEFAULT_IN_DURATION_MS = 50;
const DEFAULT_OUT_DURATION_MS = 100;

// Clamp user-supplied durations to finite, non-negative milliseconds.
// Negative, NaN, or Infinity values would produce invalid CSS transitions
// on web and negative setTimeout delays in branch 3 of the press-out path.
function sanitizeDuration(value: number): number {
  return Number.isFinite(value) && value >= 0 ? value : 0;
}

function resolveAnimationDuration(value: AnimationDuration | undefined) {
  if (value === undefined) {
    return {
      tapAnimationInDuration: DEFAULT_IN_DURATION_MS,
      tapAnimationOutDuration: DEFAULT_OUT_DURATION_MS,
      longPressAnimationOutDuration: DEFAULT_OUT_DURATION_MS,
      hoverAnimationInDuration: DEFAULT_IN_DURATION_MS,
      hoverAnimationOutDuration: DEFAULT_OUT_DURATION_MS,
    };
  }

  if (typeof value === 'number') {
    const sanitized = sanitizeDuration(value);
    return {
      tapAnimationInDuration: sanitized,
      tapAnimationOutDuration: sanitized,
      longPressAnimationOutDuration: sanitized,
      hoverAnimationInDuration: sanitized,
      hoverAnimationOutDuration: sanitized,
    };
  }

  // The union guarantees variant 2 supplies top-level `in`/`out`, variant 3
  // supplies both category objects — so per-category fallback to base is
  // always defined for well-typed input; the 0 fallbacks here are unreachable.
  const baseIn = 'in' in value ? value.in : 0;
  const baseOut = 'out' in value ? value.out : 0;
  const tapOut = value.tap?.out ?? baseOut;

  return {
    tapAnimationInDuration: sanitizeDuration(value.tap?.in ?? baseIn),
    tapAnimationOutDuration: sanitizeDuration(tapOut),
    longPressAnimationOutDuration: sanitizeDuration(
      value.longPress?.out ?? tapOut
    ),
    hoverAnimationInDuration: sanitizeDuration(value.hover?.in ?? baseIn),
    hoverAnimationOutDuration: sanitizeDuration(value.hover?.out ?? baseOut),
  };
}

export const Touchable = (props: TouchableProps) => {
  const {
    underlayColor = 'transparent',
    defaultUnderlayOpacity = 0,
    activeUnderlayOpacity = 0.105,
    defaultOpacity = 1,
    animationDuration,
    androidRipple,
    delayLongPress = 600,
    onLongPress,
    onPress,
    onPressIn,
    onPressOut,
    onHoverIn,
    onHoverOut,
    children,
    disabled = false,
    cancelOnLeave = true,
    ref,
    ...rest
  } = props;
  const [handlerTag] = useState(() => getNextHandlerTag());

  const resolvedDurations = resolveAnimationDuration(animationDuration);
  const resolvedDelayLongPress = sanitizeDuration(delayLongPress);

  const shouldUseNativeRipple = isAndroid && androidRipple !== undefined;

  const jsResponderContext = use(JSResponderContext);
  const dropKeyboardTapRef = useRef<boolean | null>(null);

  // The button handles presses natively, without a NativeDetector, so it has
  // to mark its responder events as RNGH-handled itself — otherwise a tap on
  // it looks unhandled to an enclosing ScrollView in
  // keyboardShouldPersistTaps='handled' mode and dismisses the keyboard.
  const handleStartShouldSetResponderCapture = useCallback(() => {
    if (!disabled) {
      updateResponderEventValue(jsResponderContext, true);
    }

    return false;
  }, [disabled, jsResponderContext]);

  const internalOnPress = useCallback(
    (e: NativeSyntheticEvent<ButtonEvent>) => {
      if (dropKeyboardTapRef.current) {
        return;
      }

      onPress?.(e.nativeEvent);
    },
    [onPress]
  );

  const internalOnPressIn = useCallback(
    (e: NativeSyntheticEvent<ButtonEvent>) => {
      // PressIn opens every press sequence; capture the verdict once per
      // sequence so a re-entry PressIn (with cancelOnLeave={false}) doesn't
      // overwrite it after the keyboard is already dismissed.
      dropKeyboardTapRef.current ??=
        isKeyboardDismissingTap(jsResponderContext);

      if (dropKeyboardTapRef.current) {
        return;
      }

      onPressIn?.(e.nativeEvent);
    },
    [jsResponderContext, onPressIn]
  );

  const internalOnPressOut = useCallback(
    (e: NativeSyntheticEvent<ButtonEvent>) => {
      if (dropKeyboardTapRef.current) {
        return;
      }

      onPressOut?.(e.nativeEvent);
    },
    [onPressOut]
  );

  const internalOnLongPress = useCallback(
    (e: NativeSyntheticEvent<ButtonEvent>) => {
      if (dropKeyboardTapRef.current) {
        return;
      }

      onLongPress?.(e.nativeEvent);
    },
    [onLongPress]
  );

  // Left undefined when the corresponding prop is absent so web can skip
  // building a hover payload nobody consumes — it costs a synchronous layout
  // read per pointer enter/leave. The native platforms emit either way.
  const internalOnHoverIn = useMemo(
    () =>
      onHoverIn
        ? (e: NativeSyntheticEvent<ButtonEvent>) => onHoverIn(e.nativeEvent)
        : undefined,
    [onHoverIn]
  );

  const internalOnHoverOut = useMemo(
    () =>
      onHoverOut
        ? (e: NativeSyntheticEvent<ButtonEvent>) => onHoverOut(e.nativeEvent)
        : undefined,
    [onHoverOut]
  );

  // InteractionFinished is dispatched after the terminal PressOut/Press
  // events, so resetting synchronously here is safe.
  const internalOnInteractionFinished = useCallback(() => {
    dropKeyboardTapRef.current = null;
  }, []);

  const rippleProps = shouldUseNativeRipple
    ? {
        rippleColor: androidRipple?.color,
        rippleRadius: androidRipple?.radius,
        borderless: androidRipple?.borderless,
        foreground: androidRipple?.foreground,
      }
    : TRANSPARENT_RIPPLE;

  const tvProps = getTVProps(rest);

  const hitSlop =
    typeof props.hitSlop === 'number'
      ? {
          top: props.hitSlop,
          left: props.hitSlop,
          bottom: props.hitSlop,
          right: props.hitSlop,
        }
      : props.hitSlop;

  return (
    <GestureHandlerButton
      {...rest}
      {...tvProps}
      {...rippleProps}
      {...resolvedDurations}
      ref={ref ?? null}
      enabled={!disabled}
      onStartShouldSetResponderCapture={handleStartShouldSetResponderCapture}
      moduleId={globalThis._RNGH_MODULE_ID}
      handlerTag={handlerTag}
      cancelOnLeave={cancelOnLeave}
      gestureTestID={props.testID}
      gestureHitSlop={hitSlop}
      defaultOpacity={defaultOpacity}
      defaultUnderlayOpacity={defaultUnderlayOpacity}
      activeUnderlayOpacity={activeUnderlayOpacity}
      underlayColor={underlayColor}
      longPressDuration={resolvedDelayLongPress}
      hasLongPressHandler={onLongPress !== undefined}
      onButtonPress={internalOnPress}
      onButtonPressIn={internalOnPressIn}
      onButtonPressOut={internalOnPressOut}
      onButtonLongPress={internalOnLongPress}
      onButtonHoverIn={internalOnHoverIn}
      onButtonHoverOut={internalOnHoverOut}
      onButtonInteractionFinished={internalOnInteractionFinished}>
      {children}
    </GestureHandlerButton>
  );
};
