import {
  useInfiniteQuery,
  useMutation,
  useQuery,
  useQueryClient,
} from '@tanstack/react-query';
import { isEqual } from 'lodash-es';
import { usePathname } from 'next/navigation';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useDebounceValue, useLocalStorage } from 'usehooks-ts';

import { useStores } from '@/app/(root)/AppProviders';
import { toast } from '@/components/toast/Toast';
import { createContext, useContextSelector } from '@/hooks/useContextSelector';
import { ContextType } from '@/logging/contextTypes';
import { Clip } from '@/state/clipStore';
import { PlayContext } from '@/state/queueStore';

import { useClips } from '../studio/hooks/useClips';
import { clipBrowserQueryClient } from './clipBrowserQueryClient';
import { invalidateWorkspaceQueries } from './clipBrowserQueryClient';
import useClipMatchesFilters from './clipMatchesFilters';
import getActiveFilters from './getActiveFilters';
import sortClips from './sortClips';
import { ClipBrowserFilters, ClipBrowserKeyType } from './types';
import { ClipBrowserRegistryContext } from './useClipBrowserRegistry';
import useToggleWorkspaceClipPin from './useToggleWorkspaceClipPin';

export default function useClipBrowser(
  getDefaultFilters: () => ClipBrowserFilters,
  endpointKey: string,
  fetchClips: (params: {
    cursor?: string | null;
    limit: number;
    filters: Partial<ClipBrowserFilters>;
  }) => Promise<{ clips: Clip[]; nextCursor: string | null }>,
  getClipAtOffset?: (
    offset: number,
    filters: Partial<ClipBrowserFilters>
  ) => Promise<{ offset: number; clip_id: string | null }>,
  pageLimit: number = 20,
  debounceFiltersMs: number = 350,
  memorizeFilters: boolean = false
) {
  const { clips: clipsStore, session } = useStores();
  const pathname = usePathname();
  const filtersStorageKey = `sept-12-2025-clip-browser-filters-${pathname}-${session.userId}`;

  const [savedFilters, setSavedFilters] = useLocalStorage<ClipBrowserFilters>(
    filtersStorageKey,
    getDefaultFilters
  );

  const [filters, setFilters] = useState<ClipBrowserFilters>(savedFilters);

  useEffect(() => {
    setSavedFilters(filters);
  }, [filters]);

  const queryClient = useQueryClient();
  const toggleClipPin = useToggleWorkspaceClipPin();

  // Page mode state
  const [isPageMode, setIsPageMode] = useState(false);
  const [currentPageNumber, setCurrentPageNumber] = useState<number | null>(
    null
  );
  const [pageCursor, setPageCursor] = useState<string | null>(null);
  const [refreshCounter, setRefreshCounter] = useState(0);

  const [debouncedFilters] = useDebounceValue(filters, debounceFiltersMs);

  // Function to get clip ID at specific offset
  const getClipAtOffsetCallback = useCallback(
    async (offset: number, filters: ClipBrowserFilters) => {
      if (!getClipAtOffset) {
        throw new Error(
          'Offset-based pagination not supported by this endpoint'
        );
      }

      const nonEmptyFilters = getActiveFilters(filters);
      return getClipAtOffset(offset, nonEmptyFilters);
    },
    [getClipAtOffset]
  );

  const query = useInfiniteQuery(
    {
      queryKey: [
        debouncedFilters,
        endpointKey,
        pageLimit,
        isPageMode ? 'page' : 'infinite',
        currentPageNumber,
        refreshCounter, // Add this to force fresh data
      ] as ClipBrowserKeyType,
      staleTime: 1000 * 60 * 5,
      gcTime: 1000 * 60 * 5,
      // some type error in useInfiniteQuery requires the any-ing here
      refetchOnMount: ((query: any) =>
        query.state.data?.pages.length === 0 ? 'always' : true) as any,
      retry: (failureCount, error) => {
        console.error(`Error fetching ${endpointKey} clips`, error);
        if (failureCount > 3) {
          toast({
            title: `Error fetching ${endpointKey} clips`,
            description: (error as any).message,
            status: 'error',
          });
          return false;
        }
        return true;
      },
      retryDelay: 2000,
      queryFn: async ({ pageParam = null }: { pageParam: string | null }) => {
        const nonEmptyFilters = getActiveFilters(debouncedFilters);

        const result = await fetchClips({
          cursor: isPageMode ? pageCursor : pageParam,
          limit: pageLimit,
          filters: nonEmptyFilters,
        });

        result.clips?.forEach((c: Clip) => {
          clipsStore.updateClips([c]);
        });

        return {
          filters: debouncedFilters,
          clips: result.clips.map((c: Clip) => c.id) || [],
          nextCursor: result.nextCursor || null,
          isPageMode,
          pageNumber: currentPageNumber,
        };
      },
      getNextPageParam: (lastPage) => (isPageMode ? null : lastPage.nextCursor), // Disable next page in page mode
      initialPageParam: isPageMode ? pageCursor : null,
    },
    clipBrowserQueryClient
  );

  const [lastClips, setLastClips] = useState<string[]>([]);

  const clips = useMemo(() => {
    if (isPageMode && query.data?.pages?.length) {
      // In page mode, return only clips from the first (and only) page
      return query.data.pages[0]?.clips || [];
    }
    return query.data?.pages?.flatMap((page) => page.clips) || [];
  }, [query.data?.pages, isPageMode]);

  const lastWorkspaceId = useRef<string | null>(null);
  const workspaceId = filters.workspace.workspaceId || null;

  const workspacePinsQuery = useQuery({
    queryKey: ['workspacePins', workspaceId],
    queryFn: async () => {
      const response = await clipsStore.apiClient.GET(
        '/api/project/{project_id}/pinned-clips',
        {
          params: { path: { project_id: workspaceId ?? '' } },
        }
      );
      if (response.error) {
        throw new Error((response as any).error);
      }
      return { clips: response.data?.pinned_clips ?? [] };
    },
    enabled: !!workspaceId,
    gcTime: 0,
  });

  const unpinMutation = useMutation({
    mutationFn: async (clip: Clip) => {
      return await toggleClipPin(clip, false);
    },
    onMutate: async (clip) => {
      await queryClient.cancelQueries({
        queryKey: ['workspacePins', workspaceId],
      });
      const previousPins = queryClient.getQueryData([
        'workspacePins',
        workspaceId,
      ]);
      queryClient.setQueryData(
        ['workspacePins', workspaceId],
        (prevState: any) => {
          if (!prevState) return prevState;
          return {
            ...prevState,
            clips: prevState.clips.filter((c: Clip) => c.id !== clip.id),
          };
        }
      );

      return { previousPins };
    },
    onError: (_err, _clip, context) => {
      if (context?.previousPins) {
        queryClient.setQueryData(
          ['workspacePins', workspaceId],
          context.previousPins
        );
      }
    },
    onSettled: () => {
      queryClient.invalidateQueries({
        queryKey: ['workspacePins', workspaceId],
      });
    },
  });

  const pinMutation = useMutation({
    mutationFn: async (clip: Clip) => {
      return await toggleClipPin(clip, true);
    },
    onMutate: async (clip) => {
      await queryClient.cancelQueries({
        queryKey: ['workspacePins', workspaceId],
      });
      const previousPins = queryClient.getQueryData([
        'workspacePins',
        workspaceId,
      ]);
      queryClient.setQueryData(
        ['workspacePins', workspaceId],
        (prevState: any) => {
          if (!prevState) return prevState;
          return {
            ...prevState,
            clips: [...prevState.clips, clip].sort(
              (a: Clip, b: Clip) =>
                new Date(a.created_at).getTime() -
                new Date(b.created_at).getTime()
            ),
          };
        }
      );

      return { previousPins };
    },
    onError: (_err, _clip, context) => {
      if (context?.previousPins) {
        queryClient.setQueryData(
          ['workspacePins', workspaceId],
          context.previousPins
        );
      }
    },
    onSettled: () => {
      queryClient.invalidateQueries({
        queryKey: ['workspacePins', workspaceId],
      });
    },
  });

  useEffect(() => {
    if (workspacePinsQuery.data?.clips) {
      workspacePinsQuery.data.clips.forEach((c: Clip) => {
        clipsStore.updateClips([c]);
      });
    }
  }, [workspacePinsQuery.data]);

  const workspacePinnedClips = useMemo(() => {
    return workspacePinsQuery.data?.clips ?? [];
  }, [workspacePinsQuery.data?.clips]);

  useEffect(() => {
    if (workspaceId !== lastWorkspaceId.current) {
      setLastClips([]);
      lastWorkspaceId.current = workspaceId;
    }
  }, [workspaceId]);

  useEffect(() => {
    if (clips && !query.isFetching) {
      setLastClips(clips);
    }
  }, [clips, query.isFetching]);

  const lastGetDefaultFilters =
    useRef<() => ClipBrowserFilters>(getDefaultFilters);
  useEffect(() => {
    if (
      lastGetDefaultFilters.current !== getDefaultFilters ||
      !memorizeFilters
    ) {
      setFilters(getDefaultFilters());
      lastGetDefaultFilters.current = getDefaultFilters;
    }
  }, [getDefaultFilters, memorizeFilters]);

  const clipMatchesFilters = useClipMatchesFilters();

  const { clips: clipsById } = useClips(lastClips);

  const filteredClips = useMemo(() => {
    const filteredClips = lastClips.filter(
      (c) =>
        !clipsById[c] ||
        (clipMatchesFilters(clipsById[c], filters) &&
          clipsById[c].status !== 'error')
    );
    return filteredClips;
  }, [lastClips, filters, clipsById, clipMatchesFilters]);

  const clipChanged = useCallback(
    (clip: Clip) => {
      setLastClips((prev) => {
        if (prev.includes(clip.id) && !clipMatchesFilters(clip, filters)) {
          return prev.filter((c) => c !== clip.id);
        } else if (
          !prev.includes(clip.id) &&
          clipMatchesFilters(clip, filters)
        ) {
          return [clip.id, ...prev]
            .map((c) => (c === clip.id ? clip : clipsById[c]))
            .filter(Boolean)
            .sort(sortClips(filters.sort))
            .map((c) => c.id);
        } else {
          return prev;
        }
      });
    },
    [clipsById, filters]
  );

  const registerClipChangeListener = useContextSelector(
    ClipBrowserRegistryContext,
    (c) => c.registerClipChangeListener
  );

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

  const applyingNewFilters = useMemo(
    () =>
      !isEqual(filters, query.data?.pages?.[0]?.filters) ||
      !isEqual(filters, debouncedFilters),
    [filters, query.data?.pages?.[0]?.filters, debouncedFilters]
  );

  const playContext = useMemo<PlayContext>(() => {
    return {
      contextType: ContextType.Create,
      contextId: 'create',
      clips: filteredClips.map((c) => clipsById[c]),
    };
  }, [filteredClips, clipsById]);

  const exitPageMode = useCallback(async () => {
    setIsPageMode(false);
    setCurrentPageNumber(null);
    setPageCursor(null);

    // Invalidate page mode queries to clean up
    await clipBrowserQueryClient.invalidateQueries({
      queryKey: [debouncedFilters, endpointKey, pageLimit, 'page'],
    });

    // This will trigger a refetch with the infinite scroll query key
  }, [endpointKey, debouncedFilters, pageLimit]);

  // Collaborative workspace refresh functionality
  const refreshCollaborativeWorkspace = useCallback(async () => {
    if (workspaceId) {
      // Invalidate workspace-specific queries
      await queryClient.invalidateQueries({
        queryKey: ['workspacePins', workspaceId],
      });
      await invalidateWorkspaceQueries(workspaceId);

      // Increment refresh counter to force fresh query
      setRefreshCounter((prev) => prev + 1);
    }
  }, [workspaceId, queryClient]);

  const setPageNumber = useCallback(
    async (pageNumber: number) => {
      if (pageNumber === currentPageNumber) {
        return;
      }
      if (pageNumber <= 1) {
        exitPageMode();
        return;
      }
      try {
        // For pages > 1, calculate offset accounting for cursor behavior
        // Since the API returns items *after* the cursor, we need to get the clip
        // at position (pageNumber - 1) * pageLimit - 1 to start the page correctly
        const offset = (pageNumber - 1) * pageLimit - 1;
        const offsetResult = await getClipAtOffsetCallback(
          offset,
          debouncedFilters
        );

        if (offsetResult?.clip_id) {
          // Set the page cursor first, then enter page mode
          setPageCursor(offsetResult.clip_id);
          setIsPageMode(true);
          setCurrentPageNumber(pageNumber);
          setLastClips([]);

          // The query will automatically refetch with the new query key and use pageCursor as initialPageParam
        }
      } catch (error) {
        console.error('Failed to set page number:', error);
        toast({
          title: 'Error navigating to page',
          description: 'Failed to navigate to the specified page',
          status: 'error',
        });
      }
    },
    [
      pageLimit,
      debouncedFilters,
      getClipAtOffsetCallback,
      toast,
      exitPageMode,
      currentPageNumber,
    ]
  );

  const resetFilters = useCallback(() => {
    console.log('resetFilters');
    const newFilters = getDefaultFilters();
    console.log('newFilters', newFilters);
    setFilters(getDefaultFilters());
  }, [getDefaultFilters, setFilters]);

  const canResetFilters = useMemo(() => {
    return !isEqual(filters, getDefaultFilters());
  }, [filters, getDefaultFilters]);

  return useMemo(
    () => ({
      filters,
      setFilters,
      resetFilters,
      canResetFilters,
      query,
      clips: filteredClips,
      workspacePinnedClips,
      refetchPinnedClips: async () => {
        await queryClient.invalidateQueries({
          queryKey: ['workspacePins', workspaceId],
          exact: true,
        });
      },
      pinWorkspaceClip: (clip: Clip) => {
        pinMutation.mutate(clip);
      },
      unpinWorkspaceClip: (clip: Clip) => {
        unpinMutation.mutate(clip);
      },
      clipChanged,
      applyingNewFilters,
      playContext,
      pageLimit,
      setPageNumber,
      exitPageMode,
      isPageMode,
      currentPageNumber,
      refreshCollaborativeWorkspace,
      error: query.error,
    }),
    [
      filters,
      setFilters,
      resetFilters,
      canResetFilters,
      query,
      filteredClips,
      workspacePinnedClips,
      clipChanged,
      applyingNewFilters,
      playContext,
      pageLimit,
      setPageNumber,
      exitPageMode,
      isPageMode,
      currentPageNumber,
      refreshCollaborativeWorkspace,
      query.error,
    ]
  );
}

