import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useDebounceValue } from 'usehooks-ts';

import { useStores } from '@/app/(root)/AppProviders';
import { createContext } from '@/hooks/useContextSelector';
import { Clip } from '@/state/clipStore';

import { clipBrowserQueryClient } from './clipBrowserQueryClient';
import useClipMatchesFilters from './clipMatchesFilters';
import { ClipBrowserKeyType } from './types';

export default function useClipBrowserRegistry() {
  const { clips: clipsStore } = useStores();
  const clipChangedCallbacksRef = useRef<
    ((clip: Clip, justCreated: boolean) => void)[]
  >([]);

  const [changedClips, setChangedClips] = useState<Clip[]>([]);

  const clipMatchesFilters = useClipMatchesFilters();

  const updateCachedQueries = useCallback(
    (clips: readonly Clip[]) => {
      // Remove affected queries that are no longer active
      clipBrowserQueryClient.removeQueries({
        predicate: (query) => {
          if (query.isActive()) return false;
          const [filters] = query.queryKey as ClipBrowserKeyType;
          return clips.some((clip) => clipMatchesFilters(clip, filters));
        },
      });

      // Get all active queries that might be affected by these clip changes
      const activeQueries = clipBrowserQueryClient.getQueriesData({
        predicate: (query) => {
          const [filters] = query.queryKey as ClipBrowserKeyType;
          return (
            query.isActive() &&
            clips.some((clip) => clipMatchesFilters(clip, filters))
          );
        },
      });

      // Insert new clips to the top of the list, remove old clips from the list.
      activeQueries.forEach(([queryKey, queryData]) => {
        if (!queryData) return;

        const [filters] = queryKey as ClipBrowserKeyType;
        const typedQueryData = queryData as { pages?: { clips: string[] }[] };
        let queryClips = typedQueryData.pages?.flatMap((page) => page.clips);
        let newPages = typedQueryData.pages ? [...typedQueryData.pages] : [];
        clips.forEach((clip) => {
          if (!clipMatchesFilters(clip, filters)) {
            if (queryClips?.find((c) => c === clip.id)) {
              newPages = newPages.map((page) => {
                return {
                  ...page,
                  clips: page.clips.filter((c) => c !== clip.id),
                };
              });
            }
          } else {
            if (!queryClips?.find((c) => c === clip.id)) {
              queryClips = [clip.id, ...(queryClips || [])];
              newPages = newPages.map((page, index) => {
                if (index === 0) {
                  // new clips always at the top of active queries.
                  // this is technically incorrect sorting, but for just-created clips it is preferable UX.
                  return {
                    ...page,
                    clips: [clip.id, ...page.clips],
                  };
                }
                return page;
              });
            }
          }
        });
        // Update the cached data
        clipBrowserQueryClient.setQueryData(queryKey, {
          ...queryData,
          pages: newPages,
        });
      });
    },
    [clipsStore, clipMatchesFilters]
  );

  const [debouncedChangedClips] = useDebounceValue(changedClips, 1000);
  useEffect(() => {
    updateCachedQueries(debouncedChangedClips);
    setChangedClips([]);
  }, [debouncedChangedClips, updateCachedQueries]);

  const registerClipChangeListener = useCallback(
    (clipChanged: (clip: Clip, justCreated: boolean) => void) => {
      clipChangedCallbacksRef.current = [
        ...clipChangedCallbacksRef.current,
        clipChanged,
      ];
      return () => {
        clipChangedCallbacksRef.current =
          clipChangedCallbacksRef.current.filter((b) => b !== clipChanged);
      };
    },
    []
  );

  const clipChanged = useCallback((clip: Clip) => {
    clipChangedCallbacksRef.current.forEach((clipChanged) =>
      clipChanged(clip, false)
    );
    setChangedClips((prev) => [...prev, clip]);
  }, []);

  const clipCreated = useCallback((clip: Clip) => {
    clipChangedCallbacksRef.current.forEach((clipChanged) =>
      clipChanged(clip, true)
    );
    setChangedClips((prev) => [...prev, clip]);
  }, []);

  return useMemo(
    () => ({ registerClipChangeListener, clipChanged, clipCreated }),
    [clipChanged, clipCreated, registerClipChangeListener]
  );
}

export const ClipBrowserRegistryContext = createContext<
  ReturnType<typeof useClipBrowserRegistry>
>(undefined as never);

export const ClipBrowserRegistryContextProvider = ({
  children,
}: {
  children: React.ReactNode;
}) => {
  const clipBrowserRegistry = useClipBrowserRegistry();
  return (
    <ClipBrowserRegistryContext.Provider value={clipBrowserRegistry}>
      {children}
    </ClipBrowserRegistryContext.Provider>
  );
};
