'use client';

import { useAuth } from '@clerk/nextjs';
import { useInfiniteQuery } from '@tanstack/react-query';
import clsx from 'clsx';
import { AnimatePresence, motion } from 'framer-motion';
import { debounce } from 'lodash-es';
import { observer } from 'mobx-react-lite';
import { usePathname } from 'next/navigation';
import { memo, useEffect, useMemo, useRef, useState } from 'react';
import React from 'react';
import { useInView } from 'react-intersection-observer';
import { useIsClient } from 'usehooks-ts';

import { useStores } from '@/app/(root)/AppProviders';
import DiscoverSectionWrapper from '@/components/carousel/carousels/DiscoverSectionWrapper';
import {
  ContestRowSkeleton,
  FeedSkeleton,
  HeroCarouselSkeleton,
  HooksSkeleton,
  RowSkeleton,
} from '@/components/carousel/carousels/SkeletonCarousel';
import ActivityFeedHome from '@/components/feed/ActivityFeedHome';
import { useBreakpointMd } from '@/hooks/useBreakpoint';
import usePageViewLog from '@/hooks/usePageViewLog';
import { PlaySourceContextProvider } from '@/hooks/usePlaySource';
import { useSproutTracking } from '@/hooks/useSproutTracking';
import { Clip } from '@/state/clipStore';
import { DiscoverSection } from '@/state/discoverStore';
import { isSubscriber } from '@/utils/session';

import TopRightBar from './TopRightBar';

const PromoBannerPlaceholder: React.FC<{
  isSignedIn?: boolean;
  isSubscribed?: boolean;
}> = () => null;

const FEED_PAGE_SIZE = 2;

const CONTAINER_VARIANTS = {
  hidden: { opacity: 0 },
  show: {
    when: 'beforeChildren',
    opacity: 1,
    transition: {
      staggerChildren: 0.4,
    },
  },
};

const CHILD_VARIANTS = {
  hidden: { opacity: 0, y: -20 },
  show: {
    opacity: 1,
    y: 0,
    transition: { duration: 1.25, type: 'spring', bounce: 0.45 },
  },
};

const DiscoverSectionRow = memo(
  function DiscoverSection({
    section,
    index,
  }: {
    section: DiscoverSection;
    index: number;
  }) {
    return (
      <motion.div
        suppressHydrationWarning
        variants={CHILD_VARIANTS}
        key={`${section.section_name}:${section.id}`}
      >
        <PlaySourceContextProvider
          playSourceId={section.id}
          playSourceType='discover_carousel'
        >
          <DiscoverSectionWrapper index={index} section={section} />
        </PlaySourceContextProvider>
      </motion.div>
    );
  },
  (prevProps, nextProps) => {
    return (
      prevProps.section === nextProps.section &&
      prevProps.index === nextProps.index
    );
  }
);