export type ClipBrowserContextType = ReturnType<typeof useClipBrowser>;

export const ClipBrowserContext = createContext<ClipBrowserContextType>(
  undefined as never
);

export const ClipBrowserContextProvider = ({
  children,
  getDefaultFilters,
  fetchClips,
  endpointKey,
  getClipAtOffset,
  memorizeFilters,
}: {
  children: React.ReactNode;
  getDefaultFilters: () => ClipBrowserFilters;
  endpointKey?: string;
  fetchClips?: (params: {
    cursor?: string | null;
    limit: number;
    filters: Partial<ClipBrowserFilters>;
  }) => Promise<{ clips: Clip[]; nextCursor: string | null }>;
  getClipAtOffset?: (
    offset: number,
    filters: Partial<ClipBrowserFilters>
  ) => Promise<{ offset: number; clip_id: string | null }>;
  memorizeFilters?: boolean;
}) => {
  const { clips: clipsStore } = useStores();

  // default fetchClips: uses /api/feed/v3
  const defaultFetchClips = useCallback(
    async ({
      cursor,
      limit,
      filters,
    }: {
      cursor?: string | null;
      limit: number;
      filters: Partial<ClipBrowserFilters>;
    }) => {
      const response = await clipsStore.apiClient.POST('/api/feed/v3', {
        body: {
          cursor,
          limit,
          filters,
        },
      });

      if ((response as any).error) {
        console.error(
          'Error fetching clips',
          response,
          (response as any).error
        );
        throw new Error(JSON.stringify((response as any).error));
      }

      return {
        clips: response.data?.clips || [],
        nextCursor: response.data?.next_cursor || null,
      };
    },
    [clipsStore.apiClient]
  );

  // default getClipAtOffset: /api/feed/v3/offset
  const defaultGetClipAtOffset = useCallback(
    async (offset: number, filters: Partial<ClipBrowserFilters>) => {
      const response = await clipsStore.apiClient.POST('/api/feed/v3/offset', {
        body: {
          offset,
          filters,
        },
      });

      if ((response as any).error) {
        throw new Error((response as any).error);
      }

      return response.data as { offset: number; clip_id: string | null };
    },
    [clipsStore.apiClient]
  );

  const clipBrowser = useClipBrowser(
    getDefaultFilters,
    endpointKey || 'feed_v3',
    fetchClips || defaultFetchClips,
    getClipAtOffset || defaultGetClipAtOffset,
    undefined,
    undefined,
    memorizeFilters
  );
  return (
    <ClipBrowserContext.Provider value={clipBrowser}>
      {children}
    </ClipBrowserContext.Provider>
  );
};
