import AnalyticsClient
import APIClient
import AVFoundation
import Combine
import ComposableArchitecture
import Foundation
import TabBarUtilities
import UIKit
import Utilities

// swiftlint:disable file_length
// swiftlint:disable:next cyclomatic_complexity
final actor SlidingWindowState: Sendable {
    private let maxPlayers = 5
    private let preloadBackward = 2
    private let preloadForward = 2

    private var playerPool: [AVPlayer] = []
    private var hookIndexToPlayer: [Int: Int] = [:] // hookIndex -> playerSlot
    private var playerToHookIndex: [Int: Int] = [:] // playerSlot -> hookIndex

    private var hooks: [Hook] = []

    // Currently playing Hook, used for controlling what
    // what to play and when, and also for analytics
    private var currentIndex: Int = 0

    // Tracks the previously played Hook index to determine scroll direction
    private var previousIndex: Int?

    private let eventSubject: PassthroughSubject<HooksPlayerEvent, Never>
    private var analyticsClient: HooksPlayerAnalyticsClient?
    private var hasTrackedInitialPlay: Bool = false

    // Track hooks that should not auto-play (reported or creator hidden)
    private var hiddenCreatorHandles: [String: Bool] = [:]
    private var reportedHookIds: [String: Bool] = [:]

    // Starts muted until user explicitly unmutes
    private var isMuted: Bool = true

    // Track onboarding state to prevent autoplay during onboarding
    private var isShowingOnboarding: Bool = false

    // Track the current playback state for the current hook
    private(set) var currentPlaybackState: PlaybackState = .loading

    // Track when we're in a loop operation to prevent UI flickering
    private var isLooping: Bool = false

    // Track when we're replacing/assigning player items to prevent spurious state changes
    private var isReplacingCurrentItem: Bool = false

    private var statusObservers: [Int: NSKeyValueObservation] = [:] // playerSlot -> observer
    private var timeControlObservers: [Int: Task<Void, Never>] = [:] // playerSlot -> Task

    // Audio session interruption handling
    private var audioInterruptionObserver: NSObjectProtocol?
    private var audioRouteChangeObserver: NSObjectProtocol?
    private var globalEndObserver: NSObjectProtocol?

    // Track when user manually plays to distinguish from system-initiated changes
    private var wasPlayingBeforeInterruption: Bool? = nil
    private var wasPausedByInterruption: Bool = false
    private var lastPauseCause: HookPauseCause?

    // Track window adjustment tasks for cancellation during fast swiping
    private var windowAdjustmentTask: Task<Void, Never>?

    // Simple asset pre-loading cache (hookId -> AVURLAsset)
    private var preloadedAssets: [String: AVURLAsset] = [:]
    private let maxPreloadedAssets = 20 // Keep last 20 videos in memory

    // Contextual mode state (like queueOverride in OmniPlayerState)
    private var isContextualMode: Bool = false
    private var feedStates: [SavedFeedState] = []
    private var feedSource: HooksFeedSource = .hooksFeed
    private var feedId: UUID?

    private struct SavedFeedState {
        var hooks: [Hook] // mutable to handle deleting a hook
        var currentIndex: Int
        let source: HooksFeedSource
        let wasPlaying: Bool
        let wasMuted: Bool
        let feedId: UUID
    }

    @Dependency(\.hooksPlayCountManager) var playCountManager

    var isOnHooksFeed: Bool {
        @Shared(.inMemory(.isOnHooksFeed)) var isOnHooksFeed: Bool = false
        return isOnHooksFeed
    }

    init(eventSubject: PassthroughSubject<HooksPlayerEvent, Never>) {
        self.eventSubject = eventSubject
        // Create fixed pool of `maxPlayers` players
        for _ in 0 ..< maxPlayers {
            let player = AVPlayer()
            player.automaticallyWaitsToMinimizeStalling = true
            player.isMuted = isMuted
            player.pause()
            playerPool.append(player)
        }

        // Setup audio session interruption and route change observers
        setupAudioSessionObservers()
        setupGlobalEndObserver()
    }

    func setAnalyticsClient(_ client: HooksPlayerAnalyticsClient) {
        self.analyticsClient = client
    }

    // MARK: - Audio Session Management

    private func setupAudioSessionObservers() {
        // Handle audio session interruptions (phone calls, Siri, etc.)
        audioInterruptionObserver = NotificationCenter.default.addObserver(
            forName: AVAudioSession.interruptionNotification,
            object: nil,
            queue: .main
        ) { [weak self] notification in
            Task { [weak self] in
                await self?.handleAudioInterruption(notification)
            }
        }

        // Handle audio route changes (Bluetooth disconnect, etc.)
        audioRouteChangeObserver = NotificationCenter.default.addObserver(
            forName: AVAudioSession.routeChangeNotification,
            object: nil,
            queue: .main
        ) { [weak self] notification in
            Task { [weak self] in
                await self?.handleAudioRouteChange(notification)
            }
        }
    }

    private func handleAudioInterruption(_ notification: Notification) {
        guard let userInfo = notification.userInfo,
              let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
              let type = AVAudioSession.InterruptionType(rawValue: typeValue)
        else {
            return
        }

        switch type {
        case .began:
            // Store current playing state before interruption
            let wasPlaying = (currentPlaybackState == .playing)
            wasPlayingBeforeInterruption = wasPlaying

            // Pause current hook due to audio interruption
            if wasPlaying {
                pauseCurrentHook(cause: .audioInterruption)
            }

        case .ended:
            // Don't automatically resume - require user interaction
            // This prevents unexpected resuming after screen recording or other interruptions
            break

        @unknown default:
            break
        }
    }

    private func handleAudioRouteChange(_ notification: Notification) {
        guard let userInfo = notification.userInfo,
              let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
              let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue)
        else {
            return
        }

        switch reason {
        case .oldDeviceUnavailable:
            // Bluetooth device disconnected or headphones unplugged
            if currentPlaybackState == .playing {
                pauseCurrentHook(cause: .audioInterruption)
            }

        case .categoryChange:
            // Audio category changed - could be screen recording or legitimate interruption
            // Don't pause for screen recording category changes
            break

        case .routeConfigurationChange:
            // Route configuration changed - could be screen recording
            // Don't pause for route configuration changes
            break

        default:
            break
        }
    }

    func setFeedSource(_ source: HooksFeedSource, feedId: UUID) {
        self.feedId = feedId
        self.feedSource = source
    }

    // MARK: - Contextual Feed Methods

    func attachContextualFeed(hooks: [Hook], startIndex: Int, source: HooksFeedSource, feedId: UUID) {
        // Check if we're already showing the exact same contextual feed,
        // since going back in the NavigationStack calls .task
        let wasPlaying: Bool
        if isContextualMode, self.hooks.map(\.id) == hooks.map(\.id), self.currentIndex == startIndex {
            return
        }

        let previousFeedId: UUID? = self.feedId
        let previousSource = self.feedSource
        let wasMuted = isMuted

        isContextualMode = true
        setMuteState(false) // Unmute since the user intends to play hooks
        setFeedSource(source, feedId: feedId)

        if let playerSlot = hookIndexToPlayer[currentIndex] {
            let player = playerPool[playerSlot]
            wasPlaying = player.rate > 0
        } else {
            wasPlaying = false
        }

        cleanupPlayerObservers()

        feedStates.append(
            SavedFeedState(
                hooks: self.hooks,
                currentIndex: self.currentIndex,
                source: previousSource,
                wasPlaying: wasPlaying,
                wasMuted: wasMuted,
                feedId: previousFeedId ?? UUID()
            )
        )

        previousIndex = nil

        updateHooksInternal(hooks, startIndex: startIndex)
    }

    func exitContextualMode() {
        guard let saved = feedStates.popLast(), let analyticsClient else { return }
        // Track pause event for currently playing contextual hook before exiting
        let currentHook = getCurrentHook()
        if let currentHook,
           let playerSlot = hookIndexToPlayer[currentIndex]
        {
            playerPool[playerSlot].pause()
            let currentTime = getCurrentTime(for: currentIndex)
            analyticsClient.trackPauseHook(currentHook, currentTime, .navigation(.feed))
        }

        cleanupPlayerObservers()

        setFeedSource(saved.source, feedId: saved.feedId)
        setMuteState(saved.wasMuted)
        updateHooksInternal(saved.hooks)
        currentIndex = saved.currentIndex

        // If we only have the main feed in our stack, we can flag off contextual mode
        if feedStates.isEmpty {
            isContextualMode = false
            previousIndex = nil
        }

        eventSubject.send(.refreshFeed(saved.source, saved.feedId))
    }

    // MARK: - Contextual Mode Helpers

    private func getActiveHooks() -> [Hook] {
        return hooks
    }

    private func getActiveCurrentIndex() -> Int {
        return currentIndex
    }

    private func getActiveHooksCount() -> Int {
        return getActiveHooks().count
    }

    private func updateSavedFeedState(at index: Int, hooks: [Hook], currentIndex: Int) {
        guard index >= 0 && index < feedStates.count else { return }
        var savedState = feedStates[index]
        savedState.hooks = hooks
        if currentIndex >= 0 && currentIndex < hooks.count {
            savedState.currentIndex = currentIndex
        }
        feedStates[index] = savedState
    }

    private func removeHookFromSavedFeedStates(hookId: String) {
        for index in feedStates.indices {
            var savedState = feedStates[index]
            let oldCount = savedState.hooks.count

            savedState.hooks.removeAll { $0.id == hookId }

            // Adjust currentIndex if hook was removed
            if savedState.hooks.count < oldCount {
                if savedState.hooks.isEmpty {
                    savedState.currentIndex = 0
                } else if savedState.currentIndex >= savedState.hooks.count {
                    savedState.currentIndex = max(0, savedState.hooks.count - 1)
                }
                // We don't need to adjust currentIndex if deleted hook was after it
                // The index remains valid, it just points to a different hook
            }
            feedStates[index] = savedState
        }
    }

    func updateHooks(
        _ newHooks: [Hook],
        startIndex: Int,
        reload: Bool = false,
        feedId: UUID? = nil
    ) {
        if isContextualMode {
            if let updateFeedId = feedId {
                // Check if this update is for a saved feed in the stack (e.g., main feed)
                if let savedFeedIndex = feedStates.firstIndex(where: { $0.feedId == updateFeedId }) {
                    updateSavedFeedState(at: savedFeedIndex, hooks: newHooks, currentIndex: startIndex)
                    // Don't update current hooks - we're still showing contextual feed
                    return
                } else if updateFeedId != self.feedId {
                    return
                }
            }
        }

        updateHooksInternal(newHooks, startIndex: startIndex, reload: reload)
    }

    func removeAllHooksAfterIndex(_ index: Int) {
        let nextIndex = index + 1
        hooks.removeSubrange(nextIndex ..< hooks.count)

        guard let currentPlayerSlotIndex = getPlayerSlotForIndex(currentIndex) else { return }

        for (playerIndex, _) in playerPool.enumerated() where playerIndex != currentPlayerSlotIndex {
            let player = playerPool[playerIndex]
            player.pause()
            player.replaceCurrentItem(with: nil)
            guard let hookIndex = playerToHookIndex[playerIndex] else { continue }
            hookIndexToPlayer.removeValue(forKey: hookIndex)
            playerToHookIndex.removeValue(forKey: playerIndex)
        }
    }

    func removeHookPlayerAssignment(hookId: String) {
        guard let hookIndex = hooks.firstIndex(where: { $0.id == hookId }),
                  hookIndex < hooks.count,
                  let playerSlot = hookIndexToPlayer[hookIndex]
        else {
            // Even if not in current hooks, remove from all saved feed states
            // This handles cases where hook was already removed from current array
            removeHookFromSavedFeedStates(hookId: hookId)
            return
        }

        // Clean up the player
        let player = playerPool[playerSlot]
        player.pause()
        player.replaceCurrentItem(with: nil)
        player.isMuted = true

        hookIndexToPlayer.removeValue(forKey: hookIndex)
        playerToHookIndex.removeValue(forKey: playerSlot)

        cleanupObserversForPlayerSlot(playerSlot)

        preloadedAssets.removeValue(forKey: hookId)

        removeHookFromSavedFeedStates(hookId: hookId)
    }

    func appendHooks(_ newHooks: [Hook]) {
        appendHooksInternal(newHooks)
    }

    private func updateHooksInternal(
        _ newHooks: [Hook],
        startIndex: Int? = nil,
        reload _: Bool = false
    ) {
        hooks = newHooks
        previousIndex = nil

        if let startIndex {
            currentIndex = startIndex
        }

        // Adjust currentIndex if it's now out of bounds
        if currentIndex >= hooks.count {
            currentIndex = max(0, hooks.count - 1)
        }

        clearAllPlayerAssignments()
        rebuildWindow()
    }

    private func appendHooksInternal(_ newHooks: [Hook]) {
        hooks.append(contentsOf: newHooks)
        windowAdjustmentTask?.cancel()
        windowAdjustmentTask = Task.detached(priority: .userInitiated) { [weak self] in
            guard let self else { return }
            await adjustWindow(aroundIndex: self.currentIndex)
        }
    }

    func updateCurrentIndex(_ newIndex: Int) {
        updateCurrentIndexInternal(newIndex)
    }

    private func updateCurrentIndexInternal(_ newIndex: Int, reload _: Bool = false) {
        let currentIdx = getActiveCurrentIndex()
        let activeHooks = getActiveHooks()

        guard newIndex != currentIdx || (currentIdx == 0 && newIndex == 0) else { return }

        // Make sure the new index is within bounds
        guard newIndex >= 0, newIndex < activeHooks.count else { return }

        let oldIndex = currentIdx
        currentIndex = newIndex
        previousIndex = oldIndex
        isLooping = false

        if let oldPlayerSlot = hookIndexToPlayer[oldIndex] {
            let oldPlayer = playerPool[oldPlayerSlot]
            oldPlayer.pause()
            oldPlayer.isMuted = true
            oldPlayer.seek(to: .zero)

            let currentTime = oldPlayer.currentTime().seconds
            let oldHook = activeHooks[oldIndex]

            if let analytics = analyticsClient {
                Task.detached(priority: .utility) { [analytics, oldHook, currentTime] in
                    if newIndex > oldIndex {
                        analytics.trackScrollDownPauseHook(oldHook, currentTime)
                    } else {
                        analytics.trackScrollUpPauseHook(oldHook, currentTime)
                    }
                }
            }
        }

        // Update contextual mode immediately
        if isContextualMode {
            isMuted = false
        }

        // Immediately assign player for new index to prevent black screens during fast swiping
        if hookIndexToPlayer[newIndex] == nil {
            assignPlayerForHook(newIndex, shouldPlay: shouldAutoPlayHook(at: newIndex))
        }

        Task.detached(priority: .userInitiated) { [weak self] in
            await self?.handleIndexChange(
                newIndex: newIndex,
                oldIndex: oldIndex,
                activeHooks: activeHooks
            )
        }

        windowAdjustmentTask?.cancel()
        windowAdjustmentTask = Task.detached(priority: .userInitiated) { [weak self] in
            await self?.adjustWindow(aroundIndex: newIndex)
        }
    }

    private func handleIndexChange(newIndex: Int, oldIndex: Int?, activeHooks: [Hook]) async {
        if let newPlayerSlot = hookIndexToPlayer[newIndex] {
            let newPlayer = playerPool[newPlayerSlot]

            if let hook = activeHooks.indices.contains(newIndex) ? activeHooks[newIndex] : nil {
                eventSubject.send(.playbackStateChanged(hook.id, .ready))
            }

            // if player is ready
            if let playerItem = newPlayer.currentItem {
                // Only start playback if we should auto-play
                if shouldAutoPlayHook(at: newIndex) {
                    // Only seek if the player isn't already at the beginning
                    let currentTime = newPlayer.currentTime()
                    if currentTime.seconds > 1.0 {
                        await newPlayer.seek(to: .zero)
                    }

                    if let hook = activeHooks.indices.contains(newIndex) ? activeHooks[newIndex] : nil,
                       let analyticsClient
                    {
                        if let oldIndex {
                            if oldIndex < newIndex {
                                await analyticsClient.trackScrollDownPlayNewHook(hook)
                            } else if oldIndex > newIndex {
                                await analyticsClient.trackScrollUpPlayNewHook(hook)
                            } else {
                                await analyticsClient.trackPlayNewHook(hook)
                            }
                        } else {
                            await analyticsClient.trackPlayNewHook(hook)
                        }
                    }

                    newPlayer.isMuted = isMuted
                    newPlayer.play()

                    if let hook = activeHooks.indices.contains(newIndex) ? activeHooks[newIndex] : nil {
                        updatePlaybackState(.playing, hook.id)
                    }
                } else {
                    newPlayer.pause()
                    if let hook = activeHooks.indices.contains(newIndex) ? activeHooks[newIndex] : nil {
                        updatePlaybackState(.stopped, hook.id)
                    }
                }
            } else {
                newPlayer.pause()
                if let hook = activeHooks.indices.contains(newIndex) ? activeHooks[newIndex] : nil {
                    let playbackDisplayState: PlaybackState = (newPlayer.currentItem != nil) ? .loading : .notReady
                    updatePlaybackState(playbackDisplayState, hook.id)
                }
            }
        } else {
            guard newIndex >= 0, newIndex < activeHooks.count else { return }
            let hook = activeHooks[newIndex]
            updatePlaybackState(.loading, hook.id)
            assignPlayerForHook(newIndex, shouldPlay: shouldAutoPlayHook(at: newIndex))
        }
    }

    private func assignPlayerForHook(_ hookIndex: Int, shouldPlay: Bool = false) {
        let activeHooks = getActiveHooks()
        guard hookIndex >= 0, hookIndex < activeHooks.count else { return }

        if hookIndexToPlayer[hookIndex] != nil {
            return
        }

        let availableSlot = findAvailablePlayerSlot()
        let hook = activeHooks[hookIndex]
        let player = playerPool[availableSlot]

        let hookUrl = hook.streamingUrl ?? hook.renderedVideoUrl ?? hook.renderedVideoPreviewUrl
        let targetUrl = hookUrl.flatMap { URL(string: $0) }
        let currentAssetURL = (player.currentItem?.asset as? AVURLAsset)?.url
        let needsCleanup = currentAssetURL != targetUrl

        if needsCleanup {
            cleanupPlayerSlot(availableSlot)
        }

        // Update assignments immediately for current index responsiveness
        hookIndexToPlayer[hookIndex] = availableSlot
        playerToHookIndex[availableSlot] = hookIndex

        guard let urlString = hook.streamingUrl ?? hook.renderedVideoUrl ?? hook.renderedVideoPreviewUrl,
              let url = URL(string: urlString)
        else { return }

        let asset: AVURLAsset
        if let preloadedAsset = preloadedAssets[hook.id] {
            asset = preloadedAsset
        } else {
            asset = createOptimizedVideoAsset(for: url)
            addToPreloadedCache(hookId: hook.id, asset: asset)
            Task {
                // Preload properties on background thread for future use
                await preloadAssetProperties(asset, hookId: hook.id)
            }
        }
        let playerItem = AVPlayerItem(asset: asset)

        optimizePlayerItemForNetworkConditions(playerItem)

        if shouldPlay {
            // Makes sure we don't show any incorrect "Paused" states
            // while we load the track
            self.isReplacingCurrentItem = true
        }

        let needsReplacement = currentAssetURL != url

        if needsReplacement {
            player.replaceCurrentItem(with: playerItem)
            player.playImmediately(atRate: 1.0)
            player.pause()
        }

        player.isMuted = true
        player.seek(to: .zero, toleranceBefore: .zero, toleranceAfter: .zero) { [weak self] _ in
            guard let self else { return }
            guard self.currentIndex == hookIndex, self.previousIndex == nil else { return }
            self.updateCurrentHookPlayState()
        }

        setupObserversForPlayerSlot(availableSlot, player: player)
    }

    func getPlayer(for index: Int) -> AVPlayer? {
        // Return existing player if available
        if let playerSlot = hookIndexToPlayer[index] {
            return playerPool[playerSlot]
        }

        guard index >= 0, index < hooks.count else { return nil }
        let hook = hooks[index]

        if let hookUrl = hook.streamingUrl ?? hook.renderedVideoUrl ?? hook.renderedVideoPreviewUrl,
           let targetUrl = URL(string: hookUrl)
        {
            for (playerSlot, player) in playerPool.enumerated() {
                if let currentItem = player.currentItem,
                   let currentAsset = currentItem.asset as? AVURLAsset,
                   currentAsset.url == targetUrl
                {
                    if let oldIndex = playerToHookIndex[playerSlot] {
                        hookIndexToPlayer.removeValue(forKey: oldIndex)
                    }
                    hookIndexToPlayer[index] = playerSlot
                    playerToHookIndex[playerSlot] = index

                    return player
                }
            }
        }

        let isInWindow = (index >= currentIndex - preloadBackward) && (index <= currentIndex + preloadForward)
        guard isInWindow else { return nil }

        ensurePlayerAssigned(for: index)

        // Return player if assigned
        guard let playerSlot = hookIndexToPlayer[index] else { return nil }
        return playerPool[playerSlot]
    }

    func getCurrentPlayer() -> AVPlayer? {
        if let playerSlot = hookIndexToPlayer[currentIndex] {
            return playerPool[playerSlot]
        }
        return nil
    }

    func getCurrentHook() -> Hook? {
        guard currentIndex >= 0, currentIndex < hooks.count else { return nil }
        return hooks[currentIndex]
    }

    func getCurrentTime(for index: Int) -> TimeInterval {
        if let playerSlot = hookIndexToPlayer[index] {
            return playerPool[playerSlot].currentTime().seconds
        }
        return 0
    }

    func getCurrentIndex() -> Int {
        return currentIndex
    }

    func isHookPlaying(at index: Int) -> Bool {
        if let playerSlot = hookIndexToPlayer[index] {
            let player = playerPool[playerSlot]
            return player.rate > 0 && index == getCurrentIndex()
        }
        return false
    }

    func getPlaybackState(at index: Int) -> PlaybackState {
        // If this is the current index, return the tracked state
        if index == getCurrentIndex() {
            return currentPlaybackState
        }
        return .ready
    }

    func getPlayerSlotForIndex(_ index: Int) -> Int? {
        return hookIndexToPlayer[index]
    }

    func getHooks() -> [Hook] {
        return hooks
    }

    func togglePlayPause() {
        let activeIndex = getCurrentIndex()
        if let playerSlot = hookIndexToPlayer[activeIndex] {
            let player = playerPool[playerSlot]
            if player.rate > 0 {
                // Use proper pause method with analytics
                pauseCurrentHook(cause: .manual)
            } else {
                guard shouldAutoPlayHook(at: activeIndex) else { return }
                // Use proper play method with analytics
                playCurrentHook(cause: .manual)
            }
        }
    }

    func playCurrentHook(cause: HookPlayCause = .manual) {
        // Block appForeground resume only if user manually paused
        if case .appForeground = cause {
            // Block appForeground if last pause was navigation or manual
            switch lastPauseCause {
            case .appInactive, .appBackground:
                let shouldResume = wasPlayingBeforeInterruption == true
                wasPlayingBeforeInterruption = nil
                if !shouldResume { return }
            case .navigation, .manual:
                // User intentionally paused or navigated away - block resume
                return
            case .scroll, .reloadFeed, .appTerminate, .openDeeplink, .audioInterruption:
                // System actions or interruptions - allow resume
                break
            case .openOmniPlayer:
                // OmniPlayer pause - allow resume when returning to hooks
                break
            case .none:
                // No previous pause recorded - allow resume
                break
            }
        }

        // Block navigation resume for manually paused Hooks
        if case .resume = cause {
            if currentPlaybackState == .paused, !wasPausedByInterruption {
                return
            }
        }

        let activeIndex = getCurrentIndex()
        // Only allow play if the hook is not reported/hidden
        guard shouldAutoPlayHook(at: activeIndex) else { return }

        if let playerSlot = hookIndexToPlayer[activeIndex] {
            let player = playerPool[playerSlot]

            // Track manual play actions
            if case .manual = cause {
                wasPausedByInterruption = false
                lastPauseCause = nil
            }

            guard let hook = getCurrentHook() else { return }

            // Track the play with specific cause - this sets up the session for timing
            if let analytics = analyticsClient {
                analytics.trackPlayHook(hook, player.currentTime().seconds, cause)
            }

            player.isMuted = isMuted
            player.play()

            updatePlaybackState(.playing, hook.id)
        }
    }

    func pauseCurrentHook(cause: HookPauseCause = .manual) {
        lastPauseCause = cause
        switch cause {
        case .audioInterruption, .appBackground, .appInactive:
            // System interruptions that should allow auto-resume
            if wasPlayingBeforeInterruption == nil {
                wasPlayingBeforeInterruption = (currentPlaybackState == .playing)
            }
            wasPausedByInterruption = true
        case .navigation:
            // Navigation pauses should NOT auto-resume (i.e create hook modal)
            wasPausedByInterruption = false
        default:
            // Manual pause or other causes - dont auto-resume
            wasPausedByInterruption = false
        }

        let activeIndex = getCurrentIndex()
        if let playerSlot = hookIndexToPlayer[activeIndex] {
            let player = playerPool[playerSlot]
            player.pause()
            player.isMuted = isMuted

            guard let hook = getCurrentHook() else { return }

            // Send different playback states based on pause cause
            let playbackState: PlaybackState
            switch cause {
            case .navigation,
                 .openOmniPlayer,
                 .appBackground,
                 .reloadFeed:
                playbackState = .stopped
            default:
                playbackState = .paused
            }

            updatePlaybackState(playbackState, hook.id)

            if let analytics = analyticsClient {
                let currentTime = player.currentTime().seconds
                analytics.trackPauseHook(hook, currentTime, cause)
            }
        }

        // Flush pending play counts when app goes to background
        if case .appBackground = cause {
            Task {
                await playCountManager.forceFlushPlayCountBatch()
            }
        }
    }

    func restartCurrentHook() {
        let activeIndex = getCurrentIndex()
        if let playerSlot = hookIndexToPlayer[activeIndex] {
            let player = playerPool[playerSlot]
            player.seek(to: .zero)
        }
    }

    func getMuteState() -> Bool {
        return isMuted
    }

    func setMuteState(_ isMuted: Bool) {
        self.isMuted = isMuted

        let activeIndex = getCurrentIndex()
        if let playerSlot = hookIndexToPlayer[activeIndex] {
            let player = playerPool[playerSlot]
            player.isMuted = isMuted
        }
        if let analytics = analyticsClient {
            Task {
                await analytics.setMuteState(isMuted)
            }
        }
    }

    func setOnboardingState(_ isShowingOnboarding: Bool) {
        self.isShowingOnboarding = isShowingOnboarding

        // If onboarding was dismissed, update current hook play state
        if !isShowingOnboarding {
            updateCurrentHookPlayState()
        }
    }

    func setInitialConfig(_ playbackConfig: PlaybackConfig) {
        self.isShowingOnboarding = playbackConfig.isShowingOnboarding
        self.isMuted = playbackConfig.startMuted

        // Update any currently assigned players with the new mute state
        let activeIndex = getCurrentIndex()
        if let playerSlot = hookIndexToPlayer[activeIndex] {
            let player = playerPool[playerSlot]
            player.isMuted = playbackConfig.startMuted
        }
    }

    func cleanup() {
        // Cancel any pending window adjustment work
        windowAdjustmentTask?.cancel()
        windowAdjustmentTask = nil

        // Clean up all observers and players for all slots
        cleanupPlayerObservers()

        // Clean up audio session observers
        cleanupAudioSessionObservers()

        if let observer = globalEndObserver {
            NotificationCenter.default.removeObserver(observer)
            globalEndObserver = nil
        }

        preloadedAssets.removeAll()

        // Flush any remaining play count batch
        Task {
            await playCountManager.forceFlushPlayCountBatch()
        }
    }

    private func cleanupAudioSessionObservers() {
        if let observer = audioInterruptionObserver {
            NotificationCenter.default.removeObserver(observer)
            audioInterruptionObserver = nil
        }
        if let observer = audioRouteChangeObserver {
            NotificationCenter.default.removeObserver(observer)
            audioRouteChangeObserver = nil
        }
    }

    private func setupGlobalEndObserver() {
        globalEndObserver = NotificationCenter.default.addObserver(
            forName: .AVPlayerItemDidPlayToEndTime,
            object: nil,
            queue: .main
        ) { [weak self] notification in
            Task { [weak self] in
                guard let self = self else { return }
                guard let endedItem = notification.object as? AVPlayerItem else { return }
                await self.onItemDidPlayToEnd(endedItem)
            }
        }
    }

    private func onItemDidPlayToEnd(_ item: AVPlayerItem) async {
        guard let playerSlot = playerPool.firstIndex(where: { $0.currentItem === item }) else { return }
        let currentIndex = getActiveCurrentIndex()
        let currentPlayerSlot = hookIndexToPlayer[currentIndex]
        guard currentPlayerSlot == playerSlot else { return }
        let player = playerPool[playerSlot]
        handleVideoEnded(player: player)
    }

    // MARK: - Hidden Creator Handles & Reported Hooks Management

    func updateHiddenCreatorHandles(_ handles: [String: Bool]) {
        hiddenCreatorHandles = handles
        updateCurrentHookPlayState()
    }

    func updateReportedHooks(_ hookIds: [String: Bool]) {
        reportedHookIds = hookIds
        updateCurrentHookPlayState()
    }

    private func updateCurrentHookPlayState() {
        let currentIdx = getActiveCurrentIndex()
        guard let playerSlot = hookIndexToPlayer[currentIdx]
        else { return }

        let player = playerPool[playerSlot]
        let shouldPlay = shouldAutoPlayHook(at: currentIdx)

        if shouldPlay {
            guard let hook = getCurrentHook() else { return }

            analyticsClient?.trackPlayNewHook(hook)

            player.isMuted = isMuted
            player.play()
        } else {
            // Exit early if we're already paused
            if player.rate == 0 {
                return
            }

            player.pause()
            player.isMuted = true
            // Track analytics for pause
            Task.detached(priority: .utility) { [weak self] in
                guard let self else { return }

                guard !Task.isCancelled else { return }

                guard let hook = await self.getCurrentHook() else { return }

                guard let analytics = await self.analyticsClient else { return }

                let currentTime = await self.getCurrentPlayer()?.currentTime().seconds ?? 0.0

                analytics.trackPauseHook(hook, currentTime, .manual)
            }
        }
    }

    private func shouldAutoPlayHook(at index: Int) -> Bool {
        guard isOnHooksFeed, !isShowingOnboarding, index >= 0, index < hooks.count else { return false }
        let hook = hooks[index]

        // Check if hook is individually reported
        if reportedHookIds[hook.id] ?? false {
            return false
        }

        // Check if creator is hidden
        if let handle = hook.user?.handle,
           hiddenCreatorHandles[handle] ?? false
        {
            return false
        }

        return true
    }

    private func cleanupPlayerObservers() {
        // Clean up all observers and players for all slots
        for playerSlot in 0 ..< maxPlayers {
            cleanupPlayerSlot(playerSlot)
        }
    }

    private func clearAllPlayerAssignments() {
        // Clean up all observers and players for all slots
        for playerSlot in 0 ..< maxPlayers {
            cleanupPlayerSlot(playerSlot)
        }
    }

    // MARK: - Player Observers

    private func handlePlayerReady(player _: AVPlayer, playerSlot: Int) {
        guard let hookIndex = playerToHookIndex[playerSlot] else { return }

        let activeHooks = getActiveHooks()
        let activeCurrentIndex = getActiveCurrentIndex()

        if activeHooks.indices.contains(hookIndex) {
            let hook = activeHooks[hookIndex]

            // Always send ready event to cell for refreshPlayer, but only update global state for current hook
            if hookIndex == activeCurrentIndex {
                self.updatePlaybackState(.ready, hook.id)
            } else {
                // This makes sure the cell has the video before we get there
                eventSubject.send(.playbackStateChanged(hook.id, .ready))
            }
        }
    }

    private func handleVideoEnded(player: AVPlayer) {
        guard let hook = getCurrentHook() else { return }

        isLooping = true

        // Make sure we have end time before seeking to 0s
        let endTime = player.currentTime().seconds
        let shouldAutoPlay = shouldAutoPlayHook(at: getActiveCurrentIndex())

        Task {
            guard let analytics = analyticsClient else { return }
            analytics.trackAutoRepeatPauseHook(hook, endTime)
            await player.seek(to: .zero)
            if shouldAutoPlay {
                analytics.trackAutoRepeatPlaySameHook(hook)
                player.play()
            } else {
                self.setIsLooping(false)
            }
        }
    }

    private func handlePlayerItemStatusChange(player: AVPlayer, item: AVPlayerItem) {
        guard let playerSlot = playerPool.firstIndex(of: player) else { return }

        switch item.status {
        case .readyToPlay:
            handlePlayerReady(player: player, playerSlot: playerSlot)

        case .failed:
            if let error = item.error,
               let hookIndex = playerToHookIndex[playerSlot],
               let hook = hooks.indices.contains(hookIndex) ? hooks[hookIndex] : nil
            {
                eventSubject.send(.playbackFailed(hook.id, error))
            }

        case .unknown:
            // Player item is loading or being replaced
            if let hookIndex = playerToHookIndex[playerSlot],
               hookIndex == getActiveCurrentIndex(),
               let hook = hooks.indices.contains(hookIndex) ? hooks[hookIndex] : nil
            {
                updatePlaybackState(.loading, hook.id)
            }

        @unknown default:
            break
        }
    }

    /*
     Sets up persistent observers for a player slot. These stay active for the lifetime
     of content in that slot, reducing CPU overhead and eliminating setup lag.
     - Loop observer: tells us if the video has reached the end
     - Status observer: tells us if we think the video is ready to play
     - Playback task: tells us if we think the video is playing
     */
    private func setupObserversForPlayerSlot(
        _ playerSlot: Int,
        player: AVPlayer
    ) {
        // Clean up any existing observers for this slot first
        cleanupObserversForPlayerSlot(playerSlot)

        // Wait for `readyToPlay` from AVPlayerItem's status if we have a current item
        if let playerItem = player.currentItem {
            let statusObserver = playerItem.observe(\.status, options: [.new, .initial]) { [weak self] item, _ in
                guard let self = self else { return }

                Task {
                    await self.handlePlayerItemStatusChange(player: player, item: item)
                }
            }
            statusObservers[playerSlot] = statusObserver
        }

        // Wait for `playing` or `paused` from AVPlayer's timeControlStatus
        // This helps us
        let timeControlStatusStream = AsyncStream<AVPlayer.TimeControlStatus> { continuation in
            let observation = player.observe(\.timeControlStatus, options: [.new, .initial]) { player, _ in
                continuation.yield(player.timeControlStatus)
            }
            continuation.onTermination = { _ in
                observation.invalidate()
            }
        }

        // Don't set it up if the observer already exists
        if timeControlObservers[playerSlot] != nil {
            return
        }

        let observerTask = Task {
            var isPlaying = false
            for await status in timeControlStatusStream {
                let (
                    hookIndex,
                    currentIndex,
                    hook,
                    currentlyLooping,
                    currentlyReplacing,
                    currentState,
                    currentTime
                ) = self.getPlayerState(playerSlot, player: player)

                guard let analytics = self.analyticsClient, let hook else { continue }

                let playbackState: PlaybackState
                switch status {
                case .playing:
                    playbackState = .playing

                    // Simply relying on the AVPlayer's status to determine if we're playing
                    // isn't enough because it can fire `.playing` more than once
                    // when buffering (like after swiping quickly)
                    let hasValidItem = player.currentItem != nil
                    let isBuffered = player.currentItem?.isPlaybackLikelyToKeepUp == true
                    let hasRate = player.rate > 0
                    let isActuallyPlaying = hasValidItem && isBuffered && hasRate

                    if isActuallyPlaying && isPlaying == false {
                        isPlaying = true

                        // Record actual playback start and send play event with timeToFirstFrameSecs
                        // The event type was already set when we called the track method before player.play()
                        await analytics.recordActualPlaybackStart(hook, currentTime.seconds)

                        if currentlyLooping {
                            // Reset looping flag after successful restart
                            self.setIsLooping(false)
                        }

                        // Mark as /watched
                        await self.playCountManager.recordPlayCountIfNeeded(for: hook.id)
                    }

                case .paused:
                    isPlaying = false
                    guard currentlyReplacing == false else { continue }
                    playbackState = .paused

                case .waitingToPlayAtSpecifiedRate:
                    playbackState = .loading

                @unknown default:
                    playbackState = .loading
                }

                guard let hookIndex,
                      hookIndex == currentIndex,
                      currentState != .stopped else { continue }

                guard currentlyLooping == false || playbackState == .playing else { continue }
                self.updatePlaybackState(playbackState, hook.id)
            }
        }

        timeControlObservers[playerSlot] = observerTask
    }

    private func cleanupObserversForPlayerSlot(_ playerSlot: Int) {
        statusObservers[playerSlot]?.invalidate()
        statusObservers.removeValue(forKey: playerSlot)

        timeControlObservers[playerSlot]?.cancel()
        timeControlObservers.removeValue(forKey: playerSlot)
    }

    private func cleanupPlayerSlot(_ playerSlot: Int) {
        // Clean up observers first
        cleanupObserversForPlayerSlot(playerSlot)

        // Clean up actual player
        let player = playerPool[playerSlot]
        player.pause()
        player.replaceCurrentItem(with: nil)
        player.isMuted = true

        // Remove assignments
        if let hookIndex = playerToHookIndex[playerSlot] {
            hookIndexToPlayer.removeValue(forKey: hookIndex)
        }
        playerToHookIndex.removeValue(forKey: playerSlot)
    }

    private func setCurrentPlaybackState(_ state: PlaybackState) {
        currentPlaybackState = state
    }

    private func setIsLooping(_ looping: Bool) {
        isLooping = looping
    }

    private func updatePlaybackState(_ state: PlaybackState, _ hookId: String) {
        isLooping = false
        currentPlaybackState = state
        eventSubject.send(.playbackStateChanged(hookId, state))
    }

    private func getPlayerState(_ playerSlot: Int, player: AVPlayer) -> (
        hookIndex: Int?,
        currentIndex: Int,
        hook: Hook?,
        currentlyLooping: Bool,
        currentlyReplacing: Bool,
        currentState: PlaybackState,
        currentTime: CMTime
    ) {
        let hookIndex = playerToHookIndex[playerSlot]
        let currentIndex = self.currentIndex
        let hook = hookIndex.flatMap { hooks.indices.contains($0) ? hooks[$0] : nil }
        let currentlyLooping = self.isLooping
        let currentState = self.currentPlaybackState
        let currentlyReplacing = self.isReplacingCurrentItem
        let currentTime = player.currentTime()
        return (hookIndex, currentIndex, hook, currentlyLooping, currentlyReplacing, currentState, currentTime)
    }

    // MARK: - Private Implementation

    private func ensurePlayerAssigned(for index: Int) {
        guard index >= 0, index < hooks.count else { return }

        // If already assigned, we're done
        if hookIndexToPlayer[index] != nil {
            return
        }

        // Only assign if within the current window to avoid aggressive reassignment
        let startIndex = max(0, currentIndex - preloadBackward)
        let endIndex = min(hooks.count - 1, currentIndex + preloadForward)

        if index >= startIndex, index <= endIndex {
            assignPlayerForHook(index, shouldPlay: index == currentIndex)
        }
    }

    private func rebuildWindow() {
        let activeHooks = getActiveHooks()
        let activeCurrentIndex = getActiveCurrentIndex()
        let startIndex = max(0, activeCurrentIndex - preloadBackward)
        let endIndex = min(activeHooks.count - 1, activeCurrentIndex + preloadForward)

        // Make sure the start and end indices are valid
        guard startIndex <= endIndex else { return }

        // Only assign players for missing slots in the window
        for index in startIndex ... endIndex {
            if hookIndexToPlayer[index] == nil {
                // Only shouldPlay for current index and only if autoplay is allowed
                let shouldPlay = (index == activeCurrentIndex) && shouldAutoPlayHook(at: index)
                assignPlayerForHook(index, shouldPlay: shouldPlay)
            }
        }
    }

    private func findAvailablePlayerSlot() -> Int {
        // Find a slot that's not currently assigned
        for slot in 0 ..< maxPlayers {
            if playerToHookIndex[slot] == nil {
                return slot
            }
        }

        // If all slots are taken, recycle the one furthest from current index
        // without clearing the current player's assignment
        var furthestSlot = 0
        var furthestDistance = 0

        let activeCurrentIndex = getActiveCurrentIndex()
        for (slot, hookIndex) in playerToHookIndex {
            guard hookIndex != activeCurrentIndex else { continue }

            let distance = abs(hookIndex - activeCurrentIndex)
            if distance > furthestDistance {
                furthestDistance = distance
                furthestSlot = slot
            }
        }

        // Safety check: Make sure we found a valid slot to recycle
        guard furthestDistance > 0 else {
            return 0
        }

        if let oldHookIndex = playerToHookIndex[furthestSlot] {
            hookIndexToPlayer.removeValue(forKey: oldHookIndex)
        }

        playerToHookIndex.removeValue(forKey: furthestSlot)

        return furthestSlot
    }

    // Creates an optimized `AVURLAsset` for video feed playback optimized for fast switching
    private func createOptimizedVideoAsset(for url: URL) -> AVURLAsset {
        return AVURLAsset(url: url, options: [
            AVURLAssetPreferPreciseDurationAndTimingKey: false,
            AVURLAssetHTTPCookiesKey: [], // Disable cookies for performance
            AVURLAssetAllowsCellularAccessKey: true,
        ])
    }

    // Optimizes AVPlayerItem for network conditions and mobile video streaming
    private func optimizePlayerItemForNetworkConditions(_ playerItem: AVPlayerItem) {
        playerItem.preferredForwardBufferDuration = 2.0
        playerItem.canUseNetworkResourcesForLiveStreamingWhilePaused = false
        playerItem.startsOnFirstEligibleVariant = true
        playerItem.preferredMaximumResolution = CGSize(width: 1920, height: 1080) // Cap at 1080p for cellular
    }

    /// Pre-loads AVAsset properties on background thread to prevent main thread blocking
    private func preloadAssetProperties(_ asset: AVURLAsset, hookId: String) async {
        do {
            // Check for cancellation before loading
            try Task.checkCancellation()

            // Load essential properties
            let (duration, isPlayable) = try await asset.load(.duration, .isPlayable)

            // Check for cancellation after loading
            try Task.checkCancellation()

            // Basic validation
            guard duration.isValid,
                  duration.seconds > 0,
                  !duration.seconds.isNaN,
                  isPlayable
            else {
                return
            }

            // Store asset after successful property loading
            addToPreloadedCache(hookId: hookId, asset: asset)

        } catch is CancellationError {
            return
        } catch {
            log.telemetry.error(error, message: "Error loading asset properties for hook \(hookId)")
        }
    }

    private func adjustWindow(aroundIndex: Int) async {
        Task {
            // Preload AVAsset's so players can pick them up
            // to create `AVPlayerItem`s are we swipe
            await preloadUpcomingVideos(currentIndex: aroundIndex)
        }

        if hookIndexToPlayer[currentIndex] == nil {
            // Prioritize current index
            assignPlayerForHook(currentIndex, shouldPlay: true)
        }

        let activeHooksCount = getActiveHooksCount()
        let startIndex = max(0, aroundIndex - preloadBackward)
        let endIndex = min(activeHooksCount - 1, aroundIndex + preloadForward)

        guard startIndex <= endIndex else { return }

        // Process adjacent indices first (closer to current), then outer ones
        let sortedIndices = (startIndex ... endIndex).sorted { index1, index2 in
            let distance1 = abs(index1 - aroundIndex)
            let distance2 = abs(index2 - aroundIndex)
            return distance1 < distance2
        }

        for index in sortedIndices {
            guard index != aroundIndex else { continue }
            if hookIndexToPlayer[index] == nil {
                if let reusablePlayerSlot = findReusablePlayerSlot(targetIndex: index, windowStart: startIndex, windowEnd: endIndex) {
                    reassignPlayer(from: reusablePlayerSlot, to: index)
                } else {
                    assignPlayerForHook(index, shouldPlay: false)
                }
            }
        }
    }

    private func findReusablePlayerSlot(targetIndex _: Int, windowStart: Int, windowEnd: Int) -> Int? {
        // Find a player that's assigned to an index outside the current window
        for (playerSlot, hookIndex) in playerToHookIndex {
            // Don't reuse the current player
            guard hookIndex != currentIndex else { continue }

            // Check if this hookIndex is outside the window
            if hookIndex < windowStart || hookIndex > windowEnd {
                return playerSlot
            }
        }

        return nil
    }

    private func reassignPlayer(from playerSlot: Int, to newIndex: Int) {
        let activeHooks = getActiveHooks()
        guard newIndex >= 0, newIndex < activeHooks.count else { return }

        // Clean up the player slot
        cleanupPlayerSlot(playerSlot)

        // Get the player and assign new content
        let player = playerPool[playerSlot]
        let hook = activeHooks[newIndex]

        // Load new content
        if let urlString = hook.streamingUrl ?? hook.renderedVideoUrl ?? hook.renderedVideoPreviewUrl,
           let url = URL(string: urlString)
        {
            Task {
                let asset: AVURLAsset
                // Check if we have a preloaded asset
                if let preloadedAsset = preloadedAssets[hook.id] {
                    asset = preloadedAsset
                } else {
                    // Otherwise, create a new asset
                    asset = createOptimizedVideoAsset(for: url)
                    await preloadAssetProperties(asset, hookId: hook.id)
                }
                let playerItem = AVPlayerItem(asset: asset)

                let currentAssetURL = await MainActor.run {
                    (player.currentItem?.asset as? AVURLAsset)?.url
                }
                let needsReplacement = currentAssetURL != url

                if needsReplacement {
                    optimizePlayerItemForNetworkConditions(playerItem)

                    player.replaceCurrentItem(with: playerItem)
                    player.playImmediately(atRate: 1.0)
                    player.pause()
                }

                player.isMuted = true

                player.seek(to: .zero, toleranceBefore: .zero, toleranceAfter: .zero) { _ in
                    player.pause()
                }
            }
        }

        hookIndexToPlayer[newIndex] = playerSlot
        playerToHookIndex[playerSlot] = newIndex

        if timeControlObservers[playerSlot] == nil {
            setupObserversForPlayerSlot(playerSlot, player: player)
        }
    }

    func nextPlayableIndex() -> Int? {
        let activeCurrentIndex = getActiveCurrentIndex()
        let nextIndex = activeCurrentIndex + 1
        let activeHooksCount = getActiveHooksCount()

        for index in nextIndex ..< activeHooksCount {
            guard shouldAutoPlayHook(at: index) else { continue }
            return index
        }

        return nil
    }

    func insertHookAtCurrentIndex(_ hook: Hook, source _: HooksFeedSource) {
        let index = getActiveCurrentIndex()

        // Check if hook already exists at current index
        if !hooks.isEmpty, hooks[index] == hook {
            return
        }

        if let existingIndex = hooks.firstIndex(where: { $0.id == hook.id }) {
            hooks.remove(at: existingIndex)
            // Adjust target index if the removed hook was before our insertion point
            let adjustedIndex = existingIndex < index ? index - 1 : index

            // Clean up all players since we're moving a hook which shifts indices
            cleanupPlayerObservers()

            hooks.insert(hook, at: adjustedIndex)
            currentIndex = adjustedIndex
        } else {
            // Clean up all players since we're inserting a new hook which shifts indices
            cleanupPlayerObservers()
            hooks.insert(hook, at: index)
        }
        rebuildWindow()
        setMuteState(false)
    }

    private func addToPreloadedCache(hookId: String, asset: AVURLAsset) {
        // Add the new asset
        preloadedAssets[hookId] = asset

        // If we exceed the limit, remove oldest entries
        if preloadedAssets.count > maxPreloadedAssets {
            // Remove excess entries (simple approach - remove random ones)
            let excessCount = preloadedAssets.count - maxPreloadedAssets
            let keysToRemove = Array(preloadedAssets.keys.prefix(excessCount))

            for key in keysToRemove {
                preloadedAssets.removeValue(forKey: key)
            }
        }
    }

    private func preloadUpcomingVideos(currentIndex: Int) async {
        let activeHooks = getActiveHooks()
        // Preload assets for the next 8 videos
        let preloadDistance = 8

        // Pre-load both forward AND backward for better swiping experience
        let forwardRange = 1 ... preloadDistance
        let backwardStart = max(0, currentIndex - 3)
        let backwardEnd = max(0, currentIndex - 1)
        let backwardRange = backwardStart ... backwardEnd

        // Pre-load forward videos
        for offset in forwardRange {
            let targetIndex = currentIndex + offset
            guard targetIndex < activeHooks.count else { break }

            let hook = activeHooks[targetIndex]

            let needsPreload = preloadedAssets[hook.id] == nil

            if needsPreload {
                guard let urlString = hook.streamingUrl ?? hook.renderedVideoUrl ?? hook.renderedVideoPreviewUrl,
                      let url = URL(string: urlString) else { continue }

                // Create and prepare Asset
                let asset = createOptimizedVideoAsset(for: url)

                // Pre-load asset properties
                await preloadAssetProperties(asset, hookId: hook.id)

                try? await Task.sleep(for: .milliseconds(25))
            } else {}
        }

        // Pre-load backward videos (if we're not at the beginning)
        if currentIndex > 0 {
            for targetIndex in backwardRange.reversed() {
                guard targetIndex >= 0 else { continue }

                let hook = activeHooks[targetIndex]

                // Check if this video needs preloading
                let needsPreload = preloadedAssets[hook.id] == nil

                if needsPreload {
                    guard let urlString = hook.streamingUrl ?? hook.renderedVideoUrl ?? hook.renderedVideoPreviewUrl,
                          let url = URL(string: urlString) else { continue }

                    // Create and prepare Asset
                    let asset = createOptimizedVideoAsset(for: url)

                    // Pre-load asset properties
                    await preloadAssetProperties(asset, hookId: hook.id)

                    try? await Task.sleep(for: .milliseconds(25))
                }
            }
        }
    }
}
