import APIClient
import AsyncAlgorithms
import AVFoundation
import BlendedCreateClient
import ClipPollingClient
import Combine
import ComponentLibrary
import ComposableArchitecture
import EventBusClient
import Foundation
import Localization
import LyricsClient
import MusicPlayerClient
import OpenAPIRuntime
import os.log
import PlayerUtilities
import Utilities

// swiftlint:disable file_length

private let log = Logger(category: "OmniPlayerClient")

public enum OmniPlayerEvent {
    case clipsLoaded([Clip])
    // Provide `index` for automatic or programmatic clip changes
    // so the carousel UI stays in sync with duplicate clips.
    case clipChanged(Clip, index: Int? = nil)

    case playbackStateChanged(AVPlayer.TimeControlStatus)
    case playbackTimeUpdated(currentTime: CMTime)
    case playbackScrubbingTimeout

    case fetchingMoreClips
    case moreClipsAdded([Clip])
    case errorFetchingClips(Error)

    case queueUpdated([Clip])
    case clipReplaced(Clip, at: Int)

    case timeSyncedCommentsLoaded([ClipComment])
    case timeSyncedCommentShouldShow(ClipComment)
    case timeSyncedCommentShouldHide

    case lyricsLoaded(Clip, LyricsDataV2)
    case lyricsFailed(Clip, Error)
}

@DependencyClient
public struct OmniPlayerClient {
    // Stream for events
    public var stream: @Sendable () -> AsyncStream<OmniPlayerEvent> = { .never }

    // Setup and cleanup
    public var setup: @Sendable (_ userId: String?) throws -> Void = { _ in }
    public var teardown: @Sendable () -> Void = {}

    // Playback configuration
    public var setPlaybackConfiguration: @Sendable (PlaybackConfiguration) -> Void = { _ in }
    public var getPlaybackConfiguration: @Sendable () async -> PlaybackConfiguration = { PlaybackConfiguration() }
    public var setPlaybackConfigurationOverride: @Sendable (PlaybackConfiguration) -> Void = { _ in }
    public var clearPlaybackConfigurationOverrides: @Sendable () -> Void = {}

    // Queue overrides
    public var setQueueOverride: @Sendable ([Clip], NewSongCause, SessionContext) -> Void = { _, _, _ in }
    public var clearQueueOverride: @Sendable (_ autoPlay: Bool, _ cause: PauseSongCause) -> Void = { _, _ in }

    // Player controls
    public var playCurrentClip: @Sendable () -> Void = {}
    public var pauseCurrentClip: @Sendable () -> Void = {}
    public var togglePlayPause: @Sendable () -> Void = {}
    public var playClipAtIndex: @Sendable (Int) -> Void = { _ in }
    public var playNextClip: @Sendable () -> Void = {}
    public var playPreviousClip: @Sendable () -> Void = {}
    public var cancelPendingClipChanges: @Sendable () -> Void = {}
    public var seekTo: @Sendable (CMTime) -> Void = { _ in }
    public var seekToFromCommentTrackTimestamp: @Sendable (CMTime) -> Void = { _ in }
    public var restartCurrentClip: @Sendable () -> Void = {}

    // Player attachment
    public var detachFromPlayer: @Sendable () async -> Void = {}
    public var attachToPlayer: @Sendable () async -> Void = {}
    public var isAttachedToPlayer: @Sendable () async -> Bool = { true }

    // Queue management
    public var updateClipPositionInQueue: @Sendable (_ id: ClipID, _ from: Int, _ to: Int) -> Void = { _, _, _ in }
    public var removeClipFromQueue: @Sendable (_ id: ClipID) -> Void = { _ in }
    public var updateClipInQueue: @Sendable (_ updatedClip: Clip) -> Void = { _ in }
    public var clearQueue: @Sendable () async -> Void = {}
    public var shuffleQueue: @Sendable () async -> Void = {}
    public var replaceQueue: @Sendable (_ newQueue: [Clip], _ playAtIndex: Int, _ context: SessionContext) async -> Void = { _, _, _ in }
    public var getQueue: @Sendable () async -> [Clip] = { [] }
    public var getCurrentClip: @Sendable () async -> Clip? = { nil }
    public var addClipsToQueue: @Sendable (_ clips: [Clip]) async -> Void = { _ in }

    // Additional business logic methods
    public var prefetchNextClipAssets: @Sendable (Clip, [Clip]) -> Void = { _, _ in }

    // Video cover management
    public var replaceVideoCover: @Sendable (Clip, Bool) -> Void = { _, _ in }
    public var playVideoCover: @Sendable () -> Void = {}
    public var pauseVideoCover: @Sendable () -> Void = {}
    public var restartVideoCover: @Sendable () -> Void = {}

    // Misc
    public var toggleLike: @Sendable (Clip) -> Void = { _ in }
    public var toggleDislike: @Sendable (Clip) -> Void = { _ in }

    // Generate more clips
    public var generateMore: @Sendable (Prompt) -> Void = { _ in }

    // Infinite carousel support
    public var rotateQueueForward: @Sendable () async -> Void = {}
    public var rotateQueueBackward: @Sendable () async -> Void = {}

    // Time-synced comments
    public var getTimeSyncedComments: @Sendable (
        _ id: ClipID,
        _ searchTime: Int,
        _ searchRange: Int?,
        _ margin: Int?,
        _ numRequested: Int?,
        _ endTime: Int?
    ) -> Void = { _, _, _, _, _, _ in }

    // Lyrics
    public var getLyricsForClip: @Sendable (Clip) -> Void = { _ in }

    // Session context
    public var setContext: @Sendable (SessionContext) -> Void = { _ in }

    // Utilities
    public var getUnderlyingPlayer: @Sendable () -> AVPlayer? = { nil }
    public var getCurrentSessionId: @Sendable () async -> String?

    // Autoplay
    public var fetchAutoplayClips: @Sendable (Clip) -> Void = { _ in }
}

// MARK: - Default Implementations

public extension OmniPlayerClient {
    /// Default empty implementation with all methods doing minimal work
    static var empty: Self {
        Self()
    }
}

