import AnalyticsClient
import APIClient
import AVFoundation
import Combine
import ComponentLibrary
import ComposableArchitecture
import EventBusClient
import FeatureHooksModels
import FeatureToasts
import Foundation
import Localization
import Utilities

let log = Logger(category: "HooksPlayerClient")

// Actor that makes sure we only setup the AVAudioSession and analytics client once
actor SetupState {
    private var didSetup = false

    func markAsSetup() -> Bool {
        guard !didSetup else { return false }
        didSetup = true
        return true
    }
}

public enum TapType: Equatable, Sendable {
    case single
    case double
}

public enum HooksPlayerEvent: Equatable, Sendable {
    case playbackStateChanged(String, PlaybackState) // Hook ID, PlaybackState
    case playbackFailed(String, Error) // Hook ID, Error

    // Feed events
    case feedDidLoadInitial([Hook])
    case feedDidLoadNext([Hook])
    case feedDidReloadAfterIndex([Hook], Int) // hooks, afterIndex
    case feedDidFailToLoad(Error)

    // Contextual mode events
    case refreshFeed(HooksFeedSource, UUID) // source, feedId
    case playHookInFeed(Hook)

    // Follow status events
    case followStatusUpdated(String, Bool) // Handle, isFollowing
    // Like status events
    case likeStatusUpdated(String, Bool) // Hook ID, isLiked
    case likeCountUpdated(String, Int) // Hook ID, likeCount
    case clipLikeStatusUpdated(String, Bool)

    // Hook updated events
    case hookUpdated(Hook)

    // Hook navigation events
    case scrollToIndex(Int)

    public static func == (lhs: HooksPlayerEvent, rhs: HooksPlayerEvent) -> Bool {
        switch (lhs, rhs) {
        case (.playbackStateChanged(let lhsId, let lhsState), .playbackStateChanged(let rhsId, let rhsState)):
            return lhsId == rhsId && lhsState == rhsState
        case (.playbackFailed(let lhsId, _), .playbackFailed(let rhsId, _)):
            return lhsId == rhsId
        case (.feedDidLoadInitial(let lhsHooks), .feedDidLoadInitial(let rhsHooks)):
            return lhsHooks == rhsHooks
        case (.feedDidLoadNext(let lhsHooks), .feedDidLoadNext(let rhsHooks)):
            return lhsHooks == rhsHooks
        case (.feedDidReloadAfterIndex(let lhsHooks, let lhsIndex), .feedDidReloadAfterIndex(let rhsHooks, let rhsIndex)):
            return lhsHooks == rhsHooks && lhsIndex == rhsIndex
        case (.feedDidFailToLoad, .feedDidFailToLoad):
            return true
        case (.refreshFeed(let lhsSource, let lhsFeedId), .refreshFeed(let rhsSource, let rhsFeedId)):
            return lhsSource == rhsSource && lhsFeedId == rhsFeedId
        case (.followStatusUpdated(let lhsHandle, let lhsStatus), .followStatusUpdated(let rhsHandle, let rhsStatus)):
            return lhsHandle == rhsHandle && lhsStatus == rhsStatus
        case (.likeStatusUpdated(let lhsHookId, let lhsStatus), .likeStatusUpdated(let rhsHookId, let rhsStatus)):
            return lhsHookId == rhsHookId && lhsStatus == rhsStatus
        case (.likeCountUpdated(let lhsHookId, let lhsCount), .likeCountUpdated(let rhsHookId, let rhsCount)):
            return lhsHookId == rhsHookId && lhsCount == rhsCount
        case (.clipLikeStatusUpdated(let lhsHookId, let lhsStatus), .clipLikeStatusUpdated(let rhsHookId, let rhsStatus)):
            return lhsHookId == rhsHookId && lhsStatus == rhsStatus
        case (.hookUpdated(let lhsHook), .hookUpdated(let rhsHook)):
            return lhsHook.id == rhsHook.id
        case (.scrollToIndex(let lhsIndex), .scrollToIndex(let rhsIndex)):
            return lhsIndex == rhsIndex
        case (.playHookInFeed(let lhsHook), .playHookInFeed(let rhsHook)):
            return lhsHook.id == rhsHook.id
        default:
            return false
        }
    }
}

