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

export type MediaElementEventKeys =
  | 'onPlay'
  | 'onPause'
  | 'onEnded'
  | 'onSeeking'
  | 'onSeeked'
  | 'onRateChange'
  | 'onLoadStart'
  | 'onLoadedMetadata'
  | 'onLoadedData'
  | 'onCanPlay'
  | 'onCanPlayThrough'
  | 'onProgress'
  | 'onStalled'
  | 'onSuspend'
  | 'onAbort'
  | 'onError'
  | 'onEmptied'
  | 'onTimeUpdate'
  | 'onDurationChange'
  | 'onVolumeChange'
  | 'onWaiting'
  | 'onPlaying';

export type MediaElementEventProps<
  M extends HTMLMediaElement = HTMLMediaElement,
> = {
  [K in MediaElementEventKeys]?: M[Lowercase<K>] extends
    | ((this: any, ...args: infer Args) => infer R)
    | null
    ? ((...args: Args) => R) | null
    : M[Lowercase<K>];
};

export type MediaEventHandler<K extends keyof MediaElementEventProps> =
  NonNullable<MediaElementEventProps[K]>;

export type UseMediaPlaybackOptions<
  M extends HTMLMediaElement = HTMLMediaElement,
> = {
  mediaElement?: M | (() => M) | null;
  mediaUrl?: string;
  onBuffering?: (buffering: boolean) => void;
  onPlaybackError?: (error: any) => void;
} & MediaElementEventProps<M>;

export type MediaTimeCallback = (
  currentTime: number,
  targetTime: number
) => void;

export type UseMediaPlaybackReturn<
  M extends HTMLMediaElement = HTMLMediaElement,
> = {
  mediaElement: M | null;
  isPlaying: boolean;
  isBuffering: boolean;
  duration: number;
  loadedDuration: number;
  load: (mediaUrl: string) => void;
  play: () => Promise<void>;
  pause: () => void;
  seek: (time: number) => void;
  setVolume: (volume: number) => void;
  setMuted: (muted?: boolean) => void;
  setLoop: (loop?: boolean) => void;
  toggle: () => void;
  addTimeCallback: (
    targetTime: number,
    callback: MediaTimeCallback,
    options?: {
      tolerance?: number;
      once?: boolean;
      id?: string;
    }
  ) => void;
  removeTimeCallback: (key: string) => void;
  clearTimeCallbacks: () => void;
  resetTimeCallbacks: () => void;
  getCurrentTime: () => number;
  getDuration: () => number;
  getSecondsLoaded: () => number;
  getPlaying: () => boolean;
  getVolume: () => number;
  getMuted: () => boolean;
  getLoop: () => boolean;
};

export function createVideoElement() {
  return document.createElement('video');
}

export function createAudioElement() {
  return new Audio();
}

