// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import {useRewrite} from '@agents/hooks';
import {useHardwareKeyboardEvents} from '@mattermost/hardware-keyboard';
import {useManagedConfig} from '@mattermost/react-native-emm';
import PasteableTextInput, {type PastedFile, type PasteInputRef} from '@mattermost/react-native-paste-input';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {defineMessage, type IntlShape, useIntl} from 'react-intl';
import {
    Alert, AppState, type AppStateStatus, DeviceEventEmitter, type EmitterSubscription, Keyboard,
    type NativeSyntheticEvent, Platform, type TextInputSelectionChangeEventData,
} from 'react-native';
import Animated, {cancelAnimation, Easing, runOnUI, useAnimatedStyle, useSharedValue, withRepeat, withTiming} from 'react-native-reanimated';

import {updateDraftMessage} from '@actions/local/draft';
import {userTyping} from '@actions/websocket/users';
import {Events, Screens} from '@constants';
import {useKeyboardAnimationContext} from '@context/keyboard_animation';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {useIsTablet} from '@hooks/device';
import {useInputPropagation} from '@hooks/input';
import {useFocusAfterEmojiDismiss} from '@hooks/useFocusAfterEmojiDismiss';
import {DEFAULT_INPUT_ACCESSORY_HEIGHT} from '@hooks/useInputAccessoryView';
import NavigationStore from '@store/navigation_store';
import {handleDraftUpdate} from '@utils/draft';
import {extractFileInfo} from '@utils/file';
import {changeOpacity, makeStyleSheetFromTheme, getKeyboardAppearanceFromTheme} from '@utils/theme';

import type {AvailableScreens} from '@typings/screens/navigation';

type Props = {
    testID?: string;
    channelDisplayName?: string;
    channelId: string;
    maxMessageLength: number;
    rootId: string;
    timeBetweenUserTypingUpdatesMilliseconds: number;
    maxNotificationsPerChannel: number;
    enableUserTypingMessage: boolean;
    membersInChannel: number;
    value: string;
    updateValue: React.Dispatch<React.SetStateAction<string>>;
    addFiles: (files: ExtractedFileInfo[]) => void;
    cursorPosition: number;
    updateCursorPosition: React.Dispatch<React.SetStateAction<number>>;
    sendMessage: () => void;
    inputRef: React.MutableRefObject<PasteInputRef | undefined>;
    setIsFocused: (isFocused: boolean) => void;
}

const showPasteFilesErrorDialog = (intl: IntlShape) => {
    Alert.alert(
        intl.formatMessage({
            id: 'mobile.files_paste.error_title',
            defaultMessage: 'Paste failed',
        }),
        intl.formatMessage({
            id: 'mobile.files_paste.error_description',
            defaultMessage: 'An error occurred while pasting the file(s). Please try again.',
        }),
        [
            {
                text: intl.formatMessage({
                    id: 'mobile.files_paste.error_dismiss',
                    defaultMessage: 'Dismiss',
                }),
            },
        ],
    );
};

const getPlaceHolder = (rootId?: string) => {
    let placeholder;

    if (rootId) {
        placeholder = defineMessage({id: 'create_post.thread_reply', defaultMessage: 'Reply to this thread...'});
    } else {
        placeholder = defineMessage({id: 'create_post.write', defaultMessage: 'Write to {channelDisplayName}'});
    }

    return placeholder;
};

const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
    input: {
        color: theme.centerChannelColor,
        fontSize: 15,
        lineHeight: 20,
        paddingHorizontal: 12,
        paddingTop: Platform.select({
            ios: 6,
            android: 8,
        }),
        paddingBottom: Platform.select({
            ios: 6,
            android: 2,
        }),
        minHeight: 30,
    },
}));