// MARK: - Playback Configuration

public struct PlaybackConfig: Equatable, Sendable {
    /// Whether to automatically play hooks when they become current
    public let autoPlayEnabled: Bool
    /// Whether the user is currently in onboarding (prevents autoplay)
    public let isShowingOnboarding: Bool
    /// Whether to start muted
    public let startMuted: Bool
    /// Whether this is a deeplink launch (affects initial behavior)
    public let isDeeplinkLaunch: Bool

    public init(
        autoPlayEnabled: Bool = true,
        isShowingOnboarding: Bool = false,
        startMuted: Bool = true,
        isDeeplinkLaunch: Bool = false
    ) {
        self.autoPlayEnabled = autoPlayEnabled
        self.isShowingOnboarding = isShowingOnboarding
        self.startMuted = startMuted
        self.isDeeplinkLaunch = isDeeplinkLaunch
    }

    public static let `default` = PlaybackConfig()
}

// MARK: - Feed Configuration

// Determines how many hooks to fetch and how to play them,
// and this remains constant as we call fetchNextBatch
public struct HooksFeedConfig: Equatable, Sendable {
    // Initial hooks in the feed, like when deeplinking to a hook
    // on app launch
    public let initialHooks: [Hook]?
    // How many hooks to fetch at a time
    public let pageSize: Int
    // How many hooks to preload ahead of the current index
    public let preloadDistance: Int
    // Whether to automatically play the next hook when the current one ends
    public let autoPlayEnabled: Bool
    // Whether to loop the video
    public let loopEnabled: Bool
    // Playback configuration for initial state
    public let playbackConfig: PlaybackConfig

    public init(
        initialHooks: [Hook]? = nil,
        pageSize: Int = 20,
        preloadDistance: Int = 3,
        autoPlayEnabled: Bool = true,
        loopEnabled: Bool = true,
        playbackConfig: PlaybackConfig = .default
    ) {
        self.initialHooks = initialHooks
        self.pageSize = pageSize
        self.preloadDistance = preloadDistance
        self.autoPlayEnabled = autoPlayEnabled
        self.loopEnabled = loopEnabled
        self.playbackConfig = playbackConfig
    }
}

// MARK: - Client Definition

@DependencyClient
public struct HooksPlayerClient: Sendable {
    // Setup and lifecycle
    public var setup: @Sendable (String?) -> Void = { _ in }
    public var teardown: @Sendable () -> Void = {}
    public var events: @Sendable () -> AsyncStream<HooksPlayerEvent> = { .init { _ in } }

    // Feed data management (consolidated from HooksFeedClient)
    public var loadInitial: @Sendable (HooksFeedConfig, UUID) async throws -> [Hook] = { _, _ in [] }
    public var loadNext: @Sendable () async throws -> [Hook] = { [] }
    public var reloadFeed: @Sendable () async throws -> [Hook] = { [] }

    public var updateHooks: @Sendable ([Hook], Int, Bool, UUID?) -> Void = { _, _, _, _ in } // hooks, startIndex, reload, feedId
    public var appendHooks: @Sendable ([Hook]) -> Void = { _ in }
    public var updateCurrentIndex: @Sendable (Int) -> Void = { _ in }
    public var getPlayerForIndex: @Sendable (Int) async -> AVPlayer? = { _ in nil }
    public var isHookPlaying: @Sendable (Int) async -> Bool = { _ in false }
    public var getPlaybackState: @Sendable (Int) async -> PlaybackState = { _ in .ready }
    public var getPlayerSlotForIndex: @Sendable (Int) async -> Int? = { _ in nil }
    public var getCurrentIndex: @Sendable () async -> Int = { 0 }
    public var playCurrentHook: @Sendable (HookPlayCause) -> Void = { _ in }
    public var pauseCurrentHook: @Sendable (HookPauseCause) -> Void = { _ in }
    public var restartCurrentHook: @Sendable () -> Void = {}
    public var togglePlayPause: @Sendable () -> Void = {}

