import { useCallback } from 'react';

import { useContext } from '@/hooks/useContextSelector';
import { useApiClient } from '@/lib/apiClient';
import { Clip } from '@/state/clipStore';
import { getOrGenerateOpusFileUrl } from '@/utils/download';
import { FfmpegAudioBuffer, MainModule } from '@/utils/dsp';
import { isDevOrStaging } from '@/utils/environment';

import { DSPModuleContext } from './DSPModuleContext';

// Global cache of FfmpegAudioBuffers promises by clipId
const ffmpegBufferPromises: Record<string, Promise<FfmpegAudioBuffer>> = {};
const streamingFfmpegBufferPromises: Record<
  string,
  Promise<FfmpegAudioBuffer>
> = {};
const resolvedFfmpegBuffers: Record<string, FfmpegAudioBuffer> = {};

export const getAvailableDuration = (clipId: string): number => {
  return resolvedFfmpegBuffers[clipId]?.availableDuration || 0;
};

export const getMaximumKnownClipDuration = (
  clip: Clip,
  roundAvailableUpTo: number = 30,
  lookaheadBy: number = 10
) => {
  return (
    clip.metadata?.duration ??
    (Math.ceil(
      (getAvailableDuration(clip.id ?? '') + lookaheadBy) / roundAvailableUpTo
    ) * roundAvailableUpTo ||
      0)
  );
};

export default function useFfmpegBufferCache() {
  const { module: dspModule } = useContext(DSPModuleContext);
  const apiClient = useApiClient();

  const getFfmpegBuffer = useCallback(
    (clipId: string): Promise<FfmpegAudioBuffer> => {
      let bufferPromise = ffmpegBufferPromises[clipId];
      if (!bufferPromise) {
        bufferPromise = (async () => {
          const url = await getOrGenerateOpusFileUrl(apiClient, clipId);
          return dspModule.FfmpegAudioBuffer.createFromUrl(url, clipId)!;
        })();
        ffmpegBufferPromises[clipId] = bufferPromise;
        bufferPromise.then((buffer) => {
          resolvedFfmpegBuffers[clipId] = buffer;
        });
      }
      return bufferPromise;
    },
    [apiClient, dspModule]
  );

  const getStreamingFfmpegBuffer = useCallback(
    (clipId: string): Promise<FfmpegAudioBuffer> => {
      let bufferPromise = streamingFfmpegBufferPromises[clipId];
      if (!bufferPromise) {
        bufferPromise = (async () => {
          const url = `https://${isDevOrStaging ? 'audiopipe-dev' : 'audiopipe'}.suno.ai/?item_id=${clipId}&format=webm`;
          return dspModule.FfmpegAudioBuffer.createFromUrlStreaming(
            url,
            clipId
          )!;
        })();
        streamingFfmpegBufferPromises[clipId] = bufferPromise;
        bufferPromise.then((buffer) => {
          resolvedFfmpegBuffers[clipId] = buffer;
        });
      }
      return bufferPromise;
    },
    [apiClient, dspModule]
  );

  const hasFetchedBuffer = useCallback((clipId: string): boolean => {
    return ffmpegBufferPromises[clipId] !== undefined;
  }, []);

  const hasFetchedStreamingBuffer = useCallback((clipId: string): boolean => {
    return streamingFfmpegBufferPromises[clipId] !== undefined;
  }, []);

  const waitUntilPlayable = useCallback(
    async (clip: Clip, playFromSeconds: number = 0): Promise<void> => {
      if (clip.status === 'complete' && !hasFetchedBuffer(clip.id)) {
        await getFfmpegBuffer(clip.id);
      } else if (
        clip.status === 'streaming' &&
        !hasFetchedStreamingBuffer(clip.id)
      ) {
        await getStreamingFfmpegBuffer(clip.id);
      } else if (!['complete', 'streaming'].includes(clip.status ?? '')) {
        return;
      }

      await new Promise<void>(async (resolve) => {
        const pollTime = 100;
        const timeoutTime = 30 * 1000;
        let triesLeft = timeoutTime / pollTime;
        const interval = setInterval(() => {
          if (getAvailableDuration(clip.id) > playFromSeconds) {
            clearInterval(interval);
            resolve();
          } else {
            triesLeft--;
            if (triesLeft === 0) {
              console.error('Timeout waiting for clip to be playable', clip.id);
              clearInterval(interval);
              resolve();
            }
          }
        }, pollTime);
      });
    },
    [
      hasFetchedBuffer,
      hasFetchedStreamingBuffer,
      getFfmpegBuffer,
      getStreamingFfmpegBuffer,
    ]
  );

  return {
    getFfmpegBuffer,
    getStreamingFfmpegBuffer,
    waitUntilPlayable,
  };
}

export const getResolvedFfmpegBuffer = (
  clipId: string
): FfmpegAudioBuffer | undefined => {
  return resolvedFfmpegBuffers[clipId];
};
export const setResolvedFfmpegBuffer = (
  clipId: string,
  buffer: FfmpegAudioBuffer
): void => {
  resolvedFfmpegBuffers[clipId] = buffer;
};
export const getStreamingFfmpegBufferPromise = (
  clipId: string
): Promise<FfmpegAudioBuffer> | undefined => {
  return streamingFfmpegBufferPromises[clipId];
};
export const getFfmpegBufferPromise = (
  clipId: string
): Promise<FfmpegAudioBuffer> | undefined => {
  return ffmpegBufferPromises[clipId];
};

// Non-hook helper to prewarm a streaming buffer and populate the cache.
// Useful from non-React code paths (e.g., GL region builders).
export async function ensureStreamingFfmpegBufferPrewarmed(
  dspModule: MainModule,
  clipId: string
): Promise<FfmpegAudioBuffer> {
  let bufferPromise = streamingFfmpegBufferPromises[clipId];
  if (!bufferPromise) {
    const url = `https://${isDevOrStaging ? 'audiopipe-dev' : 'audiopipe'}.suno.ai/?item_id=${clipId}&format=webm`;
    try {
      // createFromUrlStreaming returns an Embind object immediately; wrap in a Promise for cache shape parity
      const buffer = dspModule.FfmpegAudioBuffer.createFromUrlStreaming(
        url,
        clipId
      )!;
      bufferPromise = Promise.resolve(buffer);
      streamingFfmpegBufferPromises[clipId] = bufferPromise;
      // Store resolved immediately; the reference updates availableDuration over time
      resolvedFfmpegBuffers[clipId] = buffer;
    } catch (e) {
      // Surface a rejected promise in the cache to avoid hammering
      bufferPromise = Promise.reject(e as Error);
      streamingFfmpegBufferPromises[clipId] = bufferPromise;
    }
  }
  return bufferPromise;
}
