import { forwardRef, memo, useCallback, useRef } from 'react';

import { makeStyles } from '@cleartrip/ct-design-style-manager';
import useMergeRefs from '@cleartrip/ct-design-use-merge-refs';
import { ScrollContextProvider, ScrollableObservableChild } from '@cleartrip/ct-design-scroll-container';
import {
  FlatList,
  ListRenderItemInfo,
  NativeScrollEvent,
  NativeSyntheticEvent,
  Pressable,
  Text,
  View,
  ViewToken,
} from 'react-native';
import Animated, {
  Extrapolation,
  interpolate,
  useAnimatedStyle,
  useSharedValue,
  withTiming,
} from 'react-native-reanimated';

const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
const BACK_TO_TOP_ANIM_DURATION = 200;

import { IListItemRendererProps, IVariableSizedListProps, ListRef } from './type';
import { ListItemProvider } from './ListItemContext';
import { areSetsEqual } from './utils';

const BACK_TO_TOP_SHOW_THRESHOLD = 200;
const SCROLL_DELTA_TOLERANCE = 4;

const staticStyles = makeStyles(() => ({
  itemSeparator: {
    height: 24,
  },
  backToTopChip: {
    position: 'absolute',
    top: 16,
    alignSelf: 'center',
    backgroundColor: '#1A1A1A',
    paddingHorizontal: 12,
    paddingVertical: 8,
    borderRadius: 999,
    flexDirection: 'row',
    alignItems: 'center',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.2,
    shadowRadius: 4,
    elevation: 4,
  },
  backToTopText: {
    color: '#FFFFFF',
    fontSize: 14,
    fontWeight: '500',
  },
}));

/**
 * Memoised row shell — keeps individual row renders isolated from the
 * outer FlatList re-render cycle.
 */
const ItemRenderer = memo(
  <T, U>({
    item,
    index,
    RenderItem,
    itemData,
    isVisible,
  }: {
    item: T;
    index: number;
    RenderItem: React.FC<IListItemRendererProps<T, U>>;
    itemData: U;
    isVisible: boolean;
  }) => {
    return <RenderItem item={item} index={index} itemData={itemData} isVisible={isVisible} />;
  },
);

ItemRenderer.displayName = 'ItemRenderer';

/**
 * Native `List`
 *
 * Mapping from web (react-window) → native (FlatList):
 * - `minimumBatchSize` → `initialNumToRender` (first-page size)
 * - `threshold` → `onEndReachedThreshold`
 * - `itemSize` → used at row layout time
 * - `outerElementType` → `ListFooterComponent`
 * - `overScanCount` → contributes to `maxToRenderPerBatch`
 * - `onItemsRendered` → split into two `viewabilityConfigCallbackPairs`:
 *   one for visible-card impression accounting, one for 80% impression gating.
 */
