'use client';

import React, {
  useState,
  useRef,
  useEffect,
  useCallback,
  forwardRef,
  useImperativeHandle,
} from 'react';
import { Volume2, Pause, Loader2 } from 'lucide-react';
import { cn } from '@/lib/utils';
import { useTranslation } from 'react-i18next';
import useExclusiveAudio from '@/hooks/useExclusiveAudio';
import type { AudioSegment } from '@/c-utils/audio-utils';
import {
  createAudioContext,
  decodeAudioBufferFromBase64,
  playAudioBuffer,
  resumeAudioContext,
} from '@/lib/audio-playback';
import {
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from '@/components/ui/tooltip';

export interface AudioPlayerProps {
  /** OSS URL when audio is complete */
  audioUrl?: string;
  /** Base64 audio segments during streaming */
  streamingSegments?: AudioSegment[];
  /** Whether audio is still streaming */
  isStreaming?: boolean;
  /** Whether the current page is in preview mode (e.g. `?preview=true`) */
  previewMode?: boolean;
  /** Keep the control visible even when no audio is available yet */
  alwaysVisible?: boolean;
  /** Disable the player */
  disabled?: boolean;
  /** Request TTS synthesis when no audio is available yet */
  onRequestAudio?: () => Promise<any>;
  /** Icon size */
  size?: number;
  /** Additional CSS classes */
  className?: string;
  /** Callback when play state changes */
  onPlayStateChange?: (isPlaying: boolean) => void;
  /** Callback when playback reaches the natural end */
  onEnded?: () => void;
  /** Auto-play when new audio content arrives */
  autoPlay?: boolean;
}

export interface AudioPlayerHandle {
  togglePlay: () => void;
  play: () => void;
  pause: (options?: { traceId?: string; keepAutoPlay?: boolean }) => void;
}

export const shouldFallbackToCompleteUrlForWaitingStream = ({
  audioUrl,
  streamingSegmentCount,
  currentSegmentIndex,
  playedSeconds,
}: {
  audioUrl?: string;
  streamingSegmentCount: number;
  currentSegmentIndex: number;
  playedSeconds: number;
}) =>
  Boolean(audioUrl) &&
  streamingSegmentCount <= 0 &&
  currentSegmentIndex <= 0 &&
  playedSeconds <= 0;

/**
 * Audio player component for TTS playback.
 *
 * Supports two modes:
 * 1. Streaming mode: Plays base64-encoded audio segments as they arrive
 * 2. Complete mode: Plays from OSS URL after all segments are uploaded
 */
function AudioPlayerBase(
  {
    audioUrl,
    streamingSegments = [],
    isStreaming = false,
    previewMode = false,
    alwaysVisible = false,
    disabled = false,
    onRequestAudio,
    size = 16,
    className,
    onPlayStateChange,
    onEnded,
    autoPlay = false,
  }: AudioPlayerProps,
  ref: React.ForwardedRef<AudioPlayerHandle>,
) {
  const { t } = useTranslation();
  const { requestExclusive, releaseExclusive } = useExclusiveAudio();
  const [isPlaying, setIsPlaying] = useState(false);
  const [isLoading, setIsLoading] = useState(false);
  // Track if we're waiting for the next segment during streaming
  const [isWaitingForSegment, setIsWaitingForSegment] = useState(false);
  const [localAudioUrl, setLocalAudioUrl] = useState<string | undefined>(
    undefined,
  );

  const audioRef = useRef<HTMLAudioElement | null>(null);
  const audioContextRef = useRef<AudioContext | null>(null);
  const sourceNodeRef = useRef<AudioBufferSourceNode | null>(null);
  const activeSourceNodesRef = useRef<Set<AudioBufferSourceNode>>(new Set());
  const decodedSegmentBuffersRef = useRef<Map<string, AudioBuffer>>(new Map());
  const decodingSegmentPromisesRef = useRef<Map<string, Promise<AudioBuffer>>>(
    new Map(),
  );
  const decodedSegmentContextRef = useRef<AudioContext | null>(null);
  // Track how many seconds have been played from streaming segments in this play session.
  const playedSecondsRef = useRef(0);
  const playSessionRef = useRef(0);
  const pendingStreamRef = useRef(false);
  const isPausedRef = useRef(false);
  const pausedAtRef = useRef(0);
  const segmentOffsetRef = useRef(0);
  const segmentStartTimeRef = useRef(0);
  const segmentDurationRef = useRef(0);

  const effectiveAudioUrl = audioUrl || localAudioUrl;

  const audioUrlRef = useRef(effectiveAudioUrl);
  audioUrlRef.current = effectiveAudioUrl;

  // Use refs to track playback state across async callbacks
  const currentSegmentIndexRef = useRef(0);
  const isPlayingRef = useRef(false);
  const segmentsRef = useRef<AudioSegment[]>([]);
  const isStreamingRef = useRef(false);
  // Lock to prevent concurrent playSegmentByIndex calls
  const isPlayingSegmentRef = useRef(false);

  // Keep refs in sync with props/state
  segmentsRef.current = streamingSegments;
  isStreamingRef.current = isStreaming;

  // Use ref for callback to avoid stale closures
  const onPlayStateChangeRef = useRef(onPlayStateChange);
  onPlayStateChangeRef.current = onPlayStateChange;

  const onEndedRef = useRef(onEnded);
  onEndedRef.current = onEnded;

  // Check if we have audio to play
  const hasAudio = Boolean(effectiveAudioUrl) || streamingSegments.length > 0;

  // Use OSS URL if available and streaming is complete
  const useOssUrl = Boolean(effectiveAudioUrl) && !isStreaming;

  const startPlaySession = useCallback(() => {
    playSessionRef.current += 1;
    return playSessionRef.current;
  }, []);

  const isSessionActive = useCallback(
    (sessionId: number) => playSessionRef.current === sessionId,
    [],
  );

  const resetDecodedSegmentCache = useCallback(() => {
    decodedSegmentBuffersRef.current.clear();
    decodingSegmentPromisesRef.current.clear();
    decodedSegmentContextRef.current = null;
  }, []);

  const getSegmentCacheKey = useCallback((segment: AudioSegment) => {
    const audioData = segment.audioData || '';
    return [
      segment.position ?? '',
      segment.elementId ?? '',
      segment.slideId ?? '',
      segment.segmentIndex,
      audioData.length,
      audioData.slice(0, 24),
      audioData.slice(-24),
    ].join(':');
  }, []);

  const decodeSegmentBuffer = useCallback(
    (audioContext: AudioContext, segment: AudioSegment) => {
      if (decodedSegmentContextRef.current !== audioContext) {
        resetDecodedSegmentCache();
        decodedSegmentContextRef.current = audioContext;
      }

      const cacheKey = getSegmentCacheKey(segment);
      const cachedBuffer = decodedSegmentBuffersRef.current.get(cacheKey);
      if (cachedBuffer) {
        return Promise.resolve(cachedBuffer);
      }

      const pendingDecode = decodingSegmentPromisesRef.current.get(cacheKey);
      if (pendingDecode) {
        return pendingDecode;
      }

      const decodePromise = decodeAudioBufferFromBase64(
        audioContext,
        segment.audioData,
      )
        .then(buffer => {
          decodingSegmentPromisesRef.current.delete(cacheKey);
          decodedSegmentBuffersRef.current.set(cacheKey, buffer);
          return buffer;
        })
        .catch(error => {
          decodingSegmentPromisesRef.current.delete(cacheKey);
          throw error;
        });

      decodingSegmentPromisesRef.current.set(cacheKey, decodePromise);
      return decodePromise;
    },
    [getSegmentCacheKey, resetDecodedSegmentCache],
  );

  const predecodeSegmentByIndex = useCallback(
    (
      index: number,
      sessionId: number,
      audioContext: AudioContext | null = audioContextRef.current,
    ) => {
      if (!audioContext || !isSessionActive(sessionId)) {
        return;
      }
      const segment = segmentsRef.current[index];
      if (!segment) {
        return;
      }
      decodeSegmentBuffer(audioContext, segment).catch(() => {});
    },
    [decodeSegmentBuffer, isSessionActive],
  );

  const stopAllSourceNodes = useCallback(() => {
    if (activeSourceNodesRef.current.size > 0) {
      activeSourceNodesRef.current.forEach(node => {
        try {
          node.stop();
          node.disconnect();
        } catch {
          // Ignore errors when stopping
        }
      });
      activeSourceNodesRef.current.clear();
    }
    if (sourceNodeRef.current) {
      try {
        sourceNodeRef.current.stop();
        sourceNodeRef.current.disconnect();
      } catch {
        // Ignore errors when stopping
      }
      sourceNodeRef.current = null;
    }
  }, []);

  // Cleanup audio resources
  const cleanupAudio = useCallback(() => {
    // Release the segment lock
    isPlayingSegmentRef.current = false;
    stopAllSourceNodes();
    resetDecodedSegmentCache();
    if (audioRef.current) {
      audioRef.current.pause();
      audioRef.current.currentTime = 0;
    }
  }, [resetDecodedSegmentCache, stopAllSourceNodes]);

  const stopPlayback = useCallback(() => {
    playSessionRef.current += 1;
    pendingStreamRef.current = false;
    isPausedRef.current = false;
    pausedAtRef.current = 0;
    segmentOffsetRef.current = 0;
    segmentStartTimeRef.current = 0;
    segmentDurationRef.current = 0;
    cleanupAudio();
    setIsPlaying(false);
    isPlayingRef.current = false;
    setIsLoading(false);
    setIsWaitingForSegment(false);
    onPlayStateChangeRef.current?.(false);
    releaseExclusive();
  }, [cleanupAudio, releaseExclusive]);

  const pausePlayback = useCallback(
    (options?: { traceId?: string; keepAutoPlay?: boolean }) => {
      const hasLiveAudio =
        Boolean(sourceNodeRef.current) ||
        activeSourceNodesRef.current.size > 0 ||
        Boolean(audioRef.current && !audioRef.current.paused);
      const shouldPause = isPlayingRef.current || hasLiveAudio;
      const htmlAudio = audioRef.current;
      const wasHtmlPlaying = Boolean(htmlAudio && !htmlAudio.paused);
      const htmlTime = wasHtmlPlaying ? (htmlAudio?.currentTime ?? 0) : null;
      if (!shouldPause) {
        return;
      }

      requestExclusive(() => {});
      playSessionRef.current += 1;
      isPausedRef.current = !options?.keepAutoPlay;
      setIsPlaying(false);
      isPlayingRef.current = false;
      setIsLoading(false);
      setIsWaitingForSegment(false);
      onPlayStateChangeRef.current?.(false);

      const audioContext = audioContextRef.current;
      const activeNodes = activeSourceNodesRef.current.size;
      if (audioContext && (sourceNodeRef.current || activeNodes > 0)) {
        const elapsed = Math.max(
          0,
          audioContext.currentTime - segmentStartTimeRef.current,
        );
        const duration = segmentDurationRef.current;
        const nextOffset = Math.min(
          segmentOffsetRef.current + elapsed,
          duration > 0 ? duration : segmentOffsetRef.current + elapsed,
        );
        playedSecondsRef.current += Math.max(
          0,
          nextOffset - segmentOffsetRef.current,
        );
        segmentOffsetRef.current = nextOffset;
        pausedAtRef.current = playedSecondsRef.current;
        stopAllSourceNodes();
        audioContext.suspend().catch(() => {});
        audioContext.close().catch(() => {});
        audioContextRef.current = null;
        resetDecodedSegmentCache();
        isPlayingSegmentRef.current = false;
      } else {
        pausedAtRef.current = playedSecondsRef.current;
        stopAllSourceNodes();
        if (audioContextRef.current) {
          audioContextRef.current.suspend().catch(() => {});
          audioContextRef.current.close().catch(() => {});
          audioContextRef.current = null;
          resetDecodedSegmentCache();
        }
      }

      if (wasHtmlPlaying && htmlAudio) {
        const safeHtmlTime = Number.isFinite(htmlTime as number)
          ? Math.max(0, htmlTime as number)
          : pausedAtRef.current;
        pausedAtRef.current = safeHtmlTime;
        htmlAudio.pause();
      }

      releaseExclusive();
    },
    [
      releaseExclusive,
      requestExclusive,
      resetDecodedSegmentCache,
      stopAllSourceNodes,
    ],
  );

  // Play audio from OSS URL
  const playFromUrl = useCallback(
    (startAtSeconds: number = 0) => {
      const url = audioUrlRef.current;
      if (!url) return;

      isPausedRef.current = false;
      pausedAtRef.current = 0;
      segmentOffsetRef.current = 0;
      segmentStartTimeRef.current = 0;
      segmentDurationRef.current = 0;

      const sessionId = startPlaySession();
      requestExclusive(stopPlayback);

      if (!audioRef.current) {
        audioRef.current = new Audio();
      }

      const audio = audioRef.current;
      audio.onended = () => {
        if (!isSessionActive(sessionId)) return;
        setIsPlaying(false);
        isPlayingRef.current = false;
        setIsLoading(false);
        setIsWaitingForSegment(false);
        onPlayStateChangeRef.current?.(false);
        onEndedRef.current?.();
        releaseExclusive();
      };
      audio.onerror = () => {
        if (!isSessionActive(sessionId)) return;
        setIsPlaying(false);
        isPlayingRef.current = false;
        setIsLoading(false);
        setIsWaitingForSegment(false);
        onPlayStateChangeRef.current?.(false);
        releaseExclusive();
      };
      audio.oncanplay = () => {
        if (!isSessionActive(sessionId)) return;
        setIsLoading(false);
      };

      audio.src = url;
      setIsLoading(true);
      setIsWaitingForSegment(false);

      const seekTarget = Number.isFinite(startAtSeconds)
        ? Math.max(0, startAtSeconds)
        : 0;
      try {
        if (seekTarget > 0) {
          audio.currentTime = seekTarget;
        }
      } catch {
        // Some browsers require metadata before seeking; we'll best-effort seek later.
      }

      audio
        .play()
        .then(() => {
          if (!isSessionActive(sessionId)) return;
          setIsPlaying(true);
          isPlayingRef.current = true;
          onPlayStateChangeRef.current?.(true);
        })
        .catch(err => {
          if (!isSessionActive(sessionId)) return;
          console.error('Failed to play audio:', err);
          setIsPlaying(false);
          isPlayingRef.current = false;
          setIsLoading(false);
          setIsWaitingForSegment(false);
          releaseExclusive();
        });
    },
    [
      isSessionActive,
      releaseExclusive,
      requestExclusive,
      startPlaySession,
      stopPlayback,
    ],
  );

  // Play a single segment by index
  const playSegmentByIndex = useCallback(
    async function playSegmentByIndex(
      index: number,
      sessionId: number,
      startOffsetSeconds: number = 0,
    ) {
      if (!isSessionActive(sessionId)) {
        isPlayingSegmentRef.current = false;
        return;
      }

      // Prevent concurrent calls - if already playing a segment, skip
      if (isPlayingSegmentRef.current) {
        return;
      }

      const segments = segmentsRef.current;

      // Check if segment is available
      if (index >= segments.length) {
        // Segment not available yet
        if (isStreamingRef.current) {
          // Still streaming, wait for more segments
          setIsWaitingForSegment(true);
          setIsLoading(true);
          currentSegmentIndexRef.current = index;
          return;
        } else {
          // Streaming is complete and we reached the end of the received segments.
          // Do NOT auto-switch to the final URL here: providers may report 0/incorrect
          // `durationMs` per segment, which can cause overlap/replay when seeking.
          setIsPlaying(false);
          isPlayingRef.current = false;
          setIsLoading(false);
          setIsWaitingForSegment(false);
          onPlayStateChangeRef.current?.(false);
          onEndedRef.current?.();
          releaseExclusive();
          return;
        }
      }

      // Acquire lock
      pendingStreamRef.current = false;
      isPlayingSegmentRef.current = true;

      try {
        setIsWaitingForSegment(false);
        setIsLoading(true);

        // Initialize AudioContext if needed
        if (!audioContextRef.current) {
          audioContextRef.current = createAudioContext();
        }

        const audioContext = audioContextRef.current;

        // Resume context if suspended (may fail without user gesture in some browsers)
        await resumeAudioContext(audioContext);
        if (!isSessionActive(sessionId)) {
          isPlayingSegmentRef.current = false;
          return;
        }

        const segment = segments[index];
        currentSegmentIndexRef.current = index;

        const audioBuffer = await decodeSegmentBuffer(audioContext, segment);
        if (!isSessionActive(sessionId)) {
          isPlayingSegmentRef.current = false;
          return;
        }

        const initialOffset = Number.isFinite(startOffsetSeconds)
          ? Math.max(0, startOffsetSeconds)
          : 0;
        segmentOffsetRef.current = initialOffset;
        segmentStartTimeRef.current = audioContext.currentTime;
        segmentDurationRef.current = audioBuffer.duration || 0;

        const sourceNode = playAudioBuffer(
          audioContext,
          audioBuffer,
          () => {
            if (!isSessionActive(sessionId)) return;
            // Release lock before playing next segment
            isPlayingSegmentRef.current = false;
            const remainingSeconds = Math.max(
              0,
              (audioBuffer.duration || 0) - initialOffset,
            );
            playedSecondsRef.current += remainingSeconds;
            segmentOffsetRef.current = 0;
            segmentDurationRef.current = 0;
            // Play next segment
            if (isPlayingRef.current) {
              playSegmentByIndex(index + 1, sessionId);
            }
          },
          initialOffset,
        );
        activeSourceNodesRef.current.add(sourceNode);
        const originalOnEnded = sourceNode.onended as any;
        sourceNode.onended = event => {
          activeSourceNodesRef.current.delete(sourceNode);
          originalOnEnded?.(event);
        };
        sourceNodeRef.current = sourceNode;
        setIsLoading(false);
        setIsPlaying(true);
        isPlayingRef.current = true;
        onPlayStateChangeRef.current?.(true);
        predecodeSegmentByIndex(index + 1, sessionId, audioContext);
      } catch (error) {
        console.error('Failed to play audio segment:', error);
        // Release lock so future attempts can proceed.
        isPlayingSegmentRef.current = false;
        setIsLoading(false);
        setIsWaitingForSegment(false);

        // Try next segment when we are already in a play session.
        if (isPlayingRef.current) {
          playSegmentByIndex(index + 1, sessionId);
          return;
        }
        releaseExclusive();
      }
    },
    [
      decodeSegmentBuffer,
      isSessionActive,
      predecodeSegmentByIndex,
      releaseExclusive,
    ],
  );

  // Start playback from segments
  const playFromSegments = useCallback(
    async (forceStreaming: boolean = false) => {
      const sessionId = startPlaySession();
      requestExclusive(stopPlayback);
      isPausedRef.current = false;
      pausedAtRef.current = 0;
      segmentOffsetRef.current = 0;
      segmentStartTimeRef.current = 0;
      segmentDurationRef.current = 0;

      if (segmentsRef.current.length === 0) {
        if (
          isStreamingRef.current ||
          forceStreaming ||
          pendingStreamRef.current
        ) {
          if (forceStreaming) {
            pendingStreamRef.current = true;
          }
          // No segments yet but streaming, wait
          setIsWaitingForSegment(true);
          setIsLoading(true);
          setIsPlaying(true);
          isPlayingRef.current = true;
          currentSegmentIndexRef.current = 0;
          playedSecondsRef.current = 0;
          onPlayStateChangeRef.current?.(true);
          return;
        }
        releaseExclusive();
        return;
      }

      pendingStreamRef.current = false;
      setIsLoading(true);
      currentSegmentIndexRef.current = 0;
      playedSecondsRef.current = 0;
      await playSegmentByIndex(0, sessionId);
    },
    [
      playSegmentByIndex,
      releaseExclusive,
      requestExclusive,
      startPlaySession,
      stopPlayback,
    ],
  );

  const resumeFromSegments = useCallback(() => {
    const sessionId = startPlaySession();
    requestExclusive(stopPlayback);
    isPausedRef.current = false;
    pausedAtRef.current = 0;
    setIsLoading(true);
    setIsWaitingForSegment(false);
    setIsPlaying(true);
    isPlayingRef.current = true;

    const resumeIndex = currentSegmentIndexRef.current;
    const segments = segmentsRef.current;

    if (segments.length === 0) {
      if (isStreamingRef.current || pendingStreamRef.current) {
        setIsWaitingForSegment(true);
        return;
      }
      setIsPlaying(false);
      isPlayingRef.current = false;
      setIsLoading(false);
      releaseExclusive();
      return;
    }

    if (resumeIndex >= segments.length) {
      if (isStreamingRef.current) {
        setIsWaitingForSegment(true);
        return;
      }
      setIsPlaying(false);
      isPlayingRef.current = false;
      setIsLoading(false);
      releaseExclusive();
      return;
    }

    playSegmentByIndex(resumeIndex, sessionId, segmentOffsetRef.current);
  }, [
    playSegmentByIndex,
    releaseExclusive,
    requestExclusive,
    startPlaySession,
    stopPlayback,
  ]);

  useEffect(() => {
    const audioContext = audioContextRef.current;
    if (!audioContext || !isPlayingRef.current || isWaitingForSegment) {
      return;
    }
    predecodeSegmentByIndex(
      currentSegmentIndexRef.current + 1,
      playSessionRef.current,
      audioContext,
    );
  }, [isWaitingForSegment, predecodeSegmentByIndex, streamingSegments]);

  // Watch for new segments when waiting
  useEffect(() => {
    if (isWaitingForSegment && isPlayingRef.current) {
      const sessionId = playSessionRef.current;
      if (!isSessionActive(sessionId)) {
        return;
      }
      const nextIndex = currentSegmentIndexRef.current;
      if (nextIndex < streamingSegments.length) {
        // New segment available, continue playback
        pendingStreamRef.current = false;
        playSegmentByIndex(nextIndex, sessionId);
      } else if (!isStreaming) {
        const hasConsumedStreamingSegments =
          streamingSegments.length > 0 ||
          currentSegmentIndexRef.current > 0 ||
          playedSecondsRef.current > 0;

        // Only fall back to the final URL when no stream segment was ever
        // received, for example when the backend returns a cached complete
        // audio record. Once segment playback has started, switching into the
        // concatenated URL mid-session can seek slightly before the played
        // boundary and replay the first few words.
        if (
          shouldFallbackToCompleteUrlForWaitingStream({
            audioUrl: effectiveAudioUrl,
            streamingSegmentCount: streamingSegments.length,
            currentSegmentIndex: currentSegmentIndexRef.current,
            playedSeconds: playedSecondsRef.current,
          })
        ) {
          pendingStreamRef.current = false;
          cleanupAudio();
          playFromUrl(0);
          return;
        }

        if (pendingStreamRef.current && !hasConsumedStreamingSegments) {
          return;
        }

        setIsPlaying(false);
        isPlayingRef.current = false;
        setIsLoading(false);
        setIsWaitingForSegment(false);
        onPlayStateChangeRef.current?.(false);
        onEndedRef.current?.();
        releaseExclusive();
      }
    }
  }, [
    streamingSegments.length,
    isStreaming,
    isWaitingForSegment,
    isSessionActive,
    playSegmentByIndex,
    effectiveAudioUrl,
    cleanupAudio,
    playFromUrl,
    releaseExclusive,
  ]);

  // Handle play/pause toggle
  const togglePlay = useCallback(() => {
    if (isLoading) {
      return;
    }

    if (isPlaying) {
      // Pause
      pausePlayback();
      return;
    } else {
      if (isPausedRef.current) {
        if (useOssUrl && effectiveAudioUrl) {
          playFromUrl(pausedAtRef.current);
          return;
        }
        if (
          streamingSegments.length > 0 ||
          isStreaming ||
          pendingStreamRef.current
        ) {
          resumeFromSegments();
          return;
        }
        if (effectiveAudioUrl) {
          playFromUrl(pausedAtRef.current);
          return;
        }
      }
      // Play
      if (useOssUrl) {
        playFromUrl();
      } else if (streamingSegments.length > 0 || isStreaming) {
        playFromSegments();
      } else if (onRequestAudio) {
        pendingStreamRef.current = true;
        const requestSessionId = playSessionRef.current;
        setIsWaitingForSegment(false);
        playFromSegments(true);
        onRequestAudio()
          .then(result => {
            if (playSessionRef.current !== requestSessionId) return;
            const url = result?.audio_url || result?.audioUrl || undefined;
            if (!url) {
              return;
            }
            setLocalAudioUrl(url);
            audioUrlRef.current = url;
          })
          .catch(err => {
            if (playSessionRef.current !== requestSessionId) return;
            console.error('Failed to request audio:', err);
            pendingStreamRef.current = false;
            stopPlayback();
          });
      }
    }
  }, [
    isPlaying,
    isLoading,
    effectiveAudioUrl,
    useOssUrl,
    isStreaming,
    pausePlayback,
    playFromUrl,
    playFromSegments,
    resumeFromSegments,
    streamingSegments.length,
    onRequestAudio,
    stopPlayback,
  ]);

  useImperativeHandle(
    ref,
    () => ({
      togglePlay,
      play: () => {
        if (!isPlayingRef.current) {
          togglePlay();
        }
      },
      pause: (options?: { traceId?: string }) => {
        pausePlayback(options);
      },
    }),
    [pausePlayback, togglePlay],
  );

  // Cleanup on unmount
  useEffect(() => {
    return () => {
      isPlayingRef.current = false;
      playSessionRef.current += 1;
      pendingStreamRef.current = false;
      cleanupAudio();
      releaseExclusive();
      if (audioContextRef.current) {
        audioContextRef.current.close();
      }
    };
  }, [cleanupAudio, releaseExclusive]);

  // Auto-play when enabled and audio is available
  // Track previous autoPlay value to detect changes
  const prevAutoPlayRef = useRef(autoPlay);
  const hasAutoPlayedForCurrentContentRef = useRef(false);

  useEffect(() => {
    // Reset auto-played flag when autoPlay changes from false to true
    // This allows queue-based playback to trigger
    if (autoPlay && !prevAutoPlayRef.current) {
      hasAutoPlayedForCurrentContentRef.current = false;
    }
    prevAutoPlayRef.current = autoPlay;

    // Auto-play when:
    // 1. autoPlay is true
    // 2. Not currently playing
    // 3. Not disabled
    // 4. Haven't auto-played for this content yet
    // 5. Has audio content or is streaming
    if (
      autoPlay &&
      !isPlaying &&
      !isLoading &&
      !disabled &&
      !hasAutoPlayedForCurrentContentRef.current &&
      !isPausedRef.current
    ) {
      if (useOssUrl && effectiveAudioUrl) {
        hasAutoPlayedForCurrentContentRef.current = true;
        playFromUrl();
      } else if (streamingSegments.length > 0 || isStreaming) {
        hasAutoPlayedForCurrentContentRef.current = true;
        playFromSegments();
      }
    }
  }, [
    autoPlay,
    isPlaying,
    isLoading,
    disabled,
    streamingSegments.length,
    isStreaming,
    useOssUrl,
    effectiveAudioUrl,
    playFromSegments,
    playFromUrl,
  ]);

  // Don't render if no audio available and not streaming
  if (!hasAudio && !isStreaming && !alwaysVisible) {
    return null;
  }

  const isButtonDisabled =
    disabled || (!hasAudio && !isStreaming && !onRequestAudio);

  const playLabel = previewMode
    ? t('module.chat.ttsSynthesisPreview')
    : t('module.chat.playAudio');

  const ariaLabel = isLoading
    ? t('module.chat.audioLoading')
    : isPlaying
      ? t('module.chat.pauseAudio')
      : playLabel;

  const button = (
    <button
      type='button'
      aria-label={ariaLabel}
      aria-pressed={isPlaying}
      disabled={isButtonDisabled}
      onClick={togglePlay}
      className={cn(
        'inline-flex items-center justify-center',
        'w-[22px] h-[22px]',
        'rounded',
        'transition-colors duration-200',
        'hover:bg-gray-100',
        isButtonDisabled && 'opacity-50 cursor-not-allowed',
        !isButtonDisabled && 'cursor-pointer',
        className,
      )}
    >
      {isLoading ? (
        <Loader2
          size={size}
          className='animate-spin text-[#55575E]'
        />
      ) : isPlaying ? (
        <Pause
          size={size}
          strokeWidth={2}
          stroke='currentColor'
          fill='currentColor'
          className='text-[#55575E]'
        />
      ) : (
        <Volume2
          size={size}
          strokeWidth={2}
          stroke='currentColor'
          className='text-[#55575E]'
        />
      )}
    </button>
  );

  return (
    <TooltipProvider delayDuration={150}>
      <Tooltip>
        <TooltipTrigger asChild>
          {isButtonDisabled ? (
            <span className='inline-flex'>{button}</span>
          ) : (
            button
          )}
        </TooltipTrigger>
        <TooltipContent
          side='top'
          className='bg-black text-white border-none'
        >
          {ariaLabel}
        </TooltipContent>
      </Tooltip>
    </TooltipProvider>
  );
}

export const AudioPlayer = forwardRef(AudioPlayerBase);

AudioPlayer.displayName = 'AudioPlayer';

export default AudioPlayer;
