import React, {memo, useCallback, forwardRef, useRef, useEffect} from 'react';
import {
  NativeSyntheticEvent,
  FocusEvent,
  BlurEvent,
  TextInputKeyPressEventData,
  TextInputSelectionChangeEventData,
} from 'react-native';
import {spyIfWritingTaggedUser} from 'tag-users';
import debounce from 'lodash.debounce';
import {InputTags, InputTagsProps, TextInputType} from '@components';
import {useInputValue, useMentions} from '@modules/chats/contexts';

import {detectDeletedTags, shouldHandleTagDeletionManually} from './utils';

/**
 * Finds the character that was just inserted and its position.
 * Returns null if no single character was added (deletion or no change).
 */
const findLastTypedChar = (
  oldText: string,
  newText: string,
): {char: string; pos: number} | null => {
  if (newText.length !== oldText.length + 1) {
    return null;
  }
  for (let i = 0; i < oldText.length; i++) {
    if (oldText[i] !== newText[i]) {
      return {char: newText[i], pos: i};
    }
  }
  return {char: newText[newText.length - 1], pos: newText.length - 1};
};

export interface TagUsersParams
  extends Omit<
    InputTagsProps,
    'value' | 'taggedUsers' | 'onChangeText' | 'highlightedSpans'
  > {
  onDialogWillOpen?: () => void;
}

const SET_VALUE_THROTTLE_MS = 16;