extension OmniPlayerClient: DependencyKey {
    public static let liveValue: Self = {
        @Dependency(\.apiClientV2) var apiClientV2
        @Dependency(MusicPlayerClient.self) var playerClient
        @Dependency(VideoCoverClient.self) var videoCoverClient
        @Dependency(OmniPlayerAnalyticsClient.self) var analyticsClient
        @Dependency(PlayCountManager.self) var playCountManager
        @Dependency(LyricsClientV2.self) var lyricsClient

        // Configure MusicPlayerClient with AVPlayer engine type
        MusicPlayerClient.configure(with: .avPlayer)

        // Create subject for events
        let subject = PassthroughSubject<OmniPlayerEvent, Never>()

        // Create state actor to manage shared state
        let state = OmniPlayerState()

        // Time-sync comments
        let timeSyncedCommentsCache = TimeSyncedCommentsCache()
        let timeSyncCommentState = TimeSyncCommentState()

        let lyricsEventTask = Task {
            @Dependency(\.lyricsClientV2.stream) var lyricsEventStream
            for await event in lyricsEventStream() {
                guard !Task.isCancelled else { break }
                switch event {
                case .didSucceedPollingLyrics(let clip, let lyricsData, _):
                    let currentClip = await state.getCurrentClip()
                    if currentClip?.id == clip.id {
                        subject.send(.lyricsLoaded(clip, lyricsData))
                    }

                case .didFailPolling(let clip, let error):
                    let currentClip = await state.getCurrentClip()
                    if currentClip?.id == clip.id {
                        subject.send(.lyricsFailed(clip, error))
                    }

                case .didStartPollingForAlignedLyrics, .didPollClipForAlignedLyrics:
                    break
                }
            }
        }

        let playbackStatusTask = Task {
            for await status in playerClient.timeControlStatus() {
                guard !Task.isCancelled else { break }
                await state.setIsPlaying(status != .paused)
                let isReplacingCurrentItem = await state.getIsReplacingCurrentItem()
                let manualPauseState = await state.getManualPauseState()
                if !isReplacingCurrentItem {
                    subject.send(.playbackStateChanged(status))
                } else if manualPauseState {
                    subject.send(.playbackStateChanged(.paused))
                }
            }
        }

        let playbackTimeTask = Task {
            for await time in playerClient.periodicTime() {
                guard !Task.isCancelled else { break }
                // Time updates are throttled in MusicPlayerClient
                let isReplacingCurrentItem = await state.getIsReplacingCurrentItem()
                guard !isReplacingCurrentItem else { continue }

                // Store the current time in state for analytics
                await state.setCurrentTime(time)

                subject.send(.playbackTimeUpdated(currentTime: time))

                await timeSyncCommentState.handleTimeUpdate(currentTime: time)
            }
        }

        let performSeek: @Sendable (CMTime) -> Void = { time in
            Task {
                await state.cancelPendingClipChangeTask()
                await playerClient.cancelPreload()

                // Cancel any pending seek to prevent race conditions
                await state.cancelPendingSeek()

                let seekTask: Task<Void, Error> = Task {
                    try await Task.sleep(for: .milliseconds(100))

                    // Track analytics for seeking
                    if let currentClip = await state.getCurrentClip() {
                        let timeSeconds = time.seconds
                        if await state.getIsPlaying() {
                            analyticsClient.trackSeekProgressBarPlaySong(currentClip, timeSeconds)
                            // Reset analytics tracking for the new position
                            analyticsClient.startPlayback(currentClip, timeSeconds)
                        } else {
                            analyticsClient.trackSeekProgressBarPauseSong(currentClip, timeSeconds)
                        }
                    }

                    try await playerClient.seek(time)
                }

                await state.setPendingSeek(seekTask)

                Task {
                    do {
                        try await seekTask.value
                    } catch is CancellationError {
                        // No-op
                    } catch {
                        subject.send(.playbackScrubbingTimeout)
                    }
                }
            }

            // Handle time sync comments scrub end
            Task { @MainActor in
                await timeSyncCommentState.handleScrubEnd(at: time, currentTime: time)
            }
        }

        let endOfTrackTask = Task {
            for await playerItem in playerClient.didPlayToEndTime() {
                guard !Task.isCancelled else { break }

                let config = await state.getPlaybackConfiguration()
                let currentClip = await state.getCurrentClip()
                let currentOwner = await playerClient.getOwner()
                let currentPosition = await getCurrentPositionSeconds()

                // If we've been detached from the player (like when SnippetPlayerClient is using
                // AVPlayerClient to play a snippet), or there's no current clip in our queue
                // then don't automatically advance through the queue
                if currentOwner != .omniPlayer || currentClip == nil {
                    // Just pause the player and don't advance
                    playerClient.pause()
                    videoCoverClient.pause()
                    subject.send(.playbackStateChanged(.paused))
                    continue
                }

                // At this point we know we have a valid clip and should follow normal queue behavior
                guard let currentClip else { continue }

                // Track song end analytics
                analyticsClient.trackSongEnd(currentClip, await getCurrentPositionSeconds())

                switch config.repeatMode {
                case .one:
                    // Replay the same track
                    await loadAndPlayClipIfNeeded(currentClip)

                    // Track analytics for auto-repeat
                    analyticsClient.trackPlayNewSong(currentClip, .autoRepeat, 0)

                    // Send status events
                    subject.send(.playbackStateChanged(.playing))

                    // Reset comment state for auto-repeat (same clip)
                    Task {
                        await setupTimeSyncedCommentsForClip(currentClip, currentTime: CMTime.zero)
                    }
                    continue

                case .all, .off:
                    // Get queue information
                    let queue = await state.getQueue()
                    let currentIndex = await state.getCurrentClipIndex()
                    let isLastTrack = currentIndex >= queue.count - 1

                    if isLastTrack {
                        if config.repeatMode == .all {
                            // Restart the queue from the beginning
                            if let firstClip = queue.first {
                                await state.setCurrentClipIndex(0)
                                await state.setManuallyPaused(false)

                                await loadAndPlayClipIfNeeded(firstClip)

                                // Track auto-advance to first track analytics
                                analyticsClient.trackPlayNewSong(firstClip, .autoAdvance, 0)

                                subject.send(.clipChanged(firstClip, index: 0))
                                subject.send(.playbackStateChanged(.playing))

                                // Fetch comments for queue restart
                                Task {
                                    await setupTimeSyncedCommentsForClip(firstClip)
                                }
                            }
                        } else {
                            // .off - Just stop at the end and let playback stop

                            // Track end of queue analytics
                            analyticsClient.trackEndOfQueue(currentClip, await getCurrentPositionSeconds())

                            playerClient.pause()
                            subject.send(.playbackStateChanged(.paused))
                        }
                    } else {
                        // Play the next track
                        let nextIndex = currentIndex + 1
                        if nextIndex < queue.count {
                            let nextClip = queue[nextIndex]
                            await state.setCurrentClipIndex(nextIndex)
                            await state.setManuallyPaused(false)
                            await loadAndPlayClipIfNeeded(nextClip)

                            // Track auto-advance to next track analytics
                            analyticsClient.trackPlayNewSong(nextClip, .autoAdvance, 0)

                            subject.send(.clipChanged(nextClip, index: nextIndex))
                            subject.send(.playbackStateChanged(.playing))

                            // Fetch comments for auto-advance
                            Task {
                                await setupTimeSyncedCommentsForClip(nextClip)
                            }
                        }
                    }
                }
            }
        }

        @Sendable
        func removeCurrentlyPlayingClip() async {
            // First mark that we're replacing the item to prevent other operations
            await state.setIsReplacingCurrentItem(true)

            // Then clear the current item
            _ = try? await playerClient.replaceCurrentItem(nil)

            // Signal that the playback time has been reset
            subject.send(.playbackTimeUpdated(currentTime: .zero))
        }

        @Sendable
        func setupTimeSyncedCommentsForClip(_ clip: Clip, currentTime: CMTime? = nil) async {
            await timeSyncCommentState.setCurrentClipID(clip.id)
            await timeSyncCommentState.setTrackDuration(clip.duration)
            let clipRemoteID = clip.id.remoteId

            // Check if we have cached comments
            if let cachedComments = await timeSyncedCommentsCache.getCommentsForClip(clipRemoteID) {
                // Validate clip is still current before updating state
                let currentClipID = await timeSyncCommentState.getCurrentClipID()
                if currentClipID?.remoteId == clip.id.remoteId {
                    // Use provided currentTime or nil for new clips (starts from 0:00)
                    await timeSyncCommentState.updateComments(cachedComments, for: clip.id, currentTime: currentTime)
                }
            } else {
                await _internal_fetchTimeSyncedComments(clipID: clip.id)
            }
        }

        @Sendable
        func getLyricsIfNeededForClip(_ clip: Clip) {
            guard !clip.isInstrumental else { return }
            lyricsClient.getLyricsForClip(clip)
        }

        @Sendable
        func prefetchClipAssets(for clips: [Clip]) {
            let preloadTasks = [
                Task.detached { [clips] in
                    let urls = clips.compactMap { URL(string: $0.audioUrl) }
                    await playerClient.preloadAssets(urls)
                },
                Task.detached { [clips] in
                    RemoteImagePrefetcher.shared.loadImages(urls: clips.compactMap { URL(string: $0.largeImageUrl) })
                },
                Task { [clips] in
                    for clip in clips {
                        try? Task.checkCancellation()
                        if let coverUrl = clip.videoCoverUrl,
                           let url = URL(string: coverUrl)
                        {
                            videoCoverClient.cacheVideoCover(url)
                        }
                    }
                },
                Task { [clips] in
                    for clip in clips {
                        try? Task.checkCancellation()
                        getLyricsIfNeededForClip(clip)
                    }
                },
            ]

            // Register the tasks with the state actor
            Task {
                for task in preloadTasks {
                    await state.addPreloadTask(task)
                }
            }
        }

        @Sendable
        func prefetchNextClipAssets(for clip: Clip, in queue: [Clip]) {
            guard let index = queue.firstIndex(of: clip),
                  index < queue.count - 1 else { return }

            let nextClips = Array(queue.dropFirst(index + 1).prefix(3))

            prefetchClipAssets(for: nextClips)
        }

        @Sendable
        func fetchClipsForAutoplay(_ clip: Clip, config: PlaybackConfiguration) async throws -> [Clip] {
            switch config.autoplayMode {
            case .off:
                return []
            case .similarClips:
                return try await apiClientV2.getSimilarClips(clip.id.remoteId, 10, false)
            }
        }

        @Sendable
        func appendAndNotifyQueueUpdate(_ clips: [Clip]) async {
            await state.appendToQueue(clips)
            let currentQueue = await state.getQueue()

            subject.send(.moreClipsAdded(clips))
            subject.send(.queueUpdated(currentQueue))

            prefetchClipAssets(for: Array(clips.prefix(3)))
        }

        @Sendable
        func loadClipWithQueueOverride(
            clip: Clip,
            clipQueue: [Clip],
            shouldPlay: Bool = false,
            cause: NewSongCause = .vanilla,
            context: SessionContext
        ) async {
            // Cancel any preloading when we override the queue
            Task {
                await playerClient.cancelPreload()
            }

            await state.setQueueOverride(clipQueue)
            await state.setIsPlaying(shouldPlay)

            if !shouldPlay {
                // Pause any currently playing media
                playerClient.pause()
                videoCoverClient.pause()
            }

            // Clear any existing lyrics data before loading new lyrics
            subject.send(.lyricsLoaded(clip, .empty))

            analyticsClient.setContextOverride(context)

            // Load the clip
            await loadClip(clip)

            await state.setIsReplacingCurrentItem(false)

            // Load the clip (just audio, we'll handle video cover separately)
            _ = try? await playerClient.replaceCurrentItem(clip.audioUrl)

            // Load lyrics
            getLyricsIfNeededForClip(clip)

            // Play if needed
            if shouldPlay {
                playerClient.play()

                // Load and play video cover if successful (like loadAndPlayClip does)
                if await loadVideoCover(for: clip) {
                    videoCoverClient.play()
                }
            }

            // Clear replacing flag after all operations to ensure status events flow correctly
            await state.setIsReplacingCurrentItem(false)

            if shouldPlay {
                trackClipPlay(clip, cause: cause)
            }

            // Send events with the queue override
            // If we're coming from Hooks Feed, wait 0.5 seconds until
            // the event stream is set up to avoid race conditions.
            // The OmniPlayer from Hooks feed follows a different
            // animation path that we give an extra 0.5s to settle
            // before calling `.task`
            if case .hook = cause {
                try? await Task.sleep(for: .seconds(0.5))
            }

            subject.send(.clipChanged(clip))

            let currentQueue = await state.getQueue()
            subject.send(.queueUpdated(currentQueue))

            subject.send(.playbackStateChanged(shouldPlay ? .playing : .paused))

            // Fetch comments for queue override
            await setupTimeSyncedCommentsForClip(clip)
        }

        @Sendable
        func loadVideoCover(for clip: Clip) async -> Bool {
            guard let coverUrl = clip.videoCoverUrl,
                  let url = URL(string: coverUrl)
            else { return false }

            let duration = await videoCoverClient.replaceCurrentItem(url)
            let success = duration.seconds > 0
            return success
        }

        @Sendable
        func loadClip(_ clip: Clip) async {
            // Load audio
            _ = try? await playerClient.replaceCurrentItem(clip.playableSceneUrl?.absoluteString ?? clip.audioUrl)

            // Load video cover if available
            if await loadVideoCover(for: clip) {
                // Don't auto-play the video cover here - it will be handled by the player state
            }
        }

        @Sendable
        func loadAndPlayClipIfNeeded(
            _ clip: Clip,
            autoPlay: Bool = true
        ) async {
            let startTime = Date()

            // Make sure we're ready to load this clip
            do {
                // Check for cancellation at the very start - BEFORE any expensive operations
                try Task.checkCancellation()

                getLyricsIfNeededForClip(clip)

                playerClient.setupNowPlayingInfo(
                    clip.title,
                    clip.displayName,
                    clip.largeImageUrl,
                    .zero,
                    CMTime(seconds: clip.duration, preferredTimescale: 1000)
                )

                // Load audio - immediately replace current item for quick switching
                let audioUrl = clip.playableSceneUrl?.absoluteString ?? clip.audioUrl

                let audioStart = Date()
                let duration = try await playerClient.replaceCurrentItem(audioUrl)
                let audioEnd = Date()

                try Task.checkCancellation()

                // Make sure to update the state
                await state.setIsReplacingCurrentItem(false)

                // Check if user manually paused - if so, don't auto-play the new clip
                let isCurrentlyPlaying = await state.getIsPlaying()
                let isManuallyPaused = await state.getManualPauseState()
                let wasUserPaused = !isCurrentlyPlaying && isManuallyPaused
                let shouldPlay = !wasUserPaused && autoPlay

                if shouldPlay {
                    // Only play if user didn't manually pause
                    await state.setIsPlaying(true)
                    playerClient.play()

                    // Start tracking playback for analytics
                    analyticsClient.startPlayback(clip, 0)
                } else {
                    // User manually paused, so keep it paused but still load the clip
                    await state.setIsPlaying(false)
                }

                // Check for cancellation before loading video cover
                try Task.checkCancellation()

                // Load video cover and play if successful
                if await loadVideoCover(for: clip) {
                    // Check for cancellation before playing video
                    try Task.checkCancellation()
                    if shouldPlay {
                        videoCoverClient.play()
                    }
                }

                // Final cancellation check
                try Task.checkCancellation()

                let endTime = Date()
                log.debug("🎉 loadAndPlayClip: Completed successfully for '\(clip.title)' (TOTAL: +\(Int(endTime.timeIntervalSince(startTime) * 1000))ms)")

            } catch is CancellationError {
                log.debug("🚫 loadAndPlayClip: Cancelled for '\(clip.title)'")
                // Clean up on cancellation - stop any playback that might have started
                playerClient.pause()
                videoCoverClient.pause()
            } catch {
                log.telemetry.error(error, message: "❌ loadAndPlayClip: Error loading and playing clip '\(clip.title)'")
            }
        }

        @Sendable
        func getPauseCause(forPlayCause playCause: NewSongCause) -> PauseSongCause {
            switch playCause {
            case .skipForward:
                return .skipForward
            case .skipBackward:
                return .skipBackward
            case .autoAdvance:
                return .autoAdvance
            case .autoRepeat:
                return .autoRepeat
            case .seekToStart:
                return .seekToStart
            default:
                return .manual
            }
        }

        @Sendable
        func trackClipPause(
            _ clip: Clip,
            playCause: NewSongCause,
            pauseCause: PauseSongCause = .manual,
            currentPosition: TimeInterval
        ) {
            switch pauseCause {
            case .skipForward:
                analyticsClient.trackForwardPausePreSong(clip, currentPosition)
            case .skipBackward:
                analyticsClient.trackBackwardPauseSong(clip, currentPosition)
            case .manual:
                analyticsClient.trackPauseSong(clip, currentPosition)
            case .autoAdvance, .autoRepeat, .seekToStart:
                analyticsClient.trackPlayNewSongPauseSong(clip, playCause, currentPosition)
            case .playerDetached:
                analyticsClient.trackForwardPausePreSong(clip, currentPosition)
            case .closeHooksFeedOmniPlayer(hookId: let hookId):
                analyticsClient.trackPauseSongPlayHook(clip, currentPosition, pauseCause)
            }
        }

        @Sendable
        func trackClipPlay(_ clip: Clip, cause: NewSongCause) {
            let currentPosSeconds: TimeInterval = 0
            switch cause {
            case .skipForward:
                analyticsClient.trackForwardPlayNewSong(clip, currentPosSeconds)
            case .skipBackward:
                analyticsClient.trackBackwardPlayNewSong(clip, currentPosSeconds)
            case .vanilla, .autoAdvance, .autoRepeat, .seekToStart:
                analyticsClient.trackPlayNewSong(clip, cause, currentPosSeconds)
            case .hook:
                analyticsClient.trackPlayNewSongPauseHook(clip, currentPosSeconds, cause)
            }

            Task {
                await playCountManager.startPlayCountTimer(for: clip)
            }
        }

        @Sendable
        func loadAndPlayClipAtIndex(
            _ clipIndex: Int,
            prefetchNext: Bool,
            cause: NewSongCause,
            clearPreloadTasks: Bool = false,
            autoPlay: Bool = true
        ) async {
            let startTime = Date()

            Task {
                do {
                    var index = clipIndex

                    await state.cancelPendingClipChangeTask()

                    // Check for cancellation early and often
                    try Task.checkCancellation()

                    // Expand queue if needed for infinite carousel
                    await state.expandQueueIfNeeded(for: index)
                    let queue = await state.getQueue()
                    subject.send(.queueUpdated(queue))

                    if index == queue.count {
                        index = 0
                    } else if index < 0 {
                        index = queue.count - 1
                    }

                    let nextClip = queue[index]

                    // Get the current clip index before setting the new one
                    let currentClipIndex = await state.getCurrentClipIndex()
                    await state.setCurrentClipIndex(index)

                    // Reset time first to avoid progress bar positioning issues
                    subject.send(.playbackTimeUpdated(currentTime: .zero))
                    subject.send(.clipChanged(nextClip, index: index))

                    try Task.checkCancellation()

                    // Cancel preloads if requested
                    if clearPreloadTasks {
                        await playerClient.cancelPreload()
                    }

                    try Task.checkCancellation()

                    // Check if we're pausing a currently playing clip
                    if let currentClip = await state.getCurrentClip() {
                        let currentClipIndex = await state.getCurrentClipIndex()
                        let currentPosition = await getCurrentPositionSeconds()

                        // Only track pause if we're actually playing
                        if await state.getIsPlaying() {
                            // For most cases, we can figure out the pause cause from the play cause,
                            // but some cases like pausing to close OmniPlayer when opening from a Hook,
                            // need the explicit event as you'll see in `trackClipPause` in `pauseCurrentClipImpl`.
                            let pauseCause = getPauseCause(forPlayCause: cause)
                            trackClipPause(currentClip, playCause: cause, pauseCause: pauseCause, currentPosition: currentPosition)
                        }

                        // End tracking for the previous clip
                        analyticsClient.endPlayback(currentClip, currentPosition)
                    }

                    try Task.checkCancellation()

                    // Only set to playing if we're not actively paused by user
                    let isCurrentlyPlaying = await state.getIsPlaying()
                    let isManuallyPaused = await state.getManualPauseState()
                    let wasUserPaused = !isCurrentlyPlaying && isManuallyPaused
                    if !wasUserPaused {
                        await state.setIsPlaying(true)
                    }

                    trackClipPlay(nextClip, cause: cause)

                    // Fetch comments after clipChanged event is sent to avoid race conditions
                    Task {
                        await setupTimeSyncedCommentsForClip(nextClip)
                    }

                    try Task.checkCancellation()

                    // Create a task for playing the clip - use regular Task so it inherits cancellation from parent
                    let task = Task {
                        do {
                            // Final cancellation check before starting expensive operation
                            try Task.checkCancellation()
                            await loadAndPlayClipIfNeeded(nextClip, autoPlay: autoPlay)

                            // Only report completion if we weren't cancelled
                            try Task.checkCancellation()

                            // Only prefetch when needed
                            guard prefetchNext else { return }
                            try Task.checkCancellation()
                            prefetchNextClipAssets(for: nextClip, in: queue)
                        } catch is CancellationError {
                            // Cancel all play count timers when fast-swiping
                            await playCountManager.cancelAllPendingTimers()
                            log.debug("🚫 loadAndPlayClipAtIndex: Cancelled during playback for '\(nextClip.title)'")
                        } catch {
                            log.telemetry.error(error, message: "❌ loadAndPlayClipAtIndex: Error loading and playing clip at index: \(clipIndex).")
                        }
                    }

                    // Store the task IMMEDIATELY so it can be cancelled
                    await state.setPendingClipChangeTask(task)
                    let endTime = Date()
                    log.debug("🏁 loadAndPlayClipAtIndex: Setup complete for index \(clipIndex) (TOTAL: +\(Int(endTime.timeIntervalSince(startTime) * 1000))ms)")
                } catch is CancellationError {
                    log.debug("🚫 loadAndPlayClipAtIndex: Cancelled during setup for index \(clipIndex)")
                } catch {
                    log.telemetry.error(error, message: "❌ loadAndPlayClipAtIndex: Error loading and playing clip at index: \(clipIndex).")
                }
            }
        }

        // Helper function to get current position in seconds for analytics
        @Sendable
        func getCurrentPositionSeconds() async -> TimeInterval {
            let currentTime = await state.getCurrentTime()
            return currentTime.seconds
        }

        @Sendable
        func _internal_sendCommentsValueUpdateEvent(_ forClipID: String) async {
            let commentsCacheValue = await timeSyncedCommentsCache.getCommentsForClip(forClipID) ?? []
            subject.send(.timeSyncedCommentsLoaded(commentsCacheValue))
        }

        @Sendable
        func _internal_updateCacheWithComments(_ clipID: ClipID, comments: [ClipComment]) async {
            await timeSyncedCommentsCache.updateCacheWithComments(clipID.remoteId, comments: comments)

            // Use merged comments from cache instead of just new comments
            // This ensures TimeSyncCommentState has all comments, not just the newly fetched ones
            let currentClipID = await timeSyncCommentState.getCurrentClipID()
            if currentClipID?.remoteId == clipID.remoteId {
                if let allComments = await timeSyncedCommentsCache.getCommentsForClip(clipID.remoteId) {
                    await timeSyncCommentState.updateComments(allComments, for: clipID, currentTime: await state.getCurrentTime())
                }
            }
        }

        @Sendable
        func _internal_fetchTimeSyncedComments(
            clipID: ClipID,
            searchTime: Int = 0,
            searchRange: Int? = nil,
            margin: Int? = nil,
            numRequested: Int? = nil,
            endTime: Int? = nil
        ) async {
            @Dependency(APIClientV2.self) var apiClientV2
            do {
                let clipRemoteID = clipID.remoteId

                // Check if we already have comments for this time range
                let shouldFetch = await timeSyncedCommentsCache.shouldFetchForTime(clipRemoteID, targetTime: searchTime)
                if !shouldFetch {
                    // Already have comments for this time range
                    if let cachedComments = await timeSyncedCommentsCache.getCommentsForClip(clipRemoteID) {
                        // Validate clip is still current before updating state
                        let currentClipID = await timeSyncCommentState.getCurrentClipID()
                        if currentClipID?.remoteId == clipID.remoteId {
                            await timeSyncCommentState.updateComments(cachedComments, for: clipID, currentTime: await state.getCurrentTime())
                        }
                    }
                    return
                }

                // Use provided parameters or defaults
                let finalSearchRange = searchRange
                let finalMargin = margin ?? TimeSyncCommentState.Constants.fetchMarginSeconds
                let finalNumRequested = numRequested ?? TimeSyncCommentState.Constants.commentsPerBatch
                let finalEndTime = endTime ?? {
                    // Default: 30-second windowed fetching for efficiency
                    let fetchWindow = Int(TimeSyncCommentState.Constants.fetchAheadWindowSeconds)
                    return searchTime + fetchWindow
                }()

                let comments = try await apiClientV2.getTimeSyncedComments(
                    clipID,
                    searchTime,
                    finalSearchRange,
                    finalMargin,
                    finalNumRequested,
                    finalEndTime
                )
                await _internal_updateCacheWithComments(clipID, comments: comments)
                await timeSyncedCommentsCache.markTimeRangeFetched(clipRemoteID, targetTime: searchTime)
                await _internal_sendCommentsValueUpdateEvent(clipRemoteID)
            } catch {
                log.telemetry.error(error)
            }
        }

        // Break up complex expressions to help Swift compiler
        let streamImpl: @Sendable () -> AsyncStream<OmniPlayerEvent> = {
            // Convert PassthroughSubject to AsyncStream like in ShareAssetClient
            AsyncStream { continuation in
                let cancellable = subject.sink { event in
                    continuation.yield(event)
                }
                continuation.onTermination = { _ in
                    cancellable.cancel()
                }
            }
        }

        let setupImpl: @Sendable (_ userId: String?) throws -> Void = { userId in
            Task {
                do {
                    try await playerClient.setup()

                    await playerClient.setOwner(.omniPlayer)

                    // Set up event callback for time sync state
                    await timeSyncCommentState.setEventCallback { event in
                        switch event {
                        case .shouldShowComment(let comment):
                            subject.send(.timeSyncedCommentShouldShow(comment))
                        case .shouldHideComment:
                            subject.send(.timeSyncedCommentShouldHide)
                        case .shouldFetchComments:
                            // Fetch more comments for current time position
                            Task {
                                if let currentClipID = await timeSyncCommentState.getCurrentClipID() {
                                    let currentTime = Int(await state.getCurrentTime().seconds)
                                    await _internal_fetchTimeSyncedComments(
                                        clipID: currentClipID,
                                        searchTime: currentTime
                                    )
                                }
                            }
                        }
                    }
                } catch {
                    log.telemetry.error(error)
                }

                if let userId {
                    await analyticsClient.setupSession(userId)
                }
            }
        }

        let replaceQueueImpl: @Sendable ([Clip], Int, SessionContext) async -> Void = { @Sendable newQueue, playAtIndex, context in
            Task {
                // Cancel any pending operations
                await state.cancelPendingClipChangeTask()
                await state.cancelAllPreloadTasks()
                await playerClient.cancelPreload()
                await playerClient.setOwner(.omniPlayer)

                // Check if we're pausing a currently playing clip
                if let currentClip = await state.getCurrentClip() {
                    let currentPosition = await getCurrentPositionSeconds()
                    if await state.getIsPlaying() {
                        // Track pause analytics for the current clip
                        analyticsClient.trackPlayNewSongPauseSong(currentClip, .vanilla, currentPosition)
                    }
                }

                // Clear any queue overrides first
                await state.clearQueueOverride()

                // Remove currently playing clip to avoid overlap
                await removeCurrentlyPlayingClip()

                // Replace the entire queue atomically
                await state.setQueue(newQueue)

                analyticsClient.setContext(context)

                // Validate the play index
                let validIndex = max(0, min(playAtIndex, newQueue.count - 1))

                if !newQueue.isEmpty {
                    // Set the current clip index
                    await state.setCurrentClipIndex(validIndex)

                    // Clear pause state right before loading - replaceQueue should always play
                    await state.setManuallyPaused(false)

                    // Load and play the clip at the specified index
                    let clipToPlay = newQueue[validIndex]

                    subject.send(.clipChanged(clipToPlay, index: validIndex))
                    subject.send(.queueUpdated(newQueue))

                    await loadAndPlayClipIfNeeded(clipToPlay)

                    await RemoteImagePrefetcher.shared.loadAndWaitForImages(urls: newQueue.compactMap { URL(string: $0.largeImageUrl) })

                    trackClipPlay(clipToPlay, cause: .vanilla)

                    // Notify time sync comment state and fetch comments for replaceQueue
                    await setupTimeSyncedCommentsForClip(clipToPlay)

                    // Prefetch upcoming clips
                    prefetchClipAssets(for: Array(newQueue.dropFirst(validIndex + 1).prefix(3)))
                } else {
                    // Empty queue - just update the UI
                    subject.send(.queueUpdated([]))
                }
            }
        }

        let pauseCurrentClipImpl: @Sendable (PauseSongCause) -> Void = { @Sendable cause in
            Task {
                let currentClip = await state.getCurrentClip()
                let currentPositionSeconds = await getCurrentPositionSeconds()

                // Update the playing state
                await state.setIsPlaying(false)
                // Mark as manually paused
                await state.setManuallyPaused(true)

                // Track analytics for pause
                if let currentClip {
                    trackClipPause(currentClip, playCause: .vanilla, pauseCause: cause, currentPosition: currentPositionSeconds)
                }

                // Pause using player client
                playerClient.pause()

                // Pause video cover if applicable
                videoCoverClient.pause()

                // Handle time sync comments pause
                await timeSyncCommentState.handlePlaybackPause()
            }
        }

        let playCurrentClipImpl: @Sendable () -> Void = {
            Task {
                let currentClip = await state.getCurrentClip()

                // Check the player's current item state. If the song got removed we need to set it up again.
                // NOTE: Ideally we'd also check if the underlying PlayerClient is playing the correct song, as there is a
                // chance that another player using the same PlayerClient replaced the item with something else.
                // However, there is currently no easy way to check the currently loaded URL in PlayerClient.
                // There is likely an elegant solution to this in the future.
                if let currentClip, await playerClient.getCurrentItem() == nil {
                    log.debug("⚠️ playCurrentClipImpl: Found `nil` for underlying PlayerClient.getCurrentItem(). This can happen if you forgot to load the clip before attemping to `playCurrentClip()`, or if another player uses the same underlying PlayerClient and removed the item. Reloading the item from internal state.")
                    // Store the current playback time.
                    let seekTime = await state.getCurrentTime()
                    // Replace the clip directly.
                    try? await playerClient.replaceCurrentItem(currentClip.playableSceneUrl?.absoluteString ?? currentClip.audioUrl)
                    // Seek to the previous time position.
                    try? await playerClient.seek(seekTime)
                }

                // Update the playing state
                await state.setIsPlaying(true)
                // Clear manual pause state
                await state.setManuallyPaused(false)

                // Play using player client
                playerClient.play()

                // Track analytics
                if let currentClip {
                    let currentPosition = await getCurrentPositionSeconds()
                    analyticsClient.trackPlaySong(currentClip, currentPosition)
                    analyticsClient.startPlayback(currentClip, currentPosition)
                }

                // Play video cover if applicable
                videoCoverClient.play()

                // Handle time sync comments resume
                let currentTime = await state.getCurrentTime()
                await timeSyncCommentState.handlePlaybackResume(currentTime: currentTime)
            }
        }

        return .init(
            stream: streamImpl,
            setup: setupImpl,
            teardown: {
                // Clean up subscribers
                playbackStatusTask.cancel()
                playbackTimeTask.cancel()
                endOfTrackTask.cancel()
                lyricsEventTask.cancel()

                // Call teardown on videoCoverClient
                videoCoverClient.teardown()
            },
            // Playback configuration
            setPlaybackConfiguration: { config in
                Task {
                    await state.setPlaybackConfiguration(config)
                }
            },
            getPlaybackConfiguration: {
                await state.getPlaybackConfiguration()
            },
            setPlaybackConfigurationOverride: { override in
                Task {
                    await state.setPlaybackConfigurationOverride(override)
                }
            },
            clearPlaybackConfigurationOverrides: {
                Task {
                    await state.clearPlaybackConfigurationOverride()
                }
            },
            // Queue overrides
            setQueueOverride: { newQueue, cause, context in
                Task {
                    guard let firstClip = newQueue.first else { return }
                    await removeCurrentlyPlayingClip()
                    await loadClipWithQueueOverride(clip: firstClip, clipQueue: newQueue, shouldPlay: true, cause: cause, context: context)
                }
            },
            clearQueueOverride: { @Sendable autoPlay, cause in
                Task {
                    // Log the pause event before clearing the queue override
                    if let currentClip = await state.getCurrentClip() {
                        let currentPosition = await getCurrentPositionSeconds()
                        trackClipPause(currentClip, playCause: .vanilla, pauseCause: cause, currentPosition: currentPosition)
                    }

                    pauseCurrentClipImpl(cause)

                    await state.clearQueueOverride()

                    // Remove the current clip
                    await removeCurrentlyPlayingClip()

                    analyticsClient.clearContextOverride()

                    // Send queue updated event with the original source queue
                    let originalQueue = await state.getSourceQueue()
                    subject.send(.queueUpdated(originalQueue))

                    // Set the same clip again
                    if let currentClip = await state.getCurrentClip() {
                        let currentIndex = await state.getCurrentClipIndex()
                        await loadAndPlayClipAtIndex(currentIndex, prefetchNext: true, cause: .vanilla, autoPlay: autoPlay)
                    }
                }
            },
            // Player controls
            playCurrentClip: playCurrentClipImpl,
            pauseCurrentClip: { pauseCurrentClipImpl(.manual) },
            togglePlayPause: { @Sendable in
                Task {
                    // Toggle playing state
                    let isCurrentlyPlaying = await state.getIsPlaying()
                    await state.setIsPlaying(!isCurrentlyPlaying)

                    // Update manual pause state
                    await state.setManuallyPaused(isCurrentlyPlaying)

                    if !isCurrentlyPlaying {
                        // Play
                        playerClient.play()
                        videoCoverClient.play()

                        // Track analytics
                        if let currentClip = await state.getCurrentClip() {
                            let currentPosition = await getCurrentPositionSeconds()
                            analyticsClient.trackPlaySong(currentClip, currentPosition)
                            analyticsClient.startPlayback(currentClip, currentPosition)
                        }
                    } else {
                        // Track analytics for pause
                        if let currentClip = await state.getCurrentClip() {
                            analyticsClient.trackPauseSong(currentClip, await getCurrentPositionSeconds())
                        }

                        // Pause
                        playerClient.pause()
                        videoCoverClient.pause()
                    }
                }
            },
            playClipAtIndex: { index in
                // Cancel any pending clip change task first
                Task {
                    // Clear manual pause state to ensure playback starts
                    await state.setManuallyPaused(false)

                    // Wait for the current clip to be removed
                    await removeCurrentlyPlayingClip()

                    // Check if we're actually just playing the next or previous clip
                    let currentIndex = await state.getCurrentClipIndex()
                    if index == currentIndex + 1 {
                        await loadAndPlayClipAtIndex(index, prefetchNext: true, cause: .skipForward)
                    } else if index == currentIndex - 1 {
                        await loadAndPlayClipAtIndex(index, prefetchNext: true, cause: .skipBackward)
                    } else {
                        await loadAndPlayClipAtIndex(index, prefetchNext: true, cause: .vanilla, clearPreloadTasks: true)
                    }
                }
            },
            playNextClip: { @Sendable in
                // Cancel any pending clip change task first
                Task {
                    // Clear any manual pause state - user wants to play the next clip
                    await state.setManuallyPaused(false)

                    // Wait for the current clip to be removed
                    await removeCurrentlyPlayingClip()

                    let currentIndex = await state.getCurrentClipIndex()
                    let nextIndex = currentIndex + 1

                    await loadAndPlayClipAtIndex(nextIndex, prefetchNext: true, cause: .skipForward)
                }
            },
            playPreviousClip: { @Sendable in
                // Cancel any pending clip change task first
                Task {
                    // Clear any manual pause state - user wants to play the previous clip
                    await state.setManuallyPaused(false)

                    // Wait for the current clip to be removed
                    await removeCurrentlyPlayingClip()

                    let currentIndex = await state.getCurrentClipIndex()
                    let prevIndex = currentIndex - 1

                    await loadAndPlayClipAtIndex(prevIndex, prefetchNext: false, cause: .skipBackward)
                }
            },
            cancelPendingClipChanges: { @Sendable in
                Task {
                    await state.cancelPendingClipChangeTask()
                }
            },
            seekTo: performSeek,
            seekToFromCommentTrackTimestamp: { time in
                Task {
                    if let currentClip = await state.getCurrentClip() {
                        let timeSeconds = time.seconds
                        // Track comment track timestamp analytics first
                        analyticsClient.trackCommentTrackTimestampTapped(currentClip, timeSeconds)
                    }
                }
                performSeek(time)
            },
            restartCurrentClip: {
                Task {
                    if let currentClip = await state.getCurrentClip() {
                        // Get current position before restarting
                        let currentPosSeconds = await getCurrentPositionSeconds()

                        analyticsClient.trackBackwardRepeatSong(currentClip, currentPosSeconds)

                        // Reset analytics tracking for the restarted track
                        if await state.getIsPlaying() {
                            analyticsClient.startPlayback(currentClip, 0)
                        }
                    }

                    try? await playerClient.seek(.zero)
                    videoCoverClient.restart()
                    subject.send(.playbackTimeUpdated(currentTime: .zero))

                    // Check for comments when track restarts at 0:00
                    await timeSyncCommentState.handleScrubEnd(at: .zero, currentTime: .zero)
                }
            },
            // Player attachment
            detachFromPlayer: { @Sendable in
                Task {
                    await playerClient.setOwner(.none)
                    pauseCurrentClipImpl(.playerDetached)
                }
            },
            attachToPlayer: { @Sendable in
                Task {
                    await playerClient.setOwner(.omniPlayer)
                    playCurrentClipImpl()
                }
            },
            isAttachedToPlayer: {
                await playerClient.getOwner() == .omniPlayer
            },
            // Queue management
            updateClipPositionInQueue: { @Sendable _, from, to in
                Task {
                    await state.updateClipPositionInQueue(from: from, to: to)
                    let queue = await state.getQueue()
                    // Notify of queue update
                    subject.send(.queueUpdated(queue))
                }
            },
            removeClipFromQueue: { @Sendable id in
                Task {
                    var queue = await state.getQueue()
                    guard let index = queue.firstIndex(where: { $0.id == id }) else { return }

                    let removedClip = await state.removeClipFromQueue(at: index)
                    let currentClipIndex = await state.getCurrentClipIndex()
                    let newQueue = await state.getQueue()

                    if !newQueue.isEmpty {
                        if index == currentClipIndex {
                            // If we removed the current clip, play the clip at the same index
                            // (which is now the next clip that was after the one we removed)

                            // We need to invoke this method properly since we're inside a closure
                            let clipToPlay = newQueue[currentClipIndex]

                            // Play the clip in a background task
                            Task.detached {
                                await loadAndPlayClipIfNeeded(clipToPlay)
                            }

                            // Notify of changes
                            subject.send(.queueUpdated(newQueue))
                            subject.send(.clipChanged(clipToPlay, index: currentClipIndex))
                        }
                        // If we removed a clip that was before the current one,
                        // the currentClipIndex has already been adjusted by state.removeClipFromQueue
                        // so we don't need to do anything special here
                    } else {
                        // If there are no clips left, stop the player
                        playerClient.pause()
                        videoCoverClient.pause()
                    }

                    // Notify of queue update
                    subject.send(.queueUpdated(newQueue))
                }
            },
            updateClipInQueue: { @Sendable updatedClip in
                Task {
                    // Update clip in the state
                    await state.updateClipInQueue(updatedClip)

                    // Get the updated queue and notify of queue update
                    let queue = await state.getQueue()
                    subject.send(.queueUpdated(queue))
                }
            },
            clearQueue: { @Sendable in
                Task {
                    // Update state
                    await state.clearQueue()

                    // Notify of queue update
                    subject.send(.queueUpdated([]))
                }
            },
            shuffleQueue: { @Sendable in
                Task {
                    // Shuffle the queue and get the new index
                    guard await state.shuffleQueue() != nil else { return }

                    // Get the updated queue
                    let queue = await state.getQueue()
                    // Notify of queue update
                    subject.send(.queueUpdated(queue))
                }
            },
            replaceQueue: replaceQueueImpl,
            getQueue: {
                // Properly awaiting access to actor state
                await state.getQueue()
            },
            getCurrentClip: {
                // Properly awaiting access to actor state
                await state.getCurrentClip()
            },
            addClipsToQueue: { clips in
                Task {
                    await appendAndNotifyQueueUpdate(clips)
                }
            },
            // Business logic methods for omni player
            prefetchNextClipAssets: { currentClip, queue in
                prefetchNextClipAssets(for: currentClip, in: queue)
            },
            // Video cover management
            replaceVideoCover: { clip, autoPlay in
                Task {
                    guard let coverUrl = clip.videoCoverUrl,
                          let url = URL(string: coverUrl) else { return }

                    // Since this is an async function, we can directly await
                    _ = await videoCoverClient.replaceCurrentItem(url)
                    if autoPlay {
                        videoCoverClient.play()
                    }
                }
            },
            playVideoCover: {
                videoCoverClient.play()
            },
            pauseVideoCover: {
                videoCoverClient.pause()
            },
            restartVideoCover: {
                videoCoverClient.restart()
            },
            toggleLike: { clip in
                Task {
                    @Dependency(\.eventBus.sendClipEvent) var sendClipEvent
                    @Dependency(\.apiClientV2) var apiClientV2

                    // Update in OmniPlayerClient's internal queue
                    await state.updateClipInQueue(clip)

                    // Send event to notify other parts of the app
                    sendClipEvent(.toggledLike(clip))

                    do {
                        try await apiClientV2.setReaction(clip, clip.isLiked, clip.isDisliked, nil)
                    } catch {
                        var revertedClip = clip
                        revertedClip.isLiked.toggle()
                        await state.updateClipInQueue(revertedClip)
                        sendClipEvent(.toggledLike(revertedClip))
                    }
                }
            },
            toggleDislike: { clip in
                Task {
                    @Dependency(\.eventBus.sendClipEvent) var sendClipEvent
                    @Dependency(\.apiClientV2) var apiClientV2

                    // Update in OmniPlayerClient's internal queue
                    await state.updateClipInQueue(clip)

                    // Send event to notify other parts of the app
                    sendClipEvent(.toggledLike(clip))

                    do {
                        try await apiClientV2.setReaction(clip, clip.isLiked, clip.isDisliked, nil)
                    } catch {
                        var revertedClip = clip
                        revertedClip.isDisliked.toggle()
                        await state.updateClipInQueue(revertedClip)
                        sendClipEvent(.toggledLike(revertedClip))
                    }
                }
            },
            generateMore: { prompt in
                var hasStartedCompletionPolling: Bool = false
                Task {
                    @Dependency(BlendedCreateClient.self) var blendedCreateClient
                    @Dependency(APIClientV2.self) var apiClientV2
                    @Dependency(\.continuousClock) var clock
                    @Dependency(ClipPollingClient.self) var clipPollingClient

                    do {
                        // Convert Prompt to BlendedCreatePrompt
                        let blendedPrompt = BlendedCreatePrompt(from: prompt)

                        // Generate clips (they may not be ready yet)
                        let initialClips = try await blendedCreateClient.generateMore(
                            blendedPrompt,
                            createType: .textOnly,
                            lyricsModel: blendedPrompt.lyricsModel
                        )
                        // Start polling for completion
                        let clipIds = initialClips.map { $0.id.remoteId }

                        // Poll every 3 seconds until clips are ready
                        let timeoutDuration: TimeInterval = 120 // 2 minute timeout
                        let startTime = Date()

                        for await _ in clock.timer(interval: .seconds(3)) {
                            // Check for 2 minute timeout
                            guard Date().timeIntervalSince(startTime) < timeoutDuration else {
                                subject.send(.errorFetchingClips(APIError.errorMessage(L10n.FeatureCreateClip.error)))
                                return
                            }

                            let updatedClips = try await apiClientV2.getFeedByIds(clipIds)

                            if let errorClip = updatedClips.first(where: { $0.status == .error }) {
                                throw APIError.errorMessage(errorClip.errorMessage ?? "Generation failed")
                            }

                            // Update clips in queue with latest status
                            for updatedClip in updatedClips {
                                await state.updateClipInQueue(updatedClip)
                            }

                            // Fetch images for the clips
                            await RemoteImagePrefetcher.shared.loadAndWaitForImages(urls: updatedClips.compactMap { URL(string: $0.largeImageUrl) })

                            // Prefetch the assets for the first 2 clips
                            prefetchClipAssets(for: Array(updatedClips.prefix(2)))

                            // Check if all clips are ready (streaming or complete)
                            let readyClips = updatedClips.filter { $0.status == .streaming || $0.status == .complete }

                            if readyClips.count == updatedClips.count {
                                // Add the final clips to the queue
                                await state.appendToQueue(readyClips)
                                // Update the queue in OmniPlayerPlaybackReducer
                                let finalQueue = await state.getQueue()
                                subject.send(.queueUpdated(finalQueue))
                                // Notify OmniPlayerReducer that new clips are ready
                                subject.send(.moreClipsAdded(readyClips))

                                // Poll for status complete (only once, will outlive the streaming polling)
                                if !hasStartedCompletionPolling {
                                    clipPollingClient.pollClipsForStatusComplete(readyClips)
                                    hasStartedCompletionPolling = true
                                }
                                break
                            }
                        }

                    } catch {
                        subject.send(.errorFetchingClips(error))
                    }
                }
            },
            // Infinite carousel support
            rotateQueueForward: { @Sendable in
                Task {
                    await state.rotateQueueForward()
                    let updatedQueue = await state.getQueue()
                    subject.send(.queueUpdated(updatedQueue))
                }
            },
            rotateQueueBackward: { @Sendable in
                Task {
                    await state.rotateQueueBackward()
                    let updatedQueue = await state.getQueue()
                    subject.send(.queueUpdated(updatedQueue))
                }
            },
            getTimeSyncedComments: { id, searchTime, searchRange, margin, numRequested, endTime in
                Task {
                    await _internal_fetchTimeSyncedComments(
                        clipID: id,
                        searchTime: searchTime,
                        searchRange: searchRange,
                        margin: margin,
                        numRequested: numRequested,
                        endTime: endTime
                    )
                }
            },
            getLyricsForClip: { clip in
                getLyricsIfNeededForClip(clip)
            },
            setContext: { context in
                analyticsClient.setContext(context)
            },
            getUnderlyingPlayer: {
                // Return the underlying player client
                guard let player = playerClient.getUnderlyingPlayer() as? AVPlayer else {
                    log.telemetry.assertionFailure("Failed to get underlying player from player client")
                    return nil
                }
                return player
            },
            getCurrentSessionId: {
                await analyticsClient.getCurrentSessionId()
            },
            fetchAutoplayClips: { clip in
                Task {
                    do {
                        let config = await state.getPlaybackConfiguration()

                        guard config.autoplayMode != .off else { return }

                        let clips = try await fetchClipsForAutoplay(clip, config: config)

                        let currentClip = await state.getCurrentClip()
                        guard currentClip?.id == clip.id else {
                            log.debug("Skipping adding autoplay clips - clip no longer current")
                            return
                        }

                        let existingQueue = await state.getQueue()
                        var seenIds = Set<ClipID>(existingQueue.map(\.id))
                        seenIds.insert(clip.id)

                        let uniqueClips = clips.filter { candidate in
                            guard !seenIds.contains(candidate.id) else { return false }
                            seenIds.insert(candidate.id)
                            return true
                        }

                        guard !uniqueClips.isEmpty else { return }

                        await appendAndNotifyQueueUpdate(uniqueClips)
                    } catch {
                        log.telemetry.error(error)
                    }
                }
            }
        )
    }()
}

public extension DependencyValues {
    var omniplayerClient: OmniPlayerClient {
        get { self[OmniPlayerClient.self] }
        set { self[OmniPlayerClient.self] = newValue }
    }
}