export default function PostInput({
    testID,
    channelDisplayName,
    channelId,
    maxMessageLength,
    rootId,
    timeBetweenUserTypingUpdatesMilliseconds,
    maxNotificationsPerChannel,
    enableUserTypingMessage,
    membersInChannel,
    value,
    updateValue,
    addFiles,
    cursorPosition,
    updateCursorPosition,
    sendMessage,
    inputRef,
    setIsFocused,
}: Props) {
    const intl = useIntl();
    const isTablet = useIsTablet();
    const theme = useTheme();
    const style = getStyleSheet(theme);
    const serverUrl = useServerUrl();
    const managedConfig = useManagedConfig<ManagedConfig>();

    const {
        setShowInputAccessoryView,
        showInputAccessoryView,
        isInputAccessoryViewMode,
        inputAccessoryViewAnimatedHeight,
        keyboardTranslateY,
        isTransitioningFromCustomView,
        setIsEmojiSearchFocused,
        isEmojiSearchFocused,
        keyboardHeight,
        lastKeyboardHeight,
        bottomInset,
        scrollOffset,
        registerCursorPosition,
        registerPostInputCallbacks,
        isInEmojiPickerTransition,
        getPreservedCursorPosition,
        clearCursorPositionPreservation,
    } = useKeyboardAnimationContext();

    // Register cursor position updates with context
    // Always update cursorPositionRef, even when input accessory view is shown,
    // so emoji insertion works correctly at cursor position
    useEffect(() => {
        if (registerCursorPosition) {
            // Pass value length so registerCursorPosition can check if cursor is reset to end
            registerCursorPosition(cursorPosition, value.length);
        }
    }, [registerCursorPosition, cursorPosition, showInputAccessoryView, value]);

    // Register updateValue and updateCursorPosition with context
    useEffect(() => {
        if (registerPostInputCallbacks) {
            registerPostInputCallbacks(updateValue, updateCursorPosition);
        }

        // updateValue and updateCursorPosition are stable setState functions, don't need to be in deps
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [registerPostInputCallbacks]);

    const [propagateValue, shouldProcessEvent] = useInputPropagation();
    const {isProcessing} = useRewrite();

    const lastTypingEventSent = useRef(0);

    const lastNativeValue = useRef('');
    const previousAppState = useRef(AppState.currentState);

    const [longMessageAlertShown, setLongMessageAlertShown] = useState(false);

    // Handle focus after emoji picker dismissal
    const focusInput = useCallback(() => {
        inputRef.current?.focus();
    }, [inputRef]);

    const {
        focus: focusWithEmojiDismiss,
        isDismissingEmojiPicker,
        focusTimeoutRef,
        isManuallyFocusingAfterEmojiDismiss,
    } = useFocusAfterEmojiDismiss(inputRef, focusInput);

    const disableCopyAndPaste = managedConfig.copyAndPasteProtection === 'true';
    const maxHeight = isTablet ? 150 : 88;
    const pasteInputStyle = useMemo(() => {
        return {...style.input, maxHeight};
    }, [maxHeight, style.input]);

    // Pulsing animation for when AI rewrite is processing
    const pulseOpacity = useSharedValue(1);
    const pulsingAnimatedStyle = useAnimatedStyle(() => ({
        opacity: pulseOpacity.value,
    }));

    useEffect(() => {
        if (isProcessing) {
            pulseOpacity.value = withRepeat(
                withTiming(0.5, {duration: 400, easing: Easing.inOut(Easing.ease)}),
                -1,
                true,
            );
        } else {
            pulseOpacity.value = withTiming(1, {duration: 200});
        }

        return () => {
            cancelAnimation(pulseOpacity);
        };
    }, [isProcessing, pulseOpacity]);

    const onBlur = useCallback(() => {
        handleDraftUpdate({
            serverUrl,
            channelId,
            rootId,
            value,
        });
        setIsFocused(false);
    }, [serverUrl, channelId, rootId, value, setIsFocused]);

    // Handle press event (fires BEFORE onFocus) - dismiss emoji picker before keyboard opens
    const handlePress = useCallback(() => {
        focusWithEmojiDismiss();
    }, [focusWithEmojiDismiss]);

    const onFocus = useCallback(() => {
        // Ignore focus events during emoji picker dismissal - handled manually
        if (Platform.OS === 'android' && (isDismissingEmojiPicker.current || focusTimeoutRef.current || isManuallyFocusingAfterEmojiDismiss)) {
            return;
        }

        // On Android, ignore focus events when emoji search is focused
        // This prevents the emoji picker from closing when the search bar gets focus
        if (Platform.OS === 'android' && isEmojiSearchFocused) {
            return;
        }

        setIsFocused(true);

        // Reset emoji search focus immediately to prevent jumping
        // This must happen before closing the emoji picker
        setIsEmojiSearchFocused(false);

        const wasEmojiPickerOpen = showInputAccessoryView;
        if (wasEmojiPickerOpen) {
            updateCursorPosition(value.length);
            clearCursorPositionPreservation?.();
        }
        setShowInputAccessoryView(false);

        if (Platform.OS === 'android') {

            keyboardTranslateY.value = inputAccessoryViewAnimatedHeight.value;
            inputAccessoryViewAnimatedHeight.value = 0;
            isInputAccessoryViewMode.value = false;

            // IMPORTANT: Reset isTransitioningFromCustomView when keyboard opens
            // This ensures emoji picker can be opened again after keyboard appears
            isTransitioningFromCustomView.value = false;

            // Reset bottomInset and scrollOffset so the scroll restoration can trigger when emoji picker closes
            bottomInset.value = 0;
            scrollOffset.value = 0;

            return;
        }

        // Transition from emoji picker to keyboard
        if (showInputAccessoryView) {
            // Use actual keyboard height instead of emoji picker height to ensure consistency
            // This prevents height accumulation when transitioning multiple times
            // Use default keyboard height if no keyboard height has been recorded yet
            // This prevents input container from going to bottom when keyboard hasn't been opened
            const targetKeyboardHeight = keyboardHeight.value || lastKeyboardHeight || DEFAULT_INPUT_ACCESSORY_HEIGHT;

            // Set transition flag FIRST synchronously to prevent keyboard handlers from interfering
            // This must be set before disabling input accessory view mode to avoid race conditions
            isTransitioningFromCustomView.value = true;

            // Collapse emoji picker instantly
            inputAccessoryViewAnimatedHeight.value = 0;

            // Set input container height to keyboard height to ensure correct final position
            // This ensures the height always matches the keyboard, preventing accumulation
            keyboardTranslateY.value = targetKeyboardHeight;

            // Use runOnUI to disable input accessory view mode atomically
            // This ensures the transition flag is visible when keyboard handlers start processing
            runOnUI(() => {
                'worklet';

                // Disable custom view mode to allow keyboard handlers to work
                // This is done AFTER setting transition flag to prevent race conditions
                isInputAccessoryViewMode.value = false;
            })();

            // Safety net: In rare cases (app backgrounding, system interruptions, rapid toggling),
            // the keyboard onEnd event might not fire, leaving us stuck in transition state.
            // This timeout ensures we recover after 1 second if that happens.
            setTimeout(() => {
                if (isTransitioningFromCustomView.value) {
                    isTransitioningFromCustomView.value = false;
                }
            }, 1000);
        }

        // Shared values don't need to be in dependencies - they're stable references
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [
        isDismissingEmojiPicker,
        focusTimeoutRef,
        isManuallyFocusingAfterEmojiDismiss,
        isEmojiSearchFocused,
        setIsFocused,
        setIsEmojiSearchFocused,
        setShowInputAccessoryView,
        showInputAccessoryView,
        lastKeyboardHeight,
        updateCursorPosition,
        clearCursorPositionPreservation,
        value,
        cursorPosition,
    ]);

    const handleAndroidKeyboardHide = useCallback(() => {
        onBlur();
    }, [onBlur]);

    const handleAndroidKeyboardShow = useCallback(() => {
        onFocus();
    }, [onFocus]);

    const checkMessageLength = useCallback((newValue: string) => {
        const valueLength = newValue.trim().length;

        if (valueLength > maxMessageLength) {
            // Check if component is already aware message is too long
            if (!longMessageAlertShown) {
                Alert.alert(
                    intl.formatMessage({
                        id: 'mobile.message_length.title',
                        defaultMessage: 'Message Length',
                    }),
                    intl.formatMessage({
                        id: 'mobile.message_length.message',
                        defaultMessage: 'Your current message is too long. Current character count: {count}/{max}',
                    }, {
                        max: maxMessageLength,
                        count: valueLength,
                    }),
                );
                setLongMessageAlertShown(true);
            }
        } else if (longMessageAlertShown) {
            setLongMessageAlertShown(false);
        }
    }, [intl, longMessageAlertShown, maxMessageLength]);

    const handlePostDraftSelectionChanged = useCallback((event: NativeSyntheticEvent<TextInputSelectionChangeEventData> | null, fromHandleTextChange = false) => {
        // Always update cursor position, even when input accessory view is shown,
        // so emoji insertion works correctly at cursor position
        const cp = fromHandleTextChange ? cursorPosition : event!.nativeEvent.selection.end;

        // Check if cursor is being reset to end when we're transitioning to emoji picker
        // This happens when keyboard is dismissed - the TextInput selection resets to end
        // Only block if we're actually in a transition period (emoji picker opening or just opened)
        const isInTransition = isInEmojiPickerTransition?.();
        const preservedPosition = isInTransition ? getPreservedCursorPosition?.() : null;

        const isCursorAtEnd = cp === value.length;
        const cursorPositionChanged = cp !== cursorPosition;
        const hasPreservedPosition = preservedPosition !== null;
        const preservedPositionChanged = preservedPosition !== cp;
        const isResettingToEnd = isCursorAtEnd && cursorPositionChanged && hasPreservedPosition && preservedPositionChanged;
        const isPreservedPositionWithinBounds = hasPreservedPosition && preservedPosition! < value.length;

        // If we're opening emoji picker and cursor is being reset to end, ignore it and keep the preserved position
        if (!fromHandleTextChange && isResettingToEnd && isInTransition && isPreservedPositionWithinBounds) {
            return;
        }

        // When emoji picker is open, ignore cursor position updates that move to the end
        // unless the value length changed (user typed something).
        if (showInputAccessoryView && !fromHandleTextChange && isCursorAtEnd && cursorPositionChanged) {
            return;
        }

        updateCursorPosition(cp);
    }, [cursorPosition, updateCursorPosition, showInputAccessoryView, value.length, isInEmojiPickerTransition, getPreservedCursorPosition]);

    const handleTextChange = useCallback((newValue: string) => {
        if (!shouldProcessEvent(newValue)) {
            return;
        }
        updateValue(newValue);
        lastNativeValue.current = newValue;

        checkMessageLength(newValue);

        if (
            newValue &&
            lastTypingEventSent.current + timeBetweenUserTypingUpdatesMilliseconds < Date.now() &&
            membersInChannel < maxNotificationsPerChannel &&
            enableUserTypingMessage
        ) {
            userTyping(serverUrl, channelId, rootId);
            lastTypingEventSent.current = Date.now();
        }
    }, [
        shouldProcessEvent,
        updateValue,
        checkMessageLength,
        timeBetweenUserTypingUpdatesMilliseconds,
        membersInChannel,
        maxNotificationsPerChannel,
        enableUserTypingMessage,
        serverUrl,
        channelId,
        rootId,
    ]);

    const onPaste = useCallback(async (error: string | null | undefined, files: PastedFile[]) => {
        if (error) {
            showPasteFilesErrorDialog(intl);
        }

        addFiles(await extractFileInfo(files));
    }, [addFiles, intl]);

    const handleHardwareEnterPress = useCallback(() => {
        const topScreen = NavigationStore.getVisibleScreen();
        let sourceScreen: AvailableScreens = Screens.CHANNEL;
        if (rootId) {
            sourceScreen = Screens.THREAD;
        } else if (isTablet) {
            sourceScreen = Screens.HOME;
        }
        if (topScreen === sourceScreen) {
            sendMessage();
        }
    }, [sendMessage, rootId, isTablet]);

    const handleHardwareShiftEnter = useCallback(() => {
        const topScreen = NavigationStore.getVisibleScreen();
        let sourceScreen: AvailableScreens = Screens.CHANNEL;
        if (rootId) {
            sourceScreen = Screens.THREAD;
        } else if (isTablet) {
            sourceScreen = Screens.HOME;
        }

        if (topScreen === sourceScreen) {
            let newValue: string;
            updateValue((v) => {
                newValue = v.substring(0, cursorPosition) + '\n' + v.substring(cursorPosition);
                return newValue;
            });
            updateCursorPosition((pos) => pos + 1);
            propagateValue(newValue!);
        }
    }, [rootId, isTablet, updateValue, updateCursorPosition, cursorPosition, propagateValue]);

    const onAppStateChange = useCallback((appState: AppStateStatus) => {
        if (appState !== 'active' && previousAppState.current === 'active') {
            updateDraftMessage(serverUrl, channelId, rootId, value);
        }

        previousAppState.current = appState;
    }, [serverUrl, channelId, rootId, value]);

    useEffect(() => {
        let keyboardHideListener: EmitterSubscription | undefined;
        let keyboardShowListener: EmitterSubscription | undefined;
        if (Platform.OS === 'android') {
            keyboardHideListener = Keyboard.addListener('keyboardDidHide', handleAndroidKeyboardHide);
            keyboardShowListener = Keyboard.addListener('keyboardDidShow', handleAndroidKeyboardShow);
        }

        return (() => {
            keyboardShowListener?.remove();
            keyboardHideListener?.remove();
        });
    }, [handleAndroidKeyboardHide, handleAndroidKeyboardShow]);

    useEffect(() => {
        const listener = AppState.addEventListener('change', onAppStateChange);

        return () => {
            listener.remove();
        };
    }, [onAppStateChange]);

    useEffect(() => {
        const listener = DeviceEventEmitter.addListener(Events.SEND_TO_POST_DRAFT, ({text, location}: {text: string; location: string}) => {
            const sourceScreen = channelId && rootId ? Screens.THREAD : Screens.CHANNEL;
            if (location === sourceScreen) {
                const draft = value ? `${value} ${text} ` : `${text} `;
                updateValue(draft);
                updateCursorPosition(draft.length);
                propagateValue(draft);
                inputRef.current?.focus();
            }
        });
        return () => {
            listener.remove();
            updateDraftMessage(serverUrl, channelId, rootId, lastNativeValue.current); // safe draft on unmount
        };

    // - updateValue, updateCursorPosition, propagateValue are stable setState/hook functions
    // - inputRef is a ref (stable reference, doesn't need to be in deps)
    // - serverUrl, value, lastNativeValue are either stable or we want their latest values when event fires
    // - We need to recreate the listener when channelId/rootId changes to check the correct source screen
    // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [updateValue, channelId, rootId]);

    useEffect(() => {
        if (value !== lastNativeValue.current) {
            propagateValue(value);
            lastNativeValue.current = value;
        }

    // - propagateValue is from useInputPropagation hook (stable reference, doesn't need to be in deps)
    // - lastNativeValue is a ref (stable reference, doesn't need to be in deps)
    // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [value]);

    const events = useMemo(() => ({
        onEnterPressed: handleHardwareEnterPress,
        onShiftEnterPressed: handleHardwareShiftEnter,
    }), [handleHardwareEnterPress, handleHardwareShiftEnter]);
    useHardwareKeyboardEvents(events);

    return (
        <Animated.View style={pulsingAnimatedStyle}>
            <PasteableTextInput
                allowFontScaling={true}
                disableCopyPaste={disableCopyAndPaste}
                disableFullscreenUI={true}
                keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
                multiline={true}
                onBlur={onBlur}
                onChangeText={handleTextChange}
                onFocus={onFocus}
                onPress={Platform.OS === 'android' ? handlePress : undefined}
                onPaste={onPaste}
                onSelectionChange={handlePostDraftSelectionChanged}
                placeholder={intl.formatMessage(getPlaceHolder(rootId), {channelDisplayName})}
                placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
                ref={inputRef}
                showSoftInputOnFocus={Platform.OS === 'android' ? (!showInputAccessoryView || isManuallyFocusingAfterEmojiDismiss) : true}
                smartPunctuation='disable'
                submitBehavior='newline'
                style={pasteInputStyle}
                testID={testID}
                underlineColorAndroid='transparent'
                textContentType='none'
                value={value}
                autoCapitalize='sentences'
                editable={!isProcessing}
                nativeID={testID}
            />
        </Animated.View>
    );
}
