import React, {
  memo,
  useCallback,
  useEffect,
  useImperativeHandle,
  useMemo,
  useRef,
  useState,
} from 'react';
import {InteractionManager, View, ViewStyle} from 'react-native';
import {useTranslation} from 'react-i18next';
import {useDerivedValue, useSharedValue} from 'react-native-reanimated';
import debounce from 'lodash.debounce';
import throttle from 'lodash.throttle';
import {KeyboardAwareLegendList} from '@legendapp/list/keyboard';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {StateCard} from '@components';
import {markInteractiveProps} from '@config/perfomance';
import {useUnmount} from '@hooks/useUnmount';
import {useEvent} from '@hooks/useEvent';
import {useRefValue} from '@hooks/useRefValue';
import {
  DefaultMessage as DefaultMessageType,
  ExtendedMessage,
  ExtendedMessageType,
} from '@modules/chats/interfaces';
import {
  isScrollingToMessage,
  loadInitialMessagesIfNeeded,
  resetMessageWindow,
  useIsChatUserDeactivated,
} from '@modules/chats/store/zustand/useGroups';
import {getEstimatedMessageSize} from '@modules/chats/utils';
import {useExtendedMessages} from '@modules/chats/hooks';
import {
  useIsStoreReady,
  useShouldFetchInitialMessages,
  useHasFirstPageLoadError,
  useMessages,
  useIsChannel,
  useConversationLoadActions,
  useMessagesSearchStore,
} from '@modules/chats/store';
import {MessageRangesHandler} from '@modules/chats/instances/MessageRangesHandler';
import {fileHandler} from '@modules/chats/instances/FileHandler';
import {VisibleVideoAssets} from '@modules/chats/instances/VisibleVideoAssets';
import {
  isPictureAsset,
  isVideoAsset,
} from '@modules/chats/utils/mappers/assetMapper';
import {
  useMessagesListRef,
  useMessagesListMethodsRef,
} from '@modules/chats/contexts';
import {DefaultMessage, DatePill} from '@modules/chats/components';
import {useTheme} from '@shared/theme';

import SystemMessage from '../Message/SystemMessage';
import UnsupportedMessage from '../Message/UnsupportedMessage';
import DeletedMessage from '../Message/DeletedMessage';
import UnreadPill from '../Message/UnreadPill';
import MessageReactions from '../Message/MessageReactions';
import {styles} from './styles';
import {
  useViewPortSetter,
  ViewPortProvider,
} from './components/ViewPortProvider';
import ScrollToBottomButton from './components/ScrollToBottomButton';
import {MessagesSkeleton} from '../MessagesSkeleton';
import {usePreparePlaylist} from './usePreparePlaylist';
import {useInitialIndex} from './useInitialIndex';
import {useScrollToMessage} from './useScrollToMessage';
import {DialogAIMessageProvider} from '../DialogAIMessageProvider';

import type {ViewToken} from '@legendapp/list/react-native';

const messageNeedsStyle: Partial<Record<ExtendedMessageType, boolean>> = {
  [ExtendedMessageType.ListPadding]: true,
  [ExtendedMessageType.DatePill]: true,
  [ExtendedMessageType.UnreadPill]: true,
  [ExtendedMessageType.SystemMessage]: true,
  [ExtendedMessageType.Reactions]: true,
};

const getMessageStyle = (index: number, item: ExtendedMessage) => {
  if (!messageNeedsStyle[item.extendedType]) {
    return undefined;
  }

  const size = getEstimatedMessageSize(item, index);
  return {height: size, overflow: 'hidden'};
};

const ListVerticalPadding = memo(() => (
  <View style={styles.listVerticalPadding} />
));

interface Props {
  channel: string;
}

// Add every prop that could change the message height
const keyExtractor = (item: ExtendedMessage) => {
  const forcedItemType = item as unknown as DefaultMessageType;
  return `${forcedItemType.id}-${forcedItemType.data?.version ?? 0}-${
    forcedItemType.isFirst ?? false
  }`;
};

const viewabilityConfig = {
  itemVisiblePercentThreshold: 50,
};