    public var toggleFollow: @Sendable (String, HooksRecommendationMetadata?) -> Void = { _, _ in }
    public var toggleLike: @Sendable (Hook, TapType) -> Void = { _, _ in }
    public var toggleClipLike: @Sendable (Hook) -> Void = { _ in }

    // Play a hook in a contextual feed (when the user taps to play their list of hooks)
    public var playHookInContextualFeed: @Sendable (Hook, [Hook], Int, HooksFeedSource, HookNavigationOptions?, UUID) -> Void = { _, _, _, _, _, _ in }
    // Exit contextual feed and return to main feed (respects previous pause state)
    public var exitContextualFeed: @Sendable () -> Void = {}

    // Clip like status
    public var getClipLikeStatus: @Sendable (String) async -> Bool? = { _ in nil }
    public var setClipLikeStatus: @Sendable (Bool, String) async -> Void = { _, _ in }
    public var setClipPlayCount: @Sendable (Int, String) async -> Void = { _, _ in }
    public var setFollowStatus: @Sendable (Bool, String) async -> Void = { _, _ in }
    public var setCommentCount: @Sendable (Int, String) async -> Void = { _, _ in }

    public var syncHooksMetadata: @Sendable ([String], [String]) async -> (followStatuses: [String: Bool], likeStatuses: [String: Bool], likeCounts: [String: Int], commentCounts: [String: Int], clipLikeStatuses: [String: Bool], clipPlayCounts: [String: Int], dislikeStatuses: [String: Bool], lyrics: [String: LyricsDataV2]) = { _, _ in ([:], [:], [:], [:], [:], [:], [:], [:]) }

    // Update hidden creator handles to prevent auto-play
    public var updateHiddenCreatorHandles: @Sendable ([String: Bool]) async -> Void = { _ in }
    // Update reported hooks to prevent auto-play
    public var updateReportedHooks: @Sendable ([String: Bool]) async -> Void = { _ in }

    // Hook dislike methods
    public var getHookDislikeStatus: @Sendable (String) async -> Bool? = { _ in nil }
    public var setHookDislikeStatus: @Sendable (Bool, String) async -> Void = { _, _ in }

    // Mute state
    public var isMuted: @Sendable () async -> Bool = { false }
    public var setMuted: @Sendable (Bool) -> Void = { _ in }
    public var setOnboardingState: @Sendable (Bool) -> Void = { _ in }
    public var setInitialConfig: @Sendable (PlaybackConfig) -> Void = { _ in }

    // Lyrics methods
    public var getLyrics: @Sendable (String) async -> LyricsDataV2? = { _ in nil }
    public var getLyricsForHooks: @Sendable ([String]) async -> [String: LyricsDataV2] = { _ in [:] }

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

    // Deeplink playback
    public var playHookInFeed: @Sendable (Hook, HooksFeedSource) -> Void = { _, _ in }

    // Hook removal
    public var removeHook: @Sendable (String, String?) -> Void = { _, _ in } // hookId, handle
}

extension HooksPlayerClient: TestDependencyKey {
    public static let previewValue = Self()
    public static let testValue = Self()
}

// MARK: - Live Value

