import { useCallback, useEffect, useMemo, useState } from 'react';

import { useStores } from '@/app/(root)/AppProviders';
import { Clip, ClipsStore, isTimedOut } from '@/state/clipStore';
import makeRequestBatcher from '@/utils/makeRequestBatcher';

const fetchedClipIDs: { [key: string]: true } = {};
const clipErrorMessages: { [key: string]: string } = {};

let requestBatcher: ReturnType<typeof makeRequestBatcher<Clip>>;

export const clipHasTerminalStatus = (clip: Clip) => {
  return (
    clip.status &&
    (['complete', 'error'].includes(clip.status) || isTimedOut(clip))
  );
};

export const CLIP_POLL_INTERVAL = 2500;

export const fetchClip = async (
  clipStore: ClipsStore,
  clipId: string,
  andPollUntilTerminal: boolean = true,
  andRefetchIfNonTerminal: boolean = false,
  onUpdate?: (clip: Clip) => void,
  forceFetch?: boolean
): Promise<Clip> => {
  if (!forceFetch && clipStore.clipById[clipId]) {
    if (
      clipHasTerminalStatus(clipStore.clipById[clipId]) ||
      (!andPollUntilTerminal && !andRefetchIfNonTerminal)
    ) {
      onUpdate?.(clipStore.clipById[clipId]);
      return clipStore.clipById[clipId];
    }
  } else if (!forceFetch && clipErrorMessages[clipId]) {
    throw new Error(clipErrorMessages[clipId]);
  }
  if (!requestBatcher) {
    requestBatcher = makeRequestBatcher(
      async (keys: string[]) => {
        const result = await clipStore.apiClient.GET('/api/feed/v2', {
          params: { query: { ids: keys.join(',') } },
        });
        if (result.data?.clips?.length === keys.length) {
          return result.data.clips;
        } else if (!result.data?.clips?.length) {
          throw new Error('Result not found');
        } else {
          console.error(result.data.clips, keys);
          throw new Error('Result count mismatch');
        }
      },
      CLIP_POLL_INTERVAL,
      48
    );
  }
  try {
    const clip = await requestBatcher(clipId);
    onUpdate?.(clip);
    clipStore.updateClips([clip]);
    if (andPollUntilTerminal) {
      if (!clipHasTerminalStatus(clip)) {
        return await new Promise<Clip>((resolve, reject) => {
          const poll = async () => {
            try {
              const clip = await fetchClip(clipStore, clipId, false, true);
              onUpdate?.(clip);
              clipStore.updateClips([clip]);
              if (clip.status === 'complete') {
                resolve(clipStore.clipById[clipId]!);
              } else if (clip.status === 'error') {
                reject(new Error('Clip failed'));
              } else if (isTimedOut(clip)) {
                reject(new Error('Clip timed out'));
              } else {
                setTimeout(poll, CLIP_POLL_INTERVAL);
              }
            } catch (error) {
              reject(error);
            }
          };
          setTimeout(poll, CLIP_POLL_INTERVAL);
        });
      } else {
        onUpdate?.(clip);
        return clip;
      }
    } else {
      onUpdate?.(clip);
      return clipStore.clipById[clipId];
    }
  } catch (error) {
    clipErrorMessages[clipId] =
      (error as Error)?.message || (error ? String(error) : 'Unknown error');
    throw error;
  }
};

// note: must be used inside of a component wrapped with `observer()`.
export function useFetchClip() {
  const { clips: clipsStore } = useStores();
  return useCallback(
    async (
      clipId: string,
      andPollUntilTerminal: boolean = true,
      andRefetchIfNonTerminal: boolean = false,
      onUpdate?: (clip: Clip) => void,
      forceFetch?: boolean
    ) => {
      return fetchClip(
        clipsStore,
        clipId,
        andPollUntilTerminal,
        andRefetchIfNonTerminal,
        onUpdate,
        forceFetch
      );
    },
    [clipsStore]
  );
}

// note: must be used inside of a component wrapped with `observer()`.
export default function useClip(clipId?: string) {
  const { clips: clipsStore } = useStores();
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string | null>(
    clipId ? clipErrorMessages[clipId] : null
  );

  useEffect(() => {
    if (clipId) {
      setError(clipErrorMessages[clipId] || null);
    } else {
      setError(null);
    }
  }, [clipId]);

  const refetch = useCallback(
    async (keepingPreviousError: boolean = false) => {
      if (!clipId) return;
      if (!keepingPreviousError) {
        setError(null);
        delete clipErrorMessages[clipId];
      }

      setIsLoading(true);
      fetchedClipIDs[clipId] = true;

      fetchClip(clipsStore, clipId, false)
        .then((clip) => {
          setIsLoading(false);
          clipsStore.updateClips([clip]);

          if (!clipHasTerminalStatus(clip)) {
            clipsStore.queueClipToPoll(clip);
          }
        })
        .catch((error) => {
          setIsLoading(false);
          setError(error.message);
        });
    },
    [clipId, clipsStore]
  );

  useEffect(() => {
    if (clipId) {
      refetch(true);
    }
  }, [refetch, clipId]);

  const clip = clipId ? clipsStore.clipById[clipId] : null;

  return useMemo(
    () => ({
      clip,
      isLoading,
      refetch,
      error,
    }),
    [clip, isLoading, refetch, error]
  );
}