function Messages({channel}: Props) {
  const {theme} = useTheme();
  const is_channel = useIsChannel(channel);
  const setViewPort = useViewPortSetter();
  const isNearEnd = useSharedValue(false);
  const isAtEnd = useSharedValue(false);
  const isScrollToBottomVisible = useDerivedValue(
    () => !isNearEnd.get() && !isAtEnd.get(),
  );
  const insets = useSafeAreaInsets();
  const messages = useMessages(channel);
  const firstMessageTs = messages.at(0)?.ts;
  const hasToFetchInitialLoadAbove = messages.length <= 15;
  const didLoadFirstPage = useRef(false);
  const {
    loadAboveMessages,
    loadBelowMessagesIfNeeded,
    registerScrollToBottom,
    unregisterScrollToBottom,
    setIsScrolling,
    lastMessageTs,
  } = useConversationLoadActions(channel);
  const {
    setCurrentSelectedMatch,
    matchedMessagesTsIndexMap,
    isSearchVisible,
    currentSelectedMatch,
  } = useMessagesSearchStore();
  const isAtLatestWindow = lastMessageTs === messages.at(-1)?.ts;

  const isUserDeactivated = useIsChatUserDeactivated(channel);

  const extendedMessages = useExtendedMessages({
    messages,
    channel,
    is_channel: is_channel || false,
  });
  const preparePlaylist = usePreparePlaylist(extendedMessages);
  const [lastReplyMessage, setLastReplyMessage] =
    useState<DefaultMessageType | null>(null);
  const legendRef = useMessagesListRef();
  const {scrollToMessage} = useScrollToMessage({
    channel,
    extendedMessages,
  });
  const {initialIndex} = useInitialIndex({
    messages: extendedMessages,
    enabled: messages.length > 0, //In some cases there's only one initial message; additional messages load next,
  });
  const scrollToMessageByTs = useCallback(
    (replyMessage: DefaultMessageType | null) =>
      (ts: string, viewPosition = 0.5) => {
        setLastReplyMessage(replyMessage);
        scrollToMessage(ts, viewPosition);
      },
    [scrollToMessage],
  );

  const messagesListMethodsRef = useMessagesListMethodsRef();
  const scheduledScrollAction = useRef<(() => void) | null>(null);

  useImperativeHandle(messagesListMethodsRef, () => ({
    scrollToMessageByTs: scrollToMessageByTs(null),
    scheduleScrollTo: (action: () => void) => {
      scheduledScrollAction.current = action;
    },
  }));

  const onContentSizeChange = useCallback(() => {
    const action = scheduledScrollAction.current;
    if (action) {
      scheduledScrollAction.current = null;
      action();
    }
  }, []);

  const scrollToBottom = useCallback(() => {
    if (lastReplyMessage) {
      scrollToMessageByTs(null)(lastReplyMessage.data.ts);
      return;
    }

    if (isAtLatestWindow) {
      legendRef.current?.scrollToEnd();
      return;
    }

    resetMessageWindow(channel);
  }, [
    lastReplyMessage,
    isAtLatestWindow,
    channel,
    scrollToMessageByTs,
    legendRef,
  ]);

  /**
   * Load initial messages if needed
   */
  useEffect(() => {
    const fullSynced = firstMessageTs
      ? MessageRangesHandler.isAtConversationStart(channel, firstMessageTs)
      : false;

    if (
      hasToFetchInitialLoadAbove &&
      !fullSynced &&
      !didLoadFirstPage.current
    ) {
      didLoadFirstPage.current = true;
      loadAboveMessages(channel);
    }
  }, [firstMessageTs, loadAboveMessages, channel, hasToFetchInitialLoadAbove]);

  useEffect(() => {
    const scrollToBottomHandler = () => {
      if (isScrollToBottomVisible.get()) {
        InteractionManager.runAfterInteractions(() => {
          legendRef.current?.scrollToEnd();
        });
      }
      setLastReplyMessage(null);
      setIsScrolling(false);
    };

    registerScrollToBottom(channel, scrollToBottomHandler);

    return () => {
      unregisterScrollToBottom(channel);
    };
  }, [
    channel,
    registerScrollToBottom,
    unregisterScrollToBottom,
    setIsScrolling,
    isScrollToBottomVisible,
    legendRef,
  ]);

  const renderPreparePlaylist = useCallback(
    (index: number) => (onReady?: () => void) => {
      preparePlaylist(index, onReady);
    },
    [preparePlaylist],
  );

  const renderItem = useCallback(
    ({item, index}: {item: ExtendedMessage; index: number}) => {
      const style = getMessageStyle(index, item) as ViewStyle | undefined;
      return (
        <View style={style} {...markInteractiveProps}>
          {(() => {
            switch (item.extendedType) {
              case ExtendedMessageType.ListPadding:
                return <ListVerticalPadding />;
              case ExtendedMessageType.DatePill:
                return <DatePill item={item} />;
              case ExtendedMessageType.UnreadPill:
                return <UnreadPill item={item} />;
              case ExtendedMessageType.SystemMessage:
                return <SystemMessage item={item} />;
              case ExtendedMessageType.UnsupportedMessage:
                return <UnsupportedMessage item={item} />;
              case ExtendedMessageType.DeletedMessage:
                return <DeletedMessage item={item} />;
              case ExtendedMessageType.Reactions:
                return (
                  <MessageReactions
                    key={item.id}
                    item={item}
                    interactionDisabled={isUserDeactivated}
                  />
                );
              default: {
                return (
                  <DefaultMessage
                    item={item}
                    scrollToMessage={scrollToMessageByTs}
                    preparePlaylist={renderPreparePlaylist(index)}
                  />
                );
              }
            }
          })()}
        </View>
      );
    },
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [is_channel, scrollToMessageByTs, renderPreparePlaylist, isUserDeactivated],
  );

  // Ref to store current values of search related states to avoid re-creating debounce function
  const searchMatchRefValues = useRefValue({
    currentSelectedMatch,
    setCurrentSelectedMatch,
    matchedMessagesTsIndexMap,
  });

  // If user is searching, find the last visible matched message in the viewport and set it as selected
  const selectSearchMatch = useMemo(() => {
    return debounce((viewableItems: ViewToken<ExtendedMessage>[]) => {
      const {
        currentSelectedMatch: currentSelectedMatchLocal,
        matchedMessagesTsIndexMap: matchedMessagesTsIndexMapLocal,
        setCurrentSelectedMatch: setCurrentSelectedMatchLocal,
      } = searchMatchRefValues.current;

      // Skip if the viewport includes already selected match
      const hasSome = viewableItems.find(
        item =>
          (item.item as DefaultMessageType)?.data?.ts ===
          currentSelectedMatchLocal?.ts,
      );

      if (hasSome) {
        return;
      }

      // Iterate from the end to find the last matched message in view
      for (let i = viewableItems.length - 1; i >= 0; i--) {
        const item = viewableItems[i];
        const ts = (item.item as DefaultMessageType)?.data?.ts;

        if (ts) {
          const index = matchedMessagesTsIndexMapLocal.get(ts);
          if (index !== undefined) {
            setCurrentSelectedMatchLocal(index);
            return;
          }
        }
      }
    }, 1500);
  }, [searchMatchRefValues]);

  // Throttled visibility signal → FileHandler.setViewport so visible tiles
  // jump ahead of the rest of the channel's queued downloads, AND →
  // VisibleVideoAssets so the chat tile's `VideoPlayerPool.acquire` only
  // fires for tiles the user can actually see (not the dozen the FlatList
  // keeps mounted off-screen for recycling).
  const filterVisibleAssets = useMemo(
    () =>
      throttle(
        (viewableItems: ViewToken<ExtendedMessage>[]) => {
          const visibleAssetIds: number[] = [];
          const visibleVideoIds: number[] = [];
          for (const token of viewableItems) {
            if (!token.isViewable) continue;
            if (token.item.extendedType !== ExtendedMessageType.Default) {
              continue;
            }
            const attachments = (token.item as DefaultMessageType).data
              ?.attachments;
            if (!attachments?.length) continue;
            for (const a of attachments) {
              if (!isPictureAsset(a) && !isVideoAsset(a)) {
                visibleAssetIds.push(a.id);
              }
              if (isVideoAsset(a)) visibleVideoIds.push(a.id);
            }
          }
          VisibleVideoAssets.set(visibleVideoIds);
          if (visibleAssetIds.length === 0) return;
          fileHandler.setViewport(channel, visibleAssetIds);
        },
        300,
        {leading: true, trailing: true},
      ),
    [channel],
  );

  useUnmount(() => {
    filterVisibleAssets.cancel();
  });

  const onViewableItemsChanged = useEvent(
    ({viewableItems}: {viewableItems: ViewToken<ExtendedMessage>[]}) => {
      setViewPort(viewableItems);
      filterVisibleAssets(viewableItems);
      if (isSearchVisible) {
        selectSearchMatch(viewableItems);
      }
    },
  );

  const onStartReached = useCallback(async () => {
    await loadAboveMessages(channel);
  }, [loadAboveMessages, channel]);

  const onEndReached = useEvent(() => {
    if (isScrollingToMessage) {
      return;
    }
    loadBelowMessagesIfNeeded(channel);
  });

  const maintainScrollAtEndRef = useRef(true);

  useUnmount(() => {
    selectSearchMatch.cancel?.();
  });

  const List = useMemo(() => {
    return (
      <KeyboardAwareLegendList
        ref={legendRef}
        keyboardDismissMode="interactive"
        keyboardShouldPersistTaps="handled"
        keyboardOffset={insets.bottom}
        initialScrollIndex={
          initialIndex.current === -1 ? undefined : initialIndex.current
        }
        data={extendedMessages}
        renderItem={renderItem}
        keyExtractor={keyExtractor}
        contentContainerStyle={styles.contentContainer}
        onEndReached={onEndReached}
        onStartReached={onStartReached}
        onViewableItemsChanged={onViewableItemsChanged}
        onContentSizeChange={onContentSizeChange}
        viewabilityConfig={viewabilityConfig}
        style={styles.list}
        recycleItems={true}
        getFixedItemSize={getEstimatedMessageSize}
        drawDistance={350}
        maintainVisibleContentPosition
        onEndReachedThreshold={0.1}
        maintainScrollAtEnd={maintainScrollAtEndRef.current}
        alignItemsAtEnd
        sharedValues={{
          isNearEnd,
          isAtEnd,
        }}
      />
    );
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [
    extendedMessages,
    renderItem,
    onStartReached,
    onEndReached,
    onViewableItemsChanged,
    onContentSizeChange,
    isNearEnd,
  ]);

  return (
    <View
      style={[styles.list, {backgroundColor: theme.background.layout.default}]}>
      {List}
      <ScrollToBottomButton
        isVisible={isScrollToBottomVisible}
        onPress={scrollToBottom}
        style={styles.scrollToBottomButton}
      />
    </View>
  );
}

function MessagesWithViewPortProvider(props: Props) {
  const {t} = useTranslation();
  const {theme} = useTheme();
  const shouldFetchInitialMessages = useShouldFetchInitialMessages(
    props.channel,
  );
  const hasFirstPageLoadError = useHasFirstPageLoadError(props.channel);
  const isStoreReady = useIsStoreReady();
  const showLoader =
    !isStoreReady || (shouldFetchInitialMessages && !hasFirstPageLoadError);

  const onRetryPress = useMemo(
    () =>
      throttle(() => {
        loadInitialMessagesIfNeeded(props.channel);
      }, 1000),
    [props.channel],
  );

  useEffect(() => {
    if (!isStoreReady) {
      return;
    }

    loadInitialMessagesIfNeeded(props.channel);
  }, [isStoreReady, props.channel]);

  if (hasFirstPageLoadError) {
    return (
      <View
        style={[
          styles.fullContentCentered,
          {backgroundColor: theme.background.layout.default},
        ]}>
        <StateCard
          variant="error"
          title={t('chat.errors.load_messages')}
          description={t('chat.errors.try_again')}
          style={[
            styles.errorStateCard,
            {backgroundColor: theme.background.layout.default},
          ]}>
          <StateCard.Footer>
            <StateCard.Button
              variant="secondary"
              text={t('general.try_again')}
              onPress={onRetryPress}
            />
          </StateCard.Footer>
        </StateCard>
      </View>
    );
  }

  return showLoader ? (
    <MessagesSkeleton />
  ) : (
    <ViewPortProvider>
      <DialogAIMessageProvider>
        <Messages {...props} />
      </DialogAIMessageProvider>
    </ViewPortProvider>
  );
}
export default memo(MessagesWithViewPortProvider);