const InputTagsChatCmp = forwardRef<TextInputType, TagUsersParams>(
  ({onFocus, onBlur, onDialogWillOpen, ...inputProps}, ref) => {
    const [value, setValue] = useInputValue();
    const throttledSetValue = useRef(
      debounce(
        (nextText: string) => {
          setValue(prev => (prev === nextText ? prev : nextText));
        },
        SET_VALUE_THROTTLE_MS,
        {
          leading: true,
          trailing: true,
          maxWait: SET_VALUE_THROTTLE_MS,
        },
      ),
    );
    const selectionRange =
      useRef<Nullable<{start: number; end: number; text: string}>>(null);
    const currentPosition = useRef<Nullable<number>>(null);
    const previousText = useRef<string>(value);
    const {
      isDialogVisible,
      setIsDialogVisible,
      searchText,
      setSearchText,
      tags,
      messageBodyWithoutLastTag,
      setMessageBodyWithoutLastTag,
      setTagUserStartPosition,
      changeTagPosition,
      removeTag,
    } = useMentions();

    const focusProxy = useCallback(
      (e: FocusEvent) => {
        onFocus?.(e);
      },
      [onFocus],
    );

    const blurProxy = useCallback(
      (e: BlurEvent) => {
        onBlur?.(e);
        setIsDialogVisible(false);
      },
      [onBlur, setIsDialogVisible],
    );

    const onChangeTextProxy = useCallback(
      (text: string) => {
        // Guard: avoid re-processing programmatic updates that can re-trigger
        // `onChangeText` in some RN TextInput implementations (infinite loops).
        if (text === value) {
          return;
        }

        const currentSelection = selectionRange.current;
        const currentPos = currentPosition.current || 0;
        const oldText = previousText.current;

        // Check if we need to handle tag deletion manually
        if (
          shouldHandleTagDeletionManually({
            oldText,
            newText: text,
            selectionRange: currentSelection,
            currentPosition: currentPos,
          })
        ) {
          // Ensure close dialog if is tagging (dialog is open)
          if (isDialogVisible) {
            setIsDialogVisible(false);
            setSearchText('');
          }

          const selectionStart = currentSelection?.start || currentPos;
          const selectionEnd = currentSelection?.end || currentPos;

          // Detect which tags were deleted
          const deletedTags = detectDeletedTags({
            oldText,
            newText: text,
            currentTags: tags,
            selectionRange: {start: selectionStart, end: selectionEnd},
          });

          // Remove deleted tags from context
          deletedTags.forEach(tag => {
            removeTag(tag.type.user.id, tag);
          });

          // Adjust positions of remaining tags
          const lengthDifference = text.length - oldText.length;
          const changePosition = Math.min(selectionStart, selectionEnd);

          const remainingTags = tags.filter(tag => !deletedTags.includes(tag));
          remainingTags.forEach(tag => {
            if (tag.start > changePosition) {
              const newPosition = Math.max(
                changePosition,
                tag.start + lengthDifference,
              );
              changeTagPosition(tag.type.user.id, newPosition, tag.start);
            }
          });

          // Update the message body and value
          setMessageBodyWithoutLastTag(text);
          setValue(prev => (prev === text ? prev : text));
        } else {
          // Handle the case where @ is typed right after another @.
          // `itShouldTag` (inside spyIfWritingTaggedUser) only fires when @ is
          // preceded by a space/newline or is at position 0, so "@@Alex" would
          // never open the dialog through normal flow. We intercept here and
          // manually open the dialog anchored at the new @ position, leaving
          // the preceding @ chars untouched in the text.
          const typedChar = findLastTypedChar(oldText, text);
          if (
            typedChar?.char === '@' &&
            typedChar.pos > 0 &&
            text[typedChar.pos - 1] === '@'
          ) {
            // messageBodyWithoutLastTag must NOT contain the trigger @,
            // so tagNewUser can later insert "@Name" at tagUserStartPosition
            // with the preceding @ chars naturally preserved.
            const bodyWithoutTrigger =
              text.slice(0, typedChar.pos) + text.slice(typedChar.pos + 1);
            if (!isDialogVisible) {
              onDialogWillOpen?.();
            }
            setIsDialogVisible(true);
            setTagUserStartPosition(typedChar.pos);
            setSearchText('');
            setMessageBodyWithoutLastTag(bodyWithoutTrigger);
            setValue(prev => (prev === text ? prev : text));
            return;
          }

          // Handle deletion of @ trigger while in tagging mode (e.g. "@@@@" → "@@@").
          // `onSelectionChange` fires after `onChangeText` on RN, so
          // `currentPosition.current` still holds the pre-change cursor position.
          // We compute the new cursor position from the deleted length.
          if (isDialogVisible && text.length < oldText.length && !searchText) {
            const oldPos = currentPosition.current ?? oldText.length;
            const deletedLength = oldText.length - text.length;
            const newCursorPos = Math.max(0, oldPos - deletedLength);
            const charBeforeCursor =
              newCursorPos > 0 ? text[newCursorPos - 1] : null;
            if (charBeforeCursor === '@') {
              // There is still a @ before the cursor — keep dialog open and
              // move the trigger position one step back.
              const newTriggerPos = newCursorPos - 1;
              const newBodyWithoutTrigger =
                text.slice(0, newTriggerPos) + text.slice(newTriggerPos + 1);
              setTagUserStartPosition(newTriggerPos);
              setMessageBodyWithoutLastTag(newBodyWithoutTrigger);
              throttledSetValue.current(text);
              return;
            }
            // No @ before cursor — let spyIfWritingTaggedUser close the dialog.
          }

          // Use the original spyIfWritingTaggedUser for normal cases
          spyIfWritingTaggedUser({
            text,
            tagUser: isDialogVisible,
            setTagUser: (isTagging: boolean) => {
              isTagging && onDialogWillOpen?.();
              setIsDialogVisible(isTagging);
            },
            value,
            userSearch: searchText,
            setUserSearch: (
              searchValue: string | ((prevValue: string) => string),
            ) => {
              const searchValueToSet =
                typeof searchValue === 'string'
                  ? searchValue
                  : searchValue(searchText || '');
              setSearchText(searchValueToSet || '');
            },
            taggedUsers: tags,
            onChangeText: throttledSetValue.current,
            messageBodyWithoutLastTag,
            setMessageBodyWithoutLastTag,
            setTagUserStartPosition,
            changeTaggedUserStartPosition: changeTagPosition,
            deleteTaggedUser: removeTag,
          });
        }
      },
      [
        changeTagPosition,
        isDialogVisible,
        messageBodyWithoutLastTag,
        onDialogWillOpen,
        removeTag,
        searchText,
        setIsDialogVisible,
        setMessageBodyWithoutLastTag,
        setSearchText,
        setTagUserStartPosition,
        setValue,
        tags,
        value,
      ],
    );

    const onSelectionChangeProxy = useCallback(
      (e: NativeSyntheticEvent<TextInputSelectionChangeEventData>) => {
        const {start, end} = e.nativeEvent.selection;
        currentPosition.current = start;

        if (start === end) {
          selectionRange.current = null;
        } else {
          selectionRange.current = {
            start,
            end,
            text: value.slice(start, end),
          };
        }
      },
      [value],
    );

    const onKeyPressProxy = useCallback(
      (e: NativeSyntheticEvent<TextInputKeyPressEventData>) => {
        const key = e.nativeEvent.key;

        if (key === 'Enter') {
          setIsDialogVisible(false);
        }
      },
      [setIsDialogVisible],
    );

    // Update previous text reference
    useEffect(() => {
      previousText.current = value;
    }, [value]);

    useEffect(() => {
      const throttledSetValueRef = throttledSetValue.current;
      return () => {
        throttledSetValueRef.cancel();
      };
    }, []);

    return (
      <InputTags
        {...inputProps}
        key="InputTags"
        ref={ref}
        value={value}
        taggedUsers={tags}
        onChangeText={onChangeTextProxy}
        onFocus={focusProxy}
        onBlur={blurProxy}
        onSelectionChange={onSelectionChangeProxy}
        onKeyPress={onKeyPressProxy}
      />
    );
  },
);

export const InputTagsChat = memo(InputTagsChatCmp);