const List = memo(
  forwardRef(
    <T, U extends { data: T[] }>(
      {
        loadMoreItems,
        data,
        minimumBatchSize,
        threshold,
        itemData,
        RenderItem,
        outerElementType,
        itemKey,
        overScanCount,
        windowSize = 21,
        onScroll,
        style,
        ItemSeparatorComponent = () => <View style={staticStyles.itemSeparator} />,
        keyboardShouldPersistTaps = 'handled',
      }: IVariableSizedListProps<T, U>,
      forwardedRef: React.Ref<ListRef>,
    ) => {
      const _ref = useRef<FlatList<T>>(null);
      const mergedRef = useMergeRefs(_ref, forwardedRef);

      const scrollMaintainingRef = useRef(false);
      const observableChildRefs = useRef<Array<ScrollableObservableChild>>([]);
      const isScrollToScrollingRef = useRef(false);

      const prevScrollYRef = useRef(0);
      const showTargetRef = useRef(0);
      const showProgress = useSharedValue(0);

      const chipAnimatedStyle = useAnimatedStyle(() => {
        const v = showProgress.value;
        return {
          opacity: v,
          transform: [{ translateY: interpolate(v, [0, 1], [-12, 0], Extrapolation.CLAMP) }],
          pointerEvents: v > 0.5 ? 'auto' : 'none',
        };
      });

      const setShowTarget = (next: 0 | 1) => {
        if (showTargetRef.current === next) return;
        showTargetRef.current = next;
        showProgress.value = withTiming(next, { duration: BACK_TO_TOP_ANIM_DURATION });
      };

      const handleBackToTopPress = useCallback(() => {
        _ref.current?.scrollToOffset({ offset: 0, animated: true });
        if (showTargetRef.current !== 0) {
          showTargetRef.current = 0;
          showProgress.value = withTiming(0, { duration: BACK_TO_TOP_ANIM_DURATION });
        }
        // showProgress / showTargetRef are stable refs; safe with [] deps
        // eslint-disable-next-line react-hooks/exhaustive-deps
      }, []);

      const handleScroll = useCallback(
        (e: NativeSyntheticEvent<NativeScrollEvent>) => {
          const currentY = e.nativeEvent.contentOffset.y;
          const prevY = prevScrollYRef.current;

          if (currentY <= BACK_TO_TOP_SHOW_THRESHOLD) {
            setShowTarget(0);
          } else if (prevY - currentY > SCROLL_DELTA_TOLERANCE) {
            setShowTarget(1);
          } else if (currentY - prevY > SCROLL_DELTA_TOLERANCE) {
            setShowTarget(0);
          }

          prevScrollYRef.current = currentY;

          onScroll?.(e);
          scrollMaintainingRef.current = true;
        },
        [onScroll],
      );

      const viewableIndicesRef = useRef<Set<number>>(new Set());

      const viewableItemsRef = useRef<Record<string, Record<string, unknown>>>({});
      const initialRenderRef = useRef(false);

      /**
       * @description Collect viewable indices whose impression has not yet been
       * fired for the current visibility pass.
       */
      const getNonTriggeredViewableItems = useCallback(() => {
        return new Set(
          Object.keys(viewableItemsRef.current)
            .filter((key) => {
              return !viewableItemsRef.current[key].triggered;
            })
            .map((key) => {
              return parseInt(key);
            }),
        );
      }, []);

      /**
       * @description Flip the `triggered` flag to true across every currently
       * tracked viewable item so we don't re-fire impressions until visibility
       * changes again.
       */
      const handleViewableItemsUpdate = useCallback(() => {
        const updatedViewableItems = Object.keys(viewableItemsRef.current).reduce<
          Record<string, Record<string, unknown>>
        >((acc, curr) => {
          acc[curr] = {
            triggered: true,
          };
          return acc;
        }, {});

        viewableItemsRef.current = updatedViewableItems;
      }, []);

      const renderItem = useCallback(
        ({ item: row, index: rowIndex }: ListRenderItemInfo<T>) => {
          return (
            <ListItemProvider listId={rowIndex}>
              <ItemRenderer
                item={row}
                index={rowIndex}
                // @ts-expect-error - TODO: fix this
                RenderItem={RenderItem}
                itemData={itemData}
                isVisible={false}
              />
            </ListItemProvider>
          );
        },
        // eslint-disable-next-line react-hooks/exhaustive-deps
        [],
      );

      const getVisibleCards = (prevIndicesMap: Set<number>) => {
        if (!areSetsEqual(prevIndicesMap, viewableIndicesRef.current)) {
          return viewableIndicesRef.current;
        }
        return prevIndicesMap;
      };

      const onViewableItemsChanged = useCallback((props: { changed: ViewToken[]; viewableItems: ViewToken[] }) => {
        let { viewableItems } = props;
        const { changed } = props;

        // Only keep the first item if multiple are viewable (rare with the
        // 80% threshold used below for impressions, but common for the
        // 60% gating threshold).
        if (viewableItems.length > 1) {
          const firstViewable = viewableItems.find((item) => item.isViewable);
          viewableItems = firstViewable ? [firstViewable] : [];
        }

        const viewableItemsMap = new Set(
          viewableItems.map((item) => item.index).filter((index): index is number => index !== null),
        );

        let changedList = false;
        changed.forEach(({ index, isViewable }) => {
          if (index !== null) {
            if (!isViewable) {
              viewableItemsMap.delete(index);
              changedList = true;
            }
          }
        });

        if (changedList) {
          observableChildRefs.current.forEach((child) => {
            child.observe(getVisibleCards(viewableItemsMap), true);
          });
        }
        viewableIndicesRef.current = viewableItemsMap;
      }, []);

      const impressionsOnViewableItemsChanged = useCallback(
        (props: { changed: ViewToken[]; viewableItems: ViewToken[] }) => {
          const { viewableItems } = props;
          const { changed } = props;

          viewableItems.forEach((item) => {
            if (item.isViewable && item.index !== null && !viewableItemsRef.current.hasOwnProperty(item.index)) {
              viewableItemsRef.current[item.index] = {
                triggered: false,
              };
            }
          });

          changed.forEach((item) => {
            if (!item.isViewable && item.index !== null && viewableItemsRef.current.hasOwnProperty(item.index)) {
              delete viewableItemsRef.current[item.index];
            }
          });

          if (!initialRenderRef.current) {
            observableChildRefs.current.forEach((child) => {
              child.observe(getNonTriggeredViewableItems());
            });
            handleViewableItemsUpdate();
            initialRenderRef.current = true;
          }
        },
        // eslint-disable-next-line react-hooks/exhaustive-deps
        [],
      );

      const localViewabilityConfig = {
        waitForInteraction: false,
        minimumViewTime: 0,
        itemVisiblePercentThreshold: 60,
      };

      const impressionsViewabilityConfig = {
        waitForInteraction: false,
        minimumViewTime: 100,
        itemVisiblePercentThreshold: 80,
      };

      const viewabilityConfigCallbackPairs = useRef([
        { viewabilityConfig: localViewabilityConfig, onViewableItemsChanged },
        {
          viewabilityConfig: impressionsViewabilityConfig,
          onViewableItemsChanged: impressionsOnViewableItemsChanged,
        },
      ]);

      return (
        <ScrollContextProvider
          isScrolling={false}
          refs={{}}
          isScrollToScrolling={isScrollToScrollingRef.current}
          observableChildRefs={observableChildRefs.current}
          horizontal={false}
          isList={true}
        >
          <FlatList
            data={data}
            renderItem={renderItem}
            onEndReached={loadMoreItems}
            initialNumToRender={minimumBatchSize}
            windowSize={windowSize}
            maxToRenderPerBatch={(minimumBatchSize ?? 0) + (overScanCount ?? 0)}
            onEndReachedThreshold={threshold}
            keyExtractor={itemKey}
            ListFooterComponent={outerElementType}
            viewabilityConfig={localViewabilityConfig}
            viewabilityConfigCallbackPairs={viewabilityConfigCallbackPairs.current}
            ref={mergedRef}
            ItemSeparatorComponent={ItemSeparatorComponent}
            contentContainerStyle={style}
            removeClippedSubviews={true}
            showsHorizontalScrollIndicator={false}
            showsVerticalScrollIndicator={false}
            alwaysBounceHorizontal={false}
            alwaysBounceVertical={false}
            keyboardShouldPersistTaps={keyboardShouldPersistTaps}
            onScroll={handleScroll}
            onScrollEndDrag={() => {
              /**
               * !NOTE:
               * Drives impression events. Known caveat: video items at the 2nd
               * carousel index are not yet handled. Revisit if the
               * impression-on-scroll-end approach here causes issues.
               */
              if (scrollMaintainingRef.current) {
                setTimeout(() => {
                  observableChildRefs.current.forEach((child) => {
                    child.observe(getNonTriggeredViewableItems());
                  });
                  handleViewableItemsUpdate();
                }, 200);
              }
              scrollMaintainingRef.current = false;
            }}
            onMomentumScrollEnd={() => {
              if (scrollMaintainingRef.current) {
                scrollMaintainingRef.current = false;
                setTimeout(() => {
                  observableChildRefs.current.forEach((child) => {
                    child.observe(getNonTriggeredViewableItems());
                  });
                  handleViewableItemsUpdate();
                }, 200);
              }
            }}
            bounces={false}
            scrollEventThrottle={1}
          />
          <AnimatedPressable
            onPress={handleBackToTopPress}
            accessibilityRole='button'
            accessibilityLabel='Back to top'
            style={[staticStyles.backToTopChip, chipAnimatedStyle]}
          >
            <Text style={staticStyles.backToTopText}>↑ Back to top</Text>
          </AnimatedPressable>
        </ScrollContextProvider>
      );
    },
  ),
) as unknown as <T, U extends { data: T[] }>(
  props: IVariableSizedListProps<T, U> & { ref?: React.Ref<ListRef> },
) => React.JSX.Element;

(List as unknown as { displayName: string }).displayName = 'List';

export default List;
