import { useCallback, useEffect, useRef, useState } from "react";

import { API_BASE } from "../api/client";

type PlayClipArgs = {
  key: string;
  url?: string | null;
  startMs: number;
  endMs: number;
  title?: string;
  subtitle?: string;
};

export type ClipPlaybackDetails = {
  key: string;
  title?: string;
  subtitle?: string;
};

export function useClipPlayback() {
  const audioRef = useRef<HTMLAudioElement | null>(null);
  const audioContextRef = useRef<AudioContext | null>(null);
  const gainNodeRef = useRef<GainNode | null>(null);
  const playbackRangeRef = useRef<{ endSeconds: number } | null>(null);
  const fadeOutTimerRef = useRef<number | null>(null);
  const activeKeyRef = useRef<string | null>(null);
  const [activeKey, setActiveKey] = useState<string | null>(null);
  const [activeDetails, setActiveDetails] = useState<ClipPlaybackDetails | null>(null);
  const [isPlaying, setIsPlaying] = useState(false);
  const [currentTimeMs, setCurrentTimeMs] = useState<number | null>(null);

  useEffect(() => stopClip(), []);

  const setPlaybackGain = useCallback((value: number) => {
    const gainNode = gainNodeRef.current;
    if (!gainNode) {
      return;
    }

    gainNode.gain.cancelScheduledValues(gainNode.context.currentTime);
    gainNode.gain.setValueAtTime(value, gainNode.context.currentTime);
  }, []);

  const clearFadeOutTimer = useCallback(() => {
    if (fadeOutTimerRef.current !== null) {
      window.clearTimeout(fadeOutTimerRef.current);
      fadeOutTimerRef.current = null;
    }
  }, []);

  const stopClip = useCallback((resetGain = true) => {
    clearFadeOutTimer();

    const audio = audioRef.current;
    if (audio) {
      audio.pause();
      audio.currentTime = 0;
      audio.removeAttribute("src");
      audio.load();
    }

    if (resetGain) {
      setPlaybackGain(1);
    }

    activeKeyRef.current = null;
    playbackRangeRef.current = null;
    setActiveKey(null);
    setActiveDetails(null);
    setIsPlaying(false);
    setCurrentTimeMs(null);
  }, [clearFadeOutTimer, setPlaybackGain]);

  const ensureAudio = useCallback(() => {
    const audio = audioRef.current ?? new Audio();
    if (!audioRef.current) {
      audio.onplay = () => {
        setIsPlaying(true);
      };
      audio.onpause = () => {
        setIsPlaying(false);
      };
      audio.onended = () => {
        stopClip();
      };
      audio.ontimeupdate = () => {
        const range = playbackRangeRef.current;
        if (!range) {
          return;
        }
        setCurrentTimeMs(Math.round(audio.currentTime * 1000));
        if (audio.currentTime >= range.endSeconds) {
          stopClip();
        }
      };

      const AudioContextCtor =
        window.AudioContext ?? (window as Window & typeof globalThis & { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
      if (AudioContextCtor && !audioContextRef.current) {
        const context = new AudioContextCtor();
        const source = context.createMediaElementSource(audio);
        const gain = context.createGain();
        gain.gain.value = 1;
        source.connect(gain);
        gain.connect(context.destination);
        audioContextRef.current = context;
        gainNodeRef.current = gain;
      }
    }

    audioRef.current = audio;
    return audio;
  }, [stopClip]);

  const fadeOutClip = useCallback((durationMs = 1000) => {
    const audio = audioRef.current;
    if (!audio || audio.paused) {
      stopClip();
      return;
    }

    clearFadeOutTimer();
    const gainNode = gainNodeRef.current;
    if (!gainNode) {
      stopClip();
      return;
    }

    const safeDuration = Math.max(1, durationMs);
    const now = gainNode.context.currentTime;
    const currentGain = gainNode.gain.value;

    gainNode.gain.cancelScheduledValues(now);
    gainNode.gain.setValueAtTime(currentGain, now);
    gainNode.gain.linearRampToValueAtTime(0, now + safeDuration / 1000);

    fadeOutTimerRef.current = window.setTimeout(() => {
      stopClip(false);
    }, safeDuration);
  }, [clearFadeOutTimer, stopClip]);

  const playClip = useCallback(async ({ key, url, startMs, endMs, title, subtitle }: PlayClipArgs) => {
    if (!url) {
      return;
    }

    const audio = ensureAudio();
    if (activeKeyRef.current === key && !audio.paused) {
      stopClip();
      return;
    }

    stopClip();
    const nextAudio = ensureAudio();
    activeKeyRef.current = key;
    setActiveKey(key);
    setActiveDetails({
      key,
      title,
      subtitle,
    });
    setIsPlaying(false);
    nextAudio.pause();
    setPlaybackGain(1);

    const audioContext = audioContextRef.current;
    if (audioContext?.state === "suspended") {
      await audioContext.resume();
    }

    const startSeconds = startMs / 1000;
    const endSeconds = endMs / 1000;
    playbackRangeRef.current = { endSeconds };
    setCurrentTimeMs(startMs);

    const metadataReady = new Promise<void>((resolve) => {
      nextAudio.onloadedmetadata = () => {
        if (activeKeyRef.current === key) {
          nextAudio.currentTime = startSeconds;
          setCurrentTimeMs(startMs);
        }
        resolve();
      };
    });

    nextAudio.src = `${API_BASE}${url}`;
    await metadataReady;

    try {
      await nextAudio.play();
    } catch (error) {
      stopClip();
      throw error;
    }
  }, [ensureAudio, setPlaybackGain, stopClip]);

  return {
    activeKey,
    activeDetails,
    currentTimeMs,
    fadeOutClip,
    isPlaying,
    playClip,
    stopClip,
  };
}