function useMediaPlayback(options?: UseMediaPlaybackOptions) {
  const {
    mediaElement: defaultMediaElement = createAudioElement,
    mediaUrl: defaultMediaUrl,
    onBuffering,
    onPlaybackError,
    // Native events
    onPlay,
    onPause,
    onEnded,
    onSeeking,
    onSeeked,
    onRateChange,
    onLoadStart,
    onLoadedMetadata,
    onLoadedData,
    onCanPlay,
    onCanPlayThrough,
    onProgress,
    onStalled,
    onSuspend,
    onAbort,
    onError,
    onEmptied,
    onTimeUpdate,
    onDurationChange,
    onVolumeChange,
    onWaiting,
    onPlaying,
  } = options || {};

  const mediaElement = useMemo(
    () =>
      typeof defaultMediaElement === 'function'
        ? defaultMediaElement()
        : defaultMediaElement,
    [defaultMediaElement]
  );

  const mediaRef = useRef<HTMLMediaElement | null>(null);

  const timeCallbacksRef = useRef(
    new Map<
      string,
      {
        callback: MediaTimeCallback;
        targetTime: number;
        tolerance: number;
        once: boolean;
      }
    >()
  ); // Store time-based callbacks
  const triggeredCallbacksRef = useRef(new Set<string>()); // Track which callbacks have fired
  const callbackIdRef = useRef(0);

  const [isPlaying, setIsPlaying] = useState(false);
  const [duration, setDuration] = useState(0);
  const [loadedDuration, setLoadedDuration] = useState(0);
  const [isBuffering, setIsBuffering] = useState(false);

  const eventCallbacks = useRef({
    onBuffering,
    onPlaybackError,
    onPlay,
    onPause,
    onEnded,
    onSeeking,
    onSeeked,
    onRateChange,
    onLoadStart,
    onLoadedMetadata,
    onLoadedData,
    onCanPlay,
    onCanPlayThrough,
    onProgress,
    onStalled,
    onSuspend,
    onAbort,
    onError,
    onEmptied,
    onTimeUpdate,
    onDurationChange,
    onVolumeChange,
    onWaiting,
    onPlaying,
  });
  useEffect(() => {
    eventCallbacks.current = {
      onBuffering,
      onPlaybackError,
      onPlay,
      onPause,
      onEnded,
      onSeeking,
      onSeeked,
      onRateChange,
      onLoadStart,
      onLoadedMetadata,
      onLoadedData,
      onCanPlay,
      onCanPlayThrough,
      onProgress,
      onStalled,
      onSuspend,
      onAbort,
      onError,
      onEmptied,
      onTimeUpdate,
      onDurationChange,
      onVolumeChange,
      onWaiting,
      onPlaying,
    };
  }, [
    onBuffering,
    onPlaybackError,
    onPlay,
    onPause,
    onEnded,
    onSeeking,
    onSeeked,
    onRateChange,
    onLoadStart,
    onLoadedMetadata,
    onLoadedData,
    onCanPlay,
    onCanPlayThrough,
    onProgress,
    onStalled,
    onSuspend,
    onAbort,
    onError,
    onEmptied,
    onTimeUpdate,
    onDurationChange,
    onVolumeChange,
    onWaiting,
    onPlaying,
  ]);

  // Initialize media element
  useEffect(() => {
    mediaRef.current = mediaElement;

    // Need a media element to get into this party
    if (!mediaElement) return;

    // Event listeners
    const handlePlay: MediaEventHandler<'onPlay'> = (e) => {
      setIsPlaying(true);
      setIsBuffering(false);
      eventCallbacks.current.onPlay?.(e);
    };

    const handlePause: MediaEventHandler<'onPause'> = (e) => {
      setIsPlaying(false);
      eventCallbacks.current.onPause?.(e);
    };

    const handleEnded: MediaEventHandler<'onEnded'> = (e) => {
      setIsPlaying(false);
      eventCallbacks.current.onEnded?.(e);
    };

    const handleSeeking: MediaEventHandler<'onSeeking'> = (e) => {
      eventCallbacks.current.onSeeking?.(e);
    };

    const handleSeeked: MediaEventHandler<'onSeeked'> = (e) => {
      eventCallbacks.current.onSeeked?.(e);
    };

    const handleRateChange: MediaEventHandler<'onRateChange'> = (e) => {
      eventCallbacks.current.onRateChange?.(e);
    };

    const handleLoadStart: MediaEventHandler<'onLoadStart'> = (e) => {
      setIsBuffering(true);
      eventCallbacks.current.onLoadStart?.(e);
    };

    const handleLoadedMetadata: MediaEventHandler<'onLoadedMetadata'> = (e) => {
      setDuration(mediaElement.duration || 0);
      eventCallbacks.current.onLoadedMetadata?.(e);
    };

    const handleLoadedData: MediaEventHandler<'onLoadedData'> = (e) => {
      setIsBuffering(false);
      eventCallbacks.current.onLoadedData?.(e);
    };

    const handleCanPlay: MediaEventHandler<'onCanPlay'> = (e) => {
      setIsBuffering(false);
      eventCallbacks.current.onCanPlay?.(e);
    };

    const handleCanPlayThrough: MediaEventHandler<'onCanPlayThrough'> = (e) => {
      setIsBuffering(false);
      eventCallbacks.current.onCanPlayThrough?.(e);
    };

    const handleProgress: MediaEventHandler<'onProgress'> = (e) => {
      if (mediaElement.buffered.length > 0) {
        setLoadedDuration(
          mediaElement.buffered.end(mediaElement.buffered.length - 1)
        );
      }
      eventCallbacks.current.onProgress?.(e);
    };

    const handleStalled: MediaEventHandler<'onStalled'> = (e) => {
      eventCallbacks.current.onStalled?.(e);
    };

    const handleSuspend: MediaEventHandler<'onSuspend'> = (e) => {
      eventCallbacks.current.onSuspend?.(e);
    };

    const handleAbort: MediaEventHandler<'onAbort'> = (e) => {
      eventCallbacks.current.onAbort?.(e);
    };

    const handleError: MediaEventHandler<'onError'> = (e) => {
      setIsBuffering(false);
      setIsPlaying(false);
      eventCallbacks.current.onError?.(e);
    };

    const handleEmptied: MediaEventHandler<'onEmptied'> = (e) => {
      eventCallbacks.current.onEmptied?.(e);
    };

    const handleTimeUpdate: MediaEventHandler<'onTimeUpdate'> = (e) => {
      const time = mediaElement.currentTime;

      // Check for time-based callbacks
      timeCallbacksRef.current.forEach((callbackData, callbackId) => {
        const { targetTime, callback, tolerance, once } = callbackData;

        // Check if we've hit the target time (within tolerance) and haven't fired this callback yet
        if (
          Math.abs(time - targetTime) <= tolerance &&
          !triggeredCallbacksRef.current.has(callbackId)
        ) {
          triggeredCallbacksRef.current.add(callbackId);
          callback(time, targetTime);

          // Remove callback if it should only fire once
          if (once) {
            timeCallbacksRef.current.delete(callbackId);
          }
        }
      });

      eventCallbacks.current.onTimeUpdate?.(e);
    };

    const handleDurationChange: MediaEventHandler<'onDurationChange'> = (e) => {
      eventCallbacks.current.onDurationChange?.(e);
    };

    const handleVolumeChange: MediaEventHandler<'onVolumeChange'> = (e) => {
      eventCallbacks.current.onVolumeChange?.(e);
    };

    const handleWaiting: MediaEventHandler<'onWaiting'> = (e) => {
      setIsBuffering(true);
      eventCallbacks.current.onWaiting?.(e);
    };

    const handlePlaying: MediaEventHandler<'onPlaying'> = (e) => {
      eventCallbacks.current.onPlaying?.(e);
    };

    // Add event listeners
    mediaElement.addEventListener('play', handlePlay);
    mediaElement.addEventListener('pause', handlePause);
    mediaElement.addEventListener('ended', handleEnded);
    mediaElement.addEventListener('seeking', handleSeeking);
    mediaElement.addEventListener('seeked', handleSeeked);
    mediaElement.addEventListener('ratechange', handleRateChange);
    mediaElement.addEventListener('loadstart', handleLoadStart);
    mediaElement.addEventListener('loadedmetadata', handleLoadedMetadata);
    mediaElement.addEventListener('loadeddata', handleLoadedData);
    mediaElement.addEventListener('canplay', handleCanPlay);
    mediaElement.addEventListener('canplaythrough', handleCanPlayThrough);
    mediaElement.addEventListener('progress', handleProgress);
    mediaElement.addEventListener('stalled', handleStalled);
    mediaElement.addEventListener('suspend', handleSuspend);
    mediaElement.addEventListener('abort', handleAbort);
    mediaElement.addEventListener('error', handleError);
    mediaElement.addEventListener('emptied', handleEmptied);
    mediaElement.addEventListener('timeupdate', handleTimeUpdate);
    mediaElement.addEventListener('durationchange', handleDurationChange);
    mediaElement.addEventListener('volumechange', handleVolumeChange);
    mediaElement.addEventListener('waiting', handleWaiting);
    mediaElement.addEventListener('playing', handlePlaying);

    // Cleanup
    return () => {
      mediaElement.removeEventListener('play', handlePlay);
      mediaElement.removeEventListener('pause', handlePause);
      mediaElement.removeEventListener('ended', handleEnded);
      mediaElement.removeEventListener('seeking', handleSeeking);
      mediaElement.removeEventListener('seeked', handleSeeked);
      mediaElement.removeEventListener('ratechange', handleRateChange);
      mediaElement.removeEventListener('loadstart', handleLoadStart);
      mediaElement.removeEventListener('loadedmetadata', handleLoadedMetadata);
      mediaElement.removeEventListener('loadeddata', handleLoadedData);
      mediaElement.removeEventListener('canplay', handleCanPlay);
      mediaElement.removeEventListener('canplaythrough', handleCanPlayThrough);
      mediaElement.removeEventListener('progress', handleProgress);
      mediaElement.removeEventListener('stalled', handleStalled);
      mediaElement.removeEventListener('suspend', handleSuspend);
      mediaElement.removeEventListener('abort', handleAbort);
      mediaElement.removeEventListener('error', handleError);
      mediaElement.removeEventListener('emptied', handleEmptied);
      mediaElement.removeEventListener('timeupdate', handleTimeUpdate);
      mediaElement.removeEventListener('durationchange', handleDurationChange);
      mediaElement.removeEventListener('volumechange', handleVolumeChange);
      mediaElement.removeEventListener('waiting', handleWaiting);
      mediaElement.removeEventListener('playing', handlePlaying);

      mediaElement.pause();
    };
  }, [mediaElement]);

  useEffect(() => {
    if (defaultMediaUrl && mediaElement) {
      mediaElement.src = defaultMediaUrl;
      mediaElement.load();
    }
  }, [mediaElement, defaultMediaUrl]);

  const addTimeCallback = useCallback<
    UseMediaPlaybackReturn['addTimeCallback']
  >((targetTime, callback, options = {}) => {
    const {
      tolerance = 0.25,
      once = true,
      id: callbackId = `callback_${targetTime}_${++callbackIdRef.current}`,
    } = options;

    // Store with ID if provided, otherwise use targetTime as key
    timeCallbacksRef.current.set(callbackId, {
      targetTime,
      callback,
      tolerance,
      once,
    });

    return callbackId;
  }, []);

  const removeTimeCallback = useCallback<
    UseMediaPlaybackReturn['removeTimeCallback']
  >((key) => {
    timeCallbacksRef.current.delete(key);
    // Also remove from triggered callbacks
    const callbacksToRemove = Array.from(triggeredCallbacksRef.current).filter(
      (id) => id === key || id.startsWith(`${key}-`)
    );
    callbacksToRemove.forEach((id) => triggeredCallbacksRef.current.delete(id));
  }, []);

  const clearTimeCallbacks = useCallback(() => {
    timeCallbacksRef.current.clear();
    triggeredCallbacksRef.current.clear();
  }, []);

  const resetTimeCallbacks = useCallback(() => {
    triggeredCallbacksRef.current.clear();
  }, []);

  const load = useCallback((mediaUrl: string) => {
    if (mediaRef.current && mediaUrl) {
      mediaRef.current.src = mediaUrl;
      mediaRef.current.load();
      setDuration(0);
      setLoadedDuration(0);
      setIsPlaying(false);
    }
  }, []);

  const play = useCallback(async () => {
    if (mediaRef.current) {
      try {
        await mediaRef.current.play();
      } catch (error) {
        console.error('Error playing media:', error);
        eventCallbacks.current.onPlaybackError?.(error);
      }
    }
  }, []);

  const pause = useCallback(() => {
    if (mediaRef.current) {
      mediaRef.current.pause();
    }
  }, []);

  const toggle = useCallback(async () => {
    if (isPlaying) {
      pause();
    } else {
      await play();
    }
  }, [isPlaying, play, pause]);

  const seek = useCallback(
    (time: number) => {
      const seekTime = Math.min(time, duration);
      if (mediaRef.current && seekTime >= 0) {
        // Reset triggered callbacks when seeking backwards
        if (seekTime < mediaRef.current.currentTime) {
          resetTimeCallbacks();
        }
        mediaRef.current.currentTime = seekTime;
      }
    },
    [duration, resetTimeCallbacks]
  );

  const setVolume = useCallback((volume: number) => {
    if (mediaRef.current && volume >= 0) {
      mediaRef.current.volume = volume;
    }
  }, []);

  const setMuted = useCallback((muted = true) => {
    if (mediaRef.current) {
      mediaRef.current.muted = muted;
    }
  }, []);

  const setLoop = useCallback((loop = true) => {
    if (mediaRef.current) {
      mediaRef.current.loop = loop;
    }
  }, []);

  /**
   * Leave it to the consumer to check the playback time
   */
  const getCurrentTime = useCallback(() => {
    return mediaRef.current?.currentTime || 0;
  }, []);
  const getDuration = useCallback(() => {
    if (!mediaRef.current) return NaN;
    const { duration, seekable } = mediaRef.current;
    return duration === Infinity && seekable.length > 0
      ? seekable.end(seekable.length - 1)
      : duration;
  }, []);
  const getSecondsLoaded = useCallback(() => {
    if (!mediaRef.current) return NaN;
    const { buffered, duration, seekable } = mediaRef.current;
    if (buffered.length === 0) return 0;
    const end = buffered.end(buffered.length - 1);
    const effectiveDuration =
      duration === Infinity && seekable.length > 0
        ? seekable.end(seekable.length - 1)
        : duration;
    if (Number.isNaN(effectiveDuration)) return end;
    return Math.min(end, effectiveDuration);
  }, []);
  const getPlaying = useCallback(() => {
    return !mediaRef.current?.paused;
  }, []);
  const getVolume = useCallback(() => {
    return mediaRef.current?.volume || 0;
  }, []);
  const getMuted = useCallback(() => {
    return mediaRef.current?.muted || false;
  }, []);
  const getLoop = useCallback(() => {
    return mediaRef.current?.loop || false;
  }, []);

  return useMemo<UseMediaPlaybackReturn>(
    () => ({
      // Media element reference
      mediaElement,

      // State
      duration,
      loadedDuration,
      isPlaying,
      isBuffering,

      // Functions
      load,
      play,
      pause,
      toggle,
      seek,
      setVolume,
      setMuted,
      setLoop,
      addTimeCallback,
      removeTimeCallback,
      clearTimeCallbacks,
      resetTimeCallbacks,
      getCurrentTime,
      getDuration,
      getSecondsLoaded,
      getPlaying,
      getVolume,
      getMuted,
      getLoop,
    }),
    [
      mediaElement,
      duration,
      loadedDuration,
      isPlaying,
      isBuffering,
      load,
      play,
      pause,
      toggle,
      seek,
      setVolume,
      setMuted,
      setLoop,
      addTimeCallback,
      removeTimeCallback,
      clearTimeCallbacks,
      resetTimeCallbacks,
      getCurrentTime,
      getDuration,
      getSecondsLoaded,
      getPlaying,
      getVolume,
      getMuted,
      getLoop,
    ]
  );
}

export default useMediaPlayback;