extension HooksPlayerClient: DependencyKey {
    public static let liveValue: Self = {
        let eventSubject = PassthroughSubject<HooksPlayerEvent, Never>()
        let setupState = SetupState()

        let feedState = FeedState()
        let slidingWindowState = SlidingWindowState(eventSubject: eventSubject)

        @Dependency(\.hooksPlayerAnalyticsClient) var analyticsClient
        @Dependency(\.toastClient.show) var showToast

        // MARK: - Helpers

        /// Hydrates the hook metadata caches with the data from the hooks
        let followCache = SimpleFollowCache()
        let reactionCache = SimpleReactionCache()
        let clipLikeCache = ClipLikeCache()
        let clipPlayCountCache = ClipPlayCountCache()
        let commentCache = SimpleCommentCache()
        let lyricsCache = SimpleLyricsCache()

        @Sendable
        func hydrateAllCachesFromHooks(_ hooks: [Hook]) async {
            let hooksWithHandles = hooks.filter { $0.user?.handle != nil }

            async let followTask: Void = hooksWithHandles.isEmpty ? () : followCache.hydrateFromHooks(hooksWithHandles)
            async let reactionTask: Void = reactionCache.hydrateFromHooks(hooks)
            async let clipLikeTask: Void = clipLikeCache.hydrateFromHooks(hooks)
            async let clipPlayCountTask: Void = clipPlayCountCache.hydrateFromHooks(hooks)
            async let commentTask: Void = commentCache.hydrateFromHooks(hooks)
            async let lyricsTask: Void = lyricsCache.fetchForHooksIfNeeded(hooks)

            _ = await (followTask, reactionTask, clipLikeTask, clipPlayCountTask, commentTask, lyricsTask)
        }

        @Sendable
        func removeHookFromAllCaches(hookId: String, handle: String?) async {
            async let reactionTask: Void = reactionCache.removeHook(hookId)
            async let clipLikeTask: Void = clipLikeCache.removeHook(hookId)
            async let clipPlayCountTask: Void = clipPlayCountCache.removeHook(hookId)
            async let commentTask: Void = commentCache.removeHook(hookId)
            async let lyricsTask: Void = lyricsCache.removeHook(hookId)

            if let handle = handle {
                async let followTask: Void = followCache.removeHandle(handle)
                _ = await (reactionTask, clipLikeTask, clipPlayCountTask, commentTask, lyricsTask, followTask)
            } else {
                _ = await (reactionTask, clipLikeTask, clipPlayCountTask, commentTask, lyricsTask)
            }
        }

        /// Fetches and caches all hook related images
        @Sendable
        func prefetchImagesForHooks(_ hooks: [Hook]) {
            let clipArtUrls: [URL] = hooks.compactMap { hook in
                guard let clip = hook.clip else { return nil }
                return URL(string: clip.largeImageUrl)
            }
            let authorImageUrls: [URL] = hooks.compactMap { hook in
                guard let avatarImageUrl = hook.user?.avatarImageUrl else { return nil }
                return URL(string: avatarImageUrl)
            }
            let thumbnailUrls: [URL] = hooks.compactMap { hook in
                hook.thumbnailImageUrl.flatMap { URL(string: $0) }
            }
            let urls = clipArtUrls + authorImageUrls + thumbnailUrls
            guard !urls.isEmpty else { return }
            RemoteImagePrefetcher.shared.loadImages(urls: urls)
        }

        // MARK: - Client Implementation

        var client = Self(
            setup: { userId in
                Task {
                    // Only setup the AVAudioSession and analytics client once
                    guard await setupState.markAsSetup() else { return }

                    // Setup the AVAudioSession (this makes music from other apps
                    // stop playing when you open to the Hooks feed directly)
                    let audioSession = AVAudioSession.sharedInstance()
                    try? audioSession.setCategory(.playback, mode: .default)
                    try? audioSession.setActive(true)

                    // Setup the analytics client with the correct userId
                    await slidingWindowState.setAnalyticsClient(analyticsClient)
                    await analyticsClient.setupSession(userId)
                }
            },
            teardown: {
                Task {
                    await slidingWindowState.cleanup()
                }
            },
            events: {
                AsyncStream { continuation in
                    let cancellable = eventSubject.sink { event in
                        continuation.yield(event)
                    }
                    continuation.onTermination = { _ in
                        cancellable.cancel()
                    }
                }
            },
            loadInitial: { config, feedId in
                do {
                    var hooks = try await feedState.loadInitialHooks(config: config)

                    // Set analytics context for main hooks tab feed
                    await analyticsClient.setContext("hooks_feed", nil, nil, nil, nil)

                    // Set the main feed source for contextual exit
                    await slidingWindowState.setFeedSource(.hooksFeed, feedId: feedId)

                    // Configure initial playback state from config
                    await slidingWindowState.setInitialConfig(config.playbackConfig)

                    // Add any initial hooks from an app launch deeplink
                    if let initialHooks = config.initialHooks {
                        hooks = initialHooks + hooks
                    }

                    // Hydrate individual caches with hooks data
                    await hydrateAllCachesFromHooks(hooks)

                    // Prefetch all images for the hooks
                    Task.detached(priority: .userInitiated) {
                        prefetchImagesForHooks(hooks)
                    }

                    return hooks
                } catch {
                    eventSubject.send(.feedDidFailToLoad(error))
                    showToast(.warning(L10n.FeatureHooks.somethingWentWrong, .string(L10n.FeatureHooks.pleaseTryAgain), position: .bottom))
                    throw error
                }
            },
            loadNext: {
                do {
                    let existingHooks = await feedState.getAllHooks()
                    let existingHookIds = Set(existingHooks.map(\.id))

                    let newHooks = try await feedState.loadNextHooks()

                    // Filter out duplicate hooks from the new API response
                    let filteredHooks = newHooks.filter { hook in
                        !existingHookIds.contains(hook.id)
                    }

                    // Hydrate individual caches with new hooks data
                    await hydrateAllCachesFromHooks(filteredHooks)

                    // Prefetch all images for the hooks
                    Task.detached(priority: .userInitiated) {
                        prefetchImagesForHooks(filteredHooks)
                    }

                    eventSubject.send(.feedDidLoadNext(filteredHooks))
                    return filteredHooks
                } catch {
                    eventSubject.send(.feedDidFailToLoad(error))
                    showToast(.warning(L10n.FeatureHooks.somethingWentWrong, .string(L10n.FeatureHooks.pleaseTryAgain), position: .bottom))
                    throw error
                }
            },
            reloadFeed: {
                do {
                    // Pause and track currently playing hook
                    await slidingWindowState.pauseCurrentHook(cause: .reloadFeed)

                    // Reload hooks
                    let hooks = try await feedState.reloadHooks()

                    // Hydrate individual caches with reloaded hooks data
                    await hydrateAllCachesFromHooks(hooks)

                    // Prefetch all images for the hooks
                    Task.detached(priority: .userInitiated) {
                        prefetchImagesForHooks(hooks)
                    }

                    return hooks
                } catch {
                    showToast(.warning(L10n.FeatureHooks.somethingWentWrong, .string(L10n.FeatureHooks.pleaseTryAgain), position: .bottom))
                    throw error
                }
            },
            updateHooks: { hooks, startIndex, reload, feedId in
                Task {
                    await slidingWindowState.updateHooks(hooks, startIndex: startIndex, reload: reload, feedId: feedId)
                }
            },
            appendHooks: { hooks in
                Task {
                    await slidingWindowState.appendHooks(hooks)
                }
            },
            updateCurrentIndex: { index in
                Task {
                    await slidingWindowState.updateCurrentIndex(index)
                }
            },
            getPlayerForIndex: { index in
                await slidingWindowState.getPlayer(for: index)
            },
            isHookPlaying: { index in
                await slidingWindowState.isHookPlaying(at: index)
            },
            getPlaybackState: { index in
                await slidingWindowState.getPlaybackState(at: index)
            },
            getPlayerSlotForIndex: { index in
                await slidingWindowState.getPlayerSlotForIndex(index)
            },
            getCurrentIndex: {
                await slidingWindowState.getCurrentIndex()
            },

            playCurrentHook: { cause in
                Task {
                    await slidingWindowState.playCurrentHook(cause: cause)
                }
            },
            pauseCurrentHook: { cause in
                Task {
                    await slidingWindowState.pauseCurrentHook(cause: cause)
                }
            },
            restartCurrentHook: {
                Task {
                    await slidingWindowState.restartCurrentHook()
                }
            },
            togglePlayPause: {
                Task {
                    await slidingWindowState.togglePlayPause()
                }
            },

            toggleFollow: { handle, recommendationMetadata in
                Task {
                    await followCache.toggleFollow(handle: handle, eventSubject: eventSubject, recommendationMetadata: recommendationMetadata)
                }
            },
            toggleLike: { hook, tapType in
                Task {
                    let currentStatus = await reactionCache.getLikeStatus(for: hook.id) ?? false
                    let newStatus = !currentStatus

                    // Update cache immediately for optimistic UI
                    await reactionCache.updateLikeState(newStatus, for: hook.id)

                    // Send events
                    eventSubject.send(.likeStatusUpdated(hook.id, newStatus))
                    if let count = await reactionCache.getLikeCount(for: hook.id) {
                        eventSubject.send(.likeCountUpdated(hook.id, count))
                    }

                    // Make API call
                    do {
                        @Dependency(\.apiClientV2) var api
                        let action: HookReaction = newStatus ? .like : .unlike
                        let interactionType: HookInteractionType = tapType == .double ? .likeDoubleTap : .likeSingleTap
                        let recommendationMetadata = HooksRecommendationMetadata(
                            recommendationItemId: hook.recommendationItemId
                        )
                        try await api.setHookReaction(hook.id, action, recommendationMetadata)
                    } catch {
                        // Revert on error
                        await reactionCache.updateLikeState(currentStatus, for: hook.id)
                        eventSubject.send(.likeStatusUpdated(hook.id, currentStatus))
                        if let count = await reactionCache.getLikeCount(for: hook.id) {
                            eventSubject.send(.likeCountUpdated(hook.id, count))
                        }
                    }
                }
            },
            toggleClipLike: { hook in
                guard var clip = hook.clip else { return }
                Task {
                    @Dependency(\.eventBus.sendClipEvent) var sendClipEvent

                    clip.isLiked.toggle()
                    sendClipEvent(.toggledLike(clip))

                    await clipLikeCache.toggleClipLike(
                        hook: hook,
                        eventSubject: eventSubject
                    )
                }
            },
            playHookInContextualFeed: { hook, hooks, index, source, navigationOptions, feedId in
                Task {
                    await slidingWindowState.attachContextualFeed(hooks: hooks, startIndex: index, source: source, feedId: feedId)

                    let context = source.analyticsContext
                    let navigationIntent = navigationOptions?.analyticsIntent
                    let targetCommentId = navigationOptions?.analyticsTargetCommentId

                    // For deeplinks, we can pass URL in contextId
                    let sourceUrl = (context.contextType == "deeplink") ? context.contextId : nil
                    await analyticsClient.setContext(context.contextType, context.contextId, sourceUrl, navigationIntent, targetCommentId)

                    // Hydrate individual caches with hooks data
                    await hydrateAllCachesFromHooks(hooks)

                    // Prefetch all images for the hooks
                    Task.detached(priority: .userInitiated) {
                        prefetchImagesForHooks(hooks)
                    }
                }
            },
            exitContextualFeed: {
                Task.detached {
                    await slidingWindowState.exitContextualMode()
                    await analyticsClient.setContext("hooks_feed", nil, nil, nil, nil)
                }
            },
            getClipLikeStatus: { hookId in
                await clipLikeCache.getClipLikeStatus(for: hookId)
            },
            setClipLikeStatus: { isLiked, hookId in
                await clipLikeCache.setClipLikeStatus(isLiked, for: hookId)
            },
            setClipPlayCount: { playCount, hookId in
                await clipPlayCountCache.setClipPlayCount(playCount, for: hookId)
            },
            setFollowStatus: { isFollowing, handle in
                await followCache.setFollowStatus(isFollowing, for: handle)
            },
            setCommentCount: { count, hookId in
                await commentCache.setCommentCount(count, for: hookId)
            },
            syncHooksMetadata: { hookIds, handles in
                var followStatuses: [String: Bool] = [:]
                var likeStatuses: [String: Bool] = [:]
                var likeCounts: [String: Int] = [:]
                var hookClipLikeStatuses: [String: Bool] = [:]
                var hookClipPlayCounts: [String: Int] = [:]
                var commentCounts: [String: Int] = [:]
                var dislikeStatuses: [String: Bool] = [:]
                var lyrics: [String: LyricsDataV2] = [:]

                for handle in handles {
                    followStatuses[handle] = await followCache.getFollowStatus(for: handle)
                }
                for hookId in hookIds {
                    likeStatuses[hookId] = await reactionCache.getLikeStatus(for: hookId)
                    likeCounts[hookId] = await reactionCache.getLikeCount(for: hookId)
                    hookClipLikeStatuses[hookId] = await clipLikeCache.getClipLikeStatus(for: hookId)
                    hookClipPlayCounts[hookId] = await clipPlayCountCache.getClipPlayCount(for: hookId)
                    dislikeStatuses[hookId] = await reactionCache.getDislikeStatus(for: hookId)
                }
                commentCounts = await commentCache.getCommentCounts(hookIds)
                lyrics = await lyricsCache.getLyricsMap(for: hookIds)

                return (
                    followStatuses: followStatuses,
                    likeStatuses: likeStatuses,
                    likeCounts: likeCounts,
                    commentCounts: commentCounts,
                    clipLikeStatuses: hookClipLikeStatuses,
                    clipPlayCounts: hookClipPlayCounts,
                    dislikeStatuses: dislikeStatuses,
                    lyrics: lyrics
                )
            },
            updateHiddenCreatorHandles: { handles in
                await slidingWindowState.updateHiddenCreatorHandles(handles)
                guard let nextIndex = await slidingWindowState.nextPlayableIndex() else { return }
                Task {
                    await withTaskGroup(of: Void.self) { group in
                        var newHooks: [Hook] = []

                        group.addTask {
                            try? await Task.sleep(for: .seconds(1))
                            eventSubject.send(.scrollToIndex(nextIndex))
                        }

                        group.addTask {
                            do {
                                let hooks = try await feedState.loadMoreHooks(
                                    currentIndex: nextIndex,
                                    hiddenCreatorHandles: handles
                                )
                                newHooks = hooks
                            } catch {
                                showToast(.warning(L10n.FeatureHooks.somethingWentWrong, .string(L10n.FeatureHooks.pleaseTryAgain), position: .bottom))
                                // Don't throw an error and just let the last valid Hook play
                            }
                        }

                        await group.waitForAll()

                        // Apply data changes after both scroll and fetch complete
                        guard !newHooks.isEmpty else { return }
                        await slidingWindowState.removeAllHooksAfterIndex(nextIndex)
                        eventSubject.send(.feedDidReloadAfterIndex(newHooks, nextIndex))
                    }
                }
            },
            updateReportedHooks: { hookIds in
                await slidingWindowState.updateReportedHooks(hookIds)
            },
            getHookDislikeStatus: { hookId in
                await reactionCache.getDislikeStatus(for: hookId)
            },
            setHookDislikeStatus: { isDisliked, hookId in
                await reactionCache.setDislikeStatus(isDisliked, for: hookId)
            },
            isMuted: {
                await slidingWindowState.getMuteState()
            },
            setMuted: { isMuted in
                Task {
                    await slidingWindowState.setMuteState(isMuted)
                }
            },
            setOnboardingState: { isShowingOnboarding in
                Task {
                    await slidingWindowState.setOnboardingState(isShowingOnboarding)
                }
            },
            setInitialConfig: { playbackConfig in
                Task {
                    await slidingWindowState.setInitialConfig(playbackConfig)
                }
            },
            getLyrics: { hookId in
                await lyricsCache.getLyrics(for: hookId)
            },
            getLyricsForHooks: { hookIds in
               await lyricsCache.getLyricsMap(for: hookIds)
            },
            getCurrentSessionId: {
                await analyticsClient.getCurrentSessionId()
            },
            playHookInFeed: { hook, source in
                Task {
                    // Pause the current hook
                    await slidingWindowState.pauseCurrentHook(cause: .openDeeplink)

                    // Hydrate all caches with the hook data BEFORE sending event
                    await hydrateAllCachesFromHooks([hook])

                    // Prefetch all images for the hook
                    Task.detached(priority: .userInitiated) {
                        prefetchImagesForHooks([hook])
                    }

                    // Send the update to the reducer (which will now find data in cache during syncFromCache)
                    eventSubject.send(.playHookInFeed(hook))

                    // Prepare and play
                    await slidingWindowState.insertHookAtCurrentIndex(hook, source: source)
                }
            },
            removeHook: { hookId, handle in
                Task {
                    await slidingWindowState.removeHookPlayerAssignment(hookId: hookId)
                    await removeHookFromAllCaches(hookId: hookId, handle: handle)
                }
            }
        )

        return client
    }()
}

public extension DependencyValues {
    var hooksPlayerClient: HooksPlayerClient {
        get { self[HooksPlayerClient.self] }
        set { self[HooksPlayerClient.self] = newValue }
    }
}