const DiscoverPageClient = observer(() => {
  usePageViewLog({ actionName: 'PageViewed', componentContext: 'discover' });

  const pathname = usePathname();
  const { discover, session, playbar, clips: clipsStore } = useStores();

  const isMobile = !useBreakpointMd();

  // Initialize Sprout tracking
  useSproutTracking();

  // Initial state of 'show' will disable the animation when sections load in
  const initialAnimationState = useMemo(() => {
    if (typeof window === 'undefined') return 'hidden';
    const savedPosition = sessionStorage.getItem(`scroll-${pathname}`);
    return savedPosition && parseInt(savedPosition) > 0 ? 'show' : 'hidden';
  }, [pathname]);

  // Maintain scroll position when navigating between pages
  const hasSetScrollPosition = useRef(false);
  const [container, setContainerRef] = useState<HTMLDivElement | null>(null);

  useEffect(() => {
    if (!container || isMobile) return;

    const storageKey = `scroll-${pathname}`;

    // Only save scroll when changed significantly (150px) or after scrolling stops
    const saveScroll = debounce(() => {
      const currentScroll = container.scrollTop || 0;
      const lastScroll = parseInt(sessionStorage.getItem(storageKey) || '0');
      if (Math.abs(currentScroll - lastScroll) > 150) {
        sessionStorage.setItem(storageKey, currentScroll.toString());
      }
    }, 400);
    container.addEventListener('scroll', saveScroll, {
      passive: true,
    });

    // Restore previously saved position
    const savedPosition = sessionStorage.getItem(storageKey);
    if (container && savedPosition && !hasSetScrollPosition.current) {
      container.scroll(0, parseInt(savedPosition));
    }

    return () => {
      saveScroll.cancel();
      container.removeEventListener('scroll', saveScroll);
    };
  }, [isMobile, container, pathname]);

  const discoverQuery = useInfiniteQuery({
    queryKey: ['discover', 'sections'],
    queryFn: async ({ pageParam }) => {
      const responses = await Promise.all(
        new Array(FEED_PAGE_SIZE).fill(0).map((_v, i) =>
          discover.apiClient.POST('/api/discover', {
            body: {
              start_index:
                pageParam == null && !i ? undefined : (pageParam ?? 0) + i,
              page_size: 1,
              section_name: null,
              section_content: null,
              secondary_section_content: null,
              product: null,
            },
          })
        )
      );
      // Kind of dirty, but updating clips in an effect is too slow
      const clipsToUpdate: Clip[] = [];
      responses.forEach((response) => {
        response.data?.sections?.forEach((section) => {
          if (section.section_type === 'playlist') {
            clipsToUpdate.push(...section.items);
          } else if (section.section_type === 'featured_feed') {
            discover.updateFeaturedFeedClips({
              feedSections: section.items,
              clipsToUpdate,
            });
          }
        });
      });
      clipsStore.updateClips(clipsToUpdate);
      // Return response data
      return responses.map(({ data }) => data!).filter((data) => data);
    },
    initialPageParam: null as number | null,
    getNextPageParam: (lastPage, _pages, lastPageParam) => {
      const nextPageParam = (lastPageParam ?? 0) + FEED_PAGE_SIZE;
      const { total_sections: totalSections = 0 } =
        lastPage[lastPage.length - 1] || {};
      return nextPageParam >= totalSections ? undefined : nextPageParam;
    },
    select(data) {
      const lastGroup = data.pages[data.pages.length - 1];
      const processedData = {
        ...lastGroup[lastGroup.length - 1],
        num_pages: data.pages.length,
        sections: data.pages
          .flatMap((data) => data.flatMap((group) => group.sections))
          .filter((v) => v != null),
      };
      return processedData as NonNullable<typeof processedData>;
    },
    staleTime: 15 * 60000,
  });

  // Infinite genres load when we have exhausted the total discover sections
  const {
    sections: discoverQuerySections = [],
    start_index: discoverStartIndex,
    total_sections: discoverTotalSections = 0,
  } = discoverQuery.data || {};
  const enableInfiniteGenres =
    discoverStartIndex && discoverTotalSections
      ? discoverStartIndex >= discoverTotalSections - 1
      : false;
  const infiniteGenreQuery = useInfiniteQuery({
    enabled: enableInfiniteGenres,
    queryKey: ['discover', 'genres'],
    queryFn: async ({ pageParam }) => {
      const response = await discover.apiClient.POST('/api/discover', {
        body: {
          start_index: 0,
          page_size: 1,
          section_name: 'basic_genre_songs',
          section_content: null,
          secondary_section_content: null,
          page: pageParam,
          disable_shuffle: false,
        },
      });
      // Kind of dirty, but updating clips in an effect is too slow
      const clipsToUpdate: Clip[] = [];
      response.data?.sections?.forEach((section) => {
        if (section.section_type === 'playlist') {
          clipsToUpdate.push(...section.items);
        } else if (section.section_type === 'featured_feed') {
          discover.updateFeaturedFeedClips({
            feedSections: section.items,
            clipsToUpdate,
          });
        }
      });
      clipsStore.updateClips(clipsToUpdate);
      // Return response data
      return response.data;
    },
    initialPageParam: 1,
    // Normally this should just keep going, but give up if the last genre section was empty,
    // which is more likely on staging
    getNextPageParam: (lastPage, _pages, lastPageParam) =>
      lastPage?.sections?.length === 0 ? undefined : lastPageParam + 1,
    select(data) {
      const processedData = {
        ...data.pages[data.pages.length - 1],
        num_pages: data.pages.length,
        sections: data.pages
          .flatMap((page) => page?.sections)
          .filter((v) => v != null),
      };
      return processedData as NonNullable<typeof processedData>;
    },
    staleTime: 15 * 60000,
  });

  const [paginationRef] = useInView({
    root: isMobile ? undefined : container,
    threshold: 0,
    // load sections for 1.5x the viewport
    rootMargin: '50% 0px',
    onChange: async (inView) => {
      if (
        inView &&
        !discoverQuery.isFetchingNextPage &&
        !infiniteGenreQuery.isFetchingNextPage
      ) {
        // Fetch the next discovery section or infinite genre page
        if (discoverQuery.hasNextPage) {
          discoverQuery.fetchNextPage();
        } else {
          infiniteGenreQuery.fetchNextPage();
        }
      }
    },
  });

  const isClient = useIsClient();
  const PromoBanner = isClient ? PromoBannerPlaceholder : null;

  const isSubscribed = isSubscriber(session);
  const { isSignedIn } = useAuth();

  // Leave some wiggle room to tweak the list of sections and inject new ones
  const { sections: infiniteGenreQuerySections = [] } =
    infiniteGenreQuery.data || {};
  const discoverSections = useMemo(() => {
    const sections: (
      | DiscoverSection
      | (Pick<DiscoverSection, 'id' | 'section_name'> & {
          section_type?: undefined;
        })
    )[] = [...discoverQuerySections, ...infiniteGenreQuerySections];

    if (sections.length > 0) {
      const featuredPlaylistIndex = sections.findIndex(
        (section) => section.id === 'featured_playlists'
      );
      if (featuredPlaylistIndex >= 0) {
        sections.splice(featuredPlaylistIndex + 1, 0, {
          id: 'activity_feed',
          section_name: 'activity_feed',
        });
      }
    }

    return sections;
  }, [
    discoverQuerySections,
    discoverTotalSections,
    infiniteGenreQuerySections,
  ]);

  return (
    <div
      className={clsx(
        'relative h-full w-full bg-background-primary p-4 md:pt-8',
        { 'pb-[80px]': playbar.clip },
        { 'overflow-y-scroll': discoverQuery.isFetched },
        { 'overflow-y-hidden': !discoverQuery.isFetched }
      )}
      ref={setContainerRef}
    >
      {PromoBanner && (
        <PromoBanner isSignedIn={isSignedIn} isSubscribed={isSubscribed} />
      )}
      {/* sticky someday */}
      <TopRightBar className='relative top-0 right-0 mb-4 w-full justify-end max-md:hidden' />
      {!discoverQuery.isFetched ? (
        <div className='flex shrink-0 flex-col'>
          <HeroCarouselSkeleton />
          <FeedSkeleton columnCount={3} itemCount={5} showTitles={true} />
          <ContestRowSkeleton />
          <HooksSkeleton />
          <RowSkeleton />
          <RowSkeleton />
          <RowSkeleton />
          <RowSkeleton />
        </div>
      ) : discoverSections.length > 0 ? (
        <AnimatePresence>
          <motion.div
            suppressHydrationWarning
            animate='show'
            initial={initialAnimationState}
            variants={CONTAINER_VARIANTS}
          >
            {discoverSections.map((section, index) => {
              // Client hardcoded sections
              if (section.section_type === undefined) {
                switch (section.id) {
                  case 'activity_feed':
                    return (
                      <ActivityFeedHome
                        key={`${section.section_name}:${section.id}`}
                      />
                    );
                  default:
                    return null;
                }
              }

              return (
                <DiscoverSectionRow
                  index={index}
                  section={section}
                  key={`${section.section_name}:${section.id}`}
                />
              );
            })}
          </motion.div>
        </AnimatePresence>
      ) : null}
      {/* Keep showing row skeletons while fetching next page */}
      {discoverQuery.isFetchingNextPage && (
        <div className='flex items-center justify-center'>
          <RowSkeleton />
        </div>
      )}
      {!(discoverQuery.hasNextPage || infiniteGenreQuery.hasNextPage) ||
      discoverQuery.isFetchingNextPage ||
      infiniteGenreQuery.isFetchingNextPage ? null : (
        <div className='w-full' ref={paginationRef} />
      )}
    </div>
  );
});

export default DiscoverPageClient;
