import APIClient
import CommentsClient
import ComponentLibrary
import ComposableArchitecture
import CoreMedia
import EventBusClient
import FeatureCaptions
import FeatureClipDetail
import FeatureComments
import FeatureHooksModels
import FeatureHooksMoreMenu
import FeatureOmniPlayer
import FeatureProfile
import FeatureToasts
import Foundation
import HooksPlayerClient
import Localization
import LyricsClient
import NavigationRouterClient
import OmniPlayerClient
import PlayerUtilities
import SongActionsClient
import SwiftUI
import Utilities

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

/*
 - HooksPlayerClient interfaces with HooksFeedViewController to play videos
 - HooksFeedOverlay handles UI overlays and controls
 - This reducer helps us manage both
 */

@Reducer
public struct HooksFeedReducer {
    @Reducer(state: .equatable)
    public enum Destination {
        case addToPlaylist(SelectPlaylist)
        case comments(CommentsSheetReducer<Hook>)
        case profile(PublicProfileV1)
    }

    @ObservableState
    public struct State: Equatable {
        @Presents public var destination: Destination.State?
        @Shared public var me: Me

        /// Tracks whether user is on Hooks tab at root level (no navigation stack).
        /// Managed by RootTab+Navigation based on tab selection and navigation stack state.
        /// Does NOT track RootCoordinator-level modals (like hooksCreate, omniplayer).
        @Shared(.inMemory(.isOnHooksFeed)) var isOnHooksFeed: Bool = true

        /// Tracks whether the hooks omniplayer modal is visible.
        /// Managed by HooksFeedReducer when omniplayer opens/closes.
        @Shared(.inMemory(.isHooksOmniPlayerVisible)) var isHooksOmniPlayerVisible: Bool = false

        /// Tracks whether the hooks create flow modal is visible.
        /// Set to true in HooksFeedScreenReducer when create button is tapped.
        /// Set to false in RootCoordinator when create flow is dismissed.
        @Shared(.inMemory(.isHooksCreateVisible)) var isHooksCreateVisible: Bool = false

        public var hooks: IdentifiedArrayOf<Hook> = []
        public var currentIndex: Int = 0

        // Loading state used to account for initial load vs refresh
        public var loadingState: LoadingState = .idle

        public var currentPage: Int = 0
        public var playingHookId: String?

        // Track player ready events to trigger UI layer refreshes
        public var playerReadyIndex: Int?

        public var currentPlaybackState: PlaybackState = .loading

        public var hooksMetadata = HooksMetadataState()

        public var showOnboarding: Bool = false

        public var hideTopNavBar: Bool = false

        // If `source` isn't `.hooksTab`, we're in a contextual feed
        // where the user is playing Hooks from Profile or My Hooks.
        // This usually comes with a set of `initialHooks` to display
        // since they're likely on screen in a row or HooksGridScreen.
        public var source: HooksFeedSource = .hooksFeed
        // Starting position in the feed
        public var startIndex: Int = 0
        // Override initial hooks API call if provided,
        // like we do for contextual feeds
        public var initialHooks: [Hook]?

        public let likeEventDeduplicationWindow: TimeInterval = 0.5
        public var recentLikeEvents: [String: Date] = [:]

        // Allows us to control when to force update players
        // for ex. only initialLoad rebuilds the window proper
        public var refreshType: RefreshType = .initialLoad

        // Unique ID for each feed instance that helps us
        // filter `refreshFeed` events coming from `SlidingWindowState`
        public var feedId: UUID = UUID()

        public enum LoadingState: Equatable {
            case idle
            case loading
            case loaded
            case reloading
            // Used to make sure we only load or reload the feed once
            var reloadable: Bool {
                switch self {
                case .idle, .loaded:
                    return true
                case .loading, .reloading:
                    return false
                }
            }
        }

        public enum RefreshType: Equatable {
            case none
            case initialLoad // First time entering contextual feed
            case restore // Returning from nested contextual feed
            case reloadFeed // User double-tapped Hooks tab icon to reload feed
            case reloadNext // Add hooks after current index without disrupting playback
            case reloadCurrentIndex // Reload the current index
        }

        public var commentsSheetState: CommentsSheet?

        // Determines if to open a specific sub-view when the hook feed is opened.
        // For example, open the hooks feed, then a specific hook, then the comments for it.
        public var navigationOptions: HookNavigationOptions?

        public var meHandle: String {
            me.user.handle
        }

        public init(
            me: Shared<Me>,
            initialHooks: [Hook]? = nil,
            startIndex: Int = 0,
            source: HooksFeedSource = .hooksFeed,
            navigationOptions: HookNavigationOptions? = nil,
            alwaysFocused: Bool = true,
            showOnboarding: Bool = false
        ) {
            self._me = me
            self.initialHooks = initialHooks
            self.startIndex = startIndex
            self.source = source
            self.navigationOptions = navigationOptions
            self.showOnboarding = showOnboarding

            // For main hooks feed, start muted (show "Tap to Unmute")
            // Otherwise, don't start muted or show the overlay
            self.didTapToUnmute = source != .hooksFeed
            self.isFocused = alwaysFocused || source != .hooksFeed
            self.disableScroll = false

            if self.isFocused {
                self.lastFocusedIndex = startIndex
            }

            if showOnboarding {
                self.didTapToUnmute = false
            }
        }

        public var shouldLoadNext: Bool {
            guard source.canFetchMore else { return false }
            return currentIndex >= hooks.count - 3
        }

        public func getIsFollowing(for handle: String) -> Bool? {
            return hooksMetadata.getIsFollowing(for: handle)
        }

        public func getIsLiked(for hookId: String) -> Bool? {
            return hooksMetadata.getIsLiked(for: hookId)
        }

        public func getLikeCount(for hookId: String) -> Int? {
            return hooksMetadata.getLikeCount(for: hookId)
        }

        public func getCommentCount(for hookId: String) -> Int? {
            return hooksMetadata.getCommentCount(for: hookId)
        }

        public func getClipLikeStatus(for hookId: String) -> Bool? {
            return hooksMetadata.getClipLikeStatus(for: hookId)
        }

        var currentHookRecommendationMetadata: HooksRecommendationMetadata {
            let currentHook = self.hooks[self.currentIndex]
            let metadata = HooksRecommendationMetadata(
                contextType: self.source.analyticsContext.contextType,
                hookId: currentHook.id,
                recommendationItemId: currentHook.recommendationItemId
            )
            return metadata
        }

        @ObservationStateIgnored @ObservedBox public var expandedState: ExpandedPlayerReducer.State?

        // Used to handle animating the ExpandedPlayer sheet up and down
        public var isShowingOmniPlayer: Bool = false
        public var didTapProfileInOmniPlayer: Bool = false
        public var omniPlayerAnimationDuration: TimeInterval = 0.7
        public var omniPlayerTeardownAfterCloseWithDelay: TimeInterval = 1.0
        public var isSwipingToShowProfile: Bool = false
        public var isSwipingToHideProfile: Bool = false

        // Allows for an idle state when on the Hooks Tab and
        // the user hasn't interacted with the feed yet.
        public var isFocused: Bool
        // Helps us determine when to allow the swipe-to-minimize
        // gesture without degrading performance for all cell swipes
        // This gets set as currentIndex on init and when we call
        // `setFeedInFocus`.
        public var lastFocusedIndex: Int?

        // Additional override to disable scrolling in certain situations
        // (eg. feed is still in focus but being minimized/dragged down)
        public var disableScroll: Bool

        // Tap to unmute overlay state
        public var didTapToUnmute: Bool = false

        // Haptics on success states that need it (reload feed, for example)
        public var hapticProxy: Int = 0

        // Used when skipping to the next Hook after Hide Creator
        public var shouldAnimateIndexChange: Bool = false

        // Checks if we're on the main Hooks tab feed
        public var isOnHooksTabFeed: Bool {
            guard source == .hooksFeed, isOnHooksFeed else { return false }
            return true
        }
    }

    public enum Action {
        case destination(PresentationAction<Destination.Action>)
        case task
        case hooksPlayer(HooksPlayerEvent)
        case loadInitial
        case loadNext
        case reloadFeed(shouldMute: Bool)
        case setCurrentIndex(Int)
        case syncFromCache(hookIds: [String], handles: [String])
        case `internal`(Internal)
        case clipEvents(EventBusClient.ClipEvent)
        case hookEvents(EventBusClient.HookEvent)
        case createHookTapped
        case delegate(Delegate)

        @CasePathable
        @dynamicMemberLookup
        public enum Delegate {
            case goToFullSongTapped(Clip)
            case hideOnboardingAfterHookDeeplink
            case exitContextualFeed
        }

        @CasePathable
        @dynamicMemberLookup
        public enum Internal {
            case syncMetadata(followStatuses: [String: Bool], likeStatuses: [String: Bool], likeCounts: [String: Int], commentCounts: [String: Int], clipLikeStatuses: [String: Bool], clipPlayCounts: [String: Int], dislikeStatuses: [String: Bool], lyrics: [String: LyricsDataV2])
            case showAddToPlaylist(Clip)
            case showOmniPlayer(Clip)
            case showComments(commentId: String?)
            case clearShouldRefreshFlag
            case setShouldRefreshFlag
            case playReloadedHooks([Hook])
            case reloadFeedFailed(error: Error)
            case clearAnimationFlag
        }

        case authorTapped(handle: String)
        case followTapped(handle: String, unfollow: Bool?, hook: Hook)
        case likeTapped(Hook)
        case remixTapped(Hook)
        case shareTapped(Hook)
        case commentsTapped(Hook)
        case commentsDismissed
        case moreTapped(Hook)
        case changePlaylistTapped(Hook)
        case addSongToLikesTapped(Hook, Bool)
        case songTapped(Clip)
        case showReportedHook(String)
        case didDoubleTap(Hook)
        case didSwipeDownOmniPlayerToDismiss
        case teardownOmniPlayer(autoPlayHook: Bool)
        case navigateToProfileFromOmniPlayer(handle: String)
        case omniPlayer(ExpandedPlayerReducer.Action)
        case commentsSheetClient(CommentsClientV2.Event)

        @CasePathable
        @dynamicMemberLookup
        public enum SwipeToShowProfile {
            case start
            case cancel
            case finish
        }

        @CasePathable
        @dynamicMemberLookup
        public enum SwipeToHideProfile {
            case start
            case cancel
            case finish
        }

        case swipeToShowProfile(SwipeToShowProfile)
        case swipeToHideProfile(SwipeToHideProfile)
        case dismissProfile
        case tapToUnmute
        case appDidBackground
        case appDidBecomeInactive
        case appDidForeground

        case didStartScrolling
        case scrollDidBecomeNeutral
    }

    public init() {}

    struct ClipEventCancellableId: Hashable {}
    struct HookEventCancellableId: Hashable {}

    @Dependency(\.hooksPlayerClient) var hooksPlayerClient
    @Dependency(\.hooksPlayerAnalyticsClient) var hooksPlayerAnalyticsClient
    @Dependency(\.commentsClientV2) var commentsSheetClient
    @Dependency(\.apiClientV2) var apiClientV2
    @Dependency(NavigationRouterClient.self) var navigationRouter
    @Dependency(APIClient.self) var apiClient
    @Dependency(\.eventBus.getClipPublisher) private var getClipPublisher
    @Dependency(\.eventBus.getHookPublisher) private var getHookPublisher
    @Dependency(\.eventBus.sendHookEvent) var sendHookEvent
    @Dependency(\.eventBus.getCreateChannel) var getCreateChannel
    @Dependency(SongActionsClient.self) var songActionsClient
    @Dependency(OmniPlayerClient.self) var omniPlayerClient
    @Dependency(\.toastClient.show) var showToast
    @Dependency(\.clipLineageClient.hydrateRelationshipsForClip) var hydrateRelationshipsForClip

    // MARK: - Load Next Helper

    private func loadNextForSource(_ source: HooksFeedSource, _ currentPage: Int = 0) async throws -> [Hook] {
        switch source {
        case .hooksFeed:
            // Use the HooksPlayerClient's built-in loadNext which maintains feed state
            return try await hooksPlayerClient.loadNext()

        case .profile(let handle):
            let includeHooks = true
            let includeScenes = false
            let profile = try await apiClientV2.getProfile(handle, currentPage, .playCount, includeScenes, includeHooks)
            return profile.hooks

        case .myHooks:
            let fetchedHooks = try await apiClientV2.getUserCreatedHooks(currentPage, 20)
            let filteredHooks = fetchedHooks.userDisplayableHooks
            return filteredHooks

        case .clip(clipId: let clipId):
            return try await apiClientV2.getHooksForClip(clipId, currentPage, 20)

        case .notification, .deeplink, .unknown, .profileGrid, .libraryGrid:
            return [] // These can't fetch more anyway
        }
    }

    public var body: some ReducerOf<Self> {
        Reduce<State, Action> { state, action in
            struct HooksPlayerEventCancellableId: Hashable {}
            struct CommentsSheetClientEventCancellableId: Hashable {}

            switch action {
            case .task:
                var effects: [Effect<Action>] = [
                    .stream(hooksPlayerClient.events(), send: Action.hooksPlayer, cancellableId: HooksPlayerEventCancellableId()),
                    .stream(commentsSheetClient.stream(), send: Action.commentsSheetClient, cancellableId: CommentsSheetClientEventCancellableId()),
                    .subscribe(getClipPublisher(), send: Action.clipEvents, cancellableId: ClipEventCancellableId()),
                    .subscribe(getHookPublisher(), send: Action.hookEvents, cancellableId: HookEventCancellableId()),
                ]

                if state.source.shouldPlayInHooksTab {
                    effects.append(.send(.loadInitial))
                } else if let initialHooks = state.initialHooks, !initialHooks.isEmpty {
                    state.hooks = initialHooks.deduplicatedIdentifiedArray()
                    state.currentIndex = state.startIndex

                    if state.startIndex < initialHooks.count {
                        let startingHook = initialHooks[state.startIndex]
                        hooksPlayerClient.playHookInContextualFeed(startingHook, initialHooks, state.startIndex, state.source, state.navigationOptions, state.feedId)
                    }

                    let hooksMetadata = state.hooksMetadata.hydrateIfNeeded(hooks: state.hooks, currentIndex: state.currentIndex, forceSync: true)
                    effects.append(.send(.syncFromCache(hookIds: hooksMetadata.newHookIds, handles: hooksMetadata.newHandles)))

                    // Handle navigation options (like opening comments) when the feed loads
                    if let navigationOptions = state.navigationOptions,
                       navigationOptions.shouldOpenComments,
                       state.currentIndex < state.hooks.count
                    {
                        effects.append(.send(.internal(.showComments(commentId: navigationOptions.replyToCommentID))))
                    }

                    return .merge(effects)
                } else if state.hooks.isEmpty {
                    effects.append(.send(.loadInitial))
                }
                return .merge(effects)

            case .loadInitial:
                state.loadingState = .loading
                let initialHooks = state.initialHooks
                let startMuted = !state.didTapToUnmute
                return .run { [source = state.source, feedId = state.feedId, showOnboarding = state.showOnboarding] send in
                    do {
                        let hooks: [Hook]
                        if source.shouldPlayInHooksTab {
                            let config = HooksFeedConfig(
                                initialHooks: initialHooks,
                                pageSize: 10,
                                playbackConfig: PlaybackConfig(
                                    isShowingOnboarding: showOnboarding,
                                    startMuted: startMuted
                                )
                            )
                            hooks = try await hooksPlayerClient.loadInitial(config, feedId)
                        } else {
                            // Use source-specific loading for contextual feeds
                            hooks = try await loadNextForSource(source, 0)
                        }
                        await send(.hooksPlayer(.feedDidLoadInitial(hooks)))
                    } catch {
                        await send(.hooksPlayer(.feedDidFailToLoad(error)))
                    }
                }

            case .loadNext:
                guard state.shouldLoadNext else { return .none }
                guard state.loadingState != .loading else { return .none }
                state.loadingState = .loading
                state.currentPage += 1
                return .run { [source = state.source, currentPage = state.currentPage] send in
                    do {
                        let hooks = try await loadNextForSource(source, currentPage)
                        await send(.hooksPlayer(.feedDidLoadNext(hooks)))
                    } catch {
                        await send(.hooksPlayer(.feedDidFailToLoad(error)))
                    }
                }

            case .reloadFeed(let shouldMute):
                guard state.loadingState.reloadable else { return .none }
                state.loadingState = .reloading
                state.didTapToUnmute = shouldMute == false
                return .run { send in
                    do {
                        let hooks = try await hooksPlayerClient.reloadFeed()
                        await send(.internal(.playReloadedHooks(hooks)))
                    } catch {
                        await send(.internal(.reloadFeedFailed(error: error)))
                    }
                }

            case .internal(.reloadFeedFailed(let error)):
                state.loadingState = .idle
                let toast = ToastReducer.State.ToastType.warning(
                    L10n.FeatureHooks.somethingWentWrong,
                    .string(L10n.FeatureHooks.pleaseTryAgain),
                    position: .top
                )
                showToast(toast)
                log.telemetry.error(error, message: "Failed to reload feed")
                return .none

            case let .hooksPlayer(event):
                switch event {
                case .feedDidLoadInitial(let hooks):
                    state.loadingState = .loaded
                    let newHooks = hooks.deduplicatedIdentifiedArray()
                    if state.hooks.isEmpty {
                        state.hooks = newHooks
                    } else {
                        let allHooks = state.hooks + newHooks
                        state.hooks = allHooks
                    }
                    let hooksMetadata = state.hooksMetadata.hydrateIfNeeded(hooks: state.hooks, currentIndex: state.currentIndex, forceSync: true)
                    return .send(.syncFromCache(hookIds: hooksMetadata.newHookIds, handles: hooksMetadata.newHandles))

                case .feedDidLoadNext(let hooks):
                    state.loadingState = .loaded
                    state.hooks.append(contentsOf: hooks)
                    let hooksMetadata = state.hooksMetadata.hydrateIfNeeded(hooks: state.hooks, currentIndex: state.currentIndex, forceSync: true)
                    return .send(.syncFromCache(hookIds: hooksMetadata.newHookIds, handles: hooksMetadata.newHandles))

                case .feedDidReloadAfterIndex(let hooks, let afterIndex):
                    state.loadingState = .loaded
                    state.refreshType = .reloadNext
                    var updatedHooks = state.hooks
                    updatedHooks.removeSubrange((afterIndex + 1) ..< updatedHooks.count)
                    // Append while checking for duplicates just to be safe
                    // We already filter out duplicates for existing and incoming hooks in HooksPlayerClient,
                    // but this is the extra check since we're passing [Hook] into IdentifiedArray
                    let existingHookIds = Set(state.hooks.map(\.id))
                    let filteredHooks = hooks.filter { hook in
                        !existingHookIds.contains(hook.id)
                    }
                    updatedHooks.append(contentsOf: filteredHooks)
                    state.hooks = IdentifiedArray(uniqueElements: updatedHooks)

                    let hooksMetadata = state.hooksMetadata.hydrateIfNeeded(hooks: state.hooks, currentIndex: state.currentIndex, forceSync: true)
                    return .send(.syncFromCache(hookIds: hooksMetadata.newHookIds, handles: hooksMetadata.newHandles))

                case .feedDidFailToLoad:
                    state.loadingState = .loaded
                    return .none

                case .refreshFeed(let source, let feedId):
                    // If we're going to Hooks tab, only check source
                    // Otherwise, check both source and feedId
                    switch source {
                    case .hooksFeed:
                        guard state.source == source else {
                            return .none
                        }

                    default:
                        guard state.source == source, state.feedId == feedId else {
                            return .none
                        }
                    }

                    state.refreshType = .restore
                    let hooksMetadata = state.hooksMetadata.hydrateIfNeeded(hooks: state.hooks, currentIndex: state.currentIndex, forceSync: true)
                    return .send(.syncFromCache(hookIds: hooksMetadata.newHookIds, handles: hooksMetadata.newHandles))

                case .playbackStateChanged(let hookId, let playbackState):
                    state.playingHookId = playbackState == .playing ? hookId : nil
                    // Handle .ready state (replaces old playerReady event)
                    guard let hookIndex = state.hooks.firstIndex(where: { $0.id == hookId }) else {
                        return .none
                    }
                    if playbackState == .ready {
                        // All cells receive the "Ready" event
                        state.playerReadyIndex = hookIndex
                    }
                    if hookIndex == state.currentIndex {
                        // Current cell receives all playback state changes
                        state.currentPlaybackState = playbackState
                    }
                    return .none

                case .playbackFailed:
                    // TODO: Handle playback failure
                    return .none

                case .followStatusUpdated(let handle, let isFollowing):
                    state.hooksMetadata.followStatus[handle] = isFollowing
                    return .none

                case .likeStatusUpdated(let hookId, let isLiked):
                    state.hooksMetadata.likeStatus[hookId] = isLiked
                    if isLiked {
                        guard let hook = state.hooks[id: hookId] else {
                            return .none
                        }
                        var updatedHook = hook
                        updatedHook.currentUserLiked = true
                        sendHookEvent(.hookLiked(hook: updatedHook))
                    } else {
                        sendHookEvent(.hookUnliked(hookId: hookId))
                    }
                    return .none

                case .likeCountUpdated(let hookId, let likeCount):
                    state.hooksMetadata.likeCount[hookId] = likeCount
                    return .none

                case .clipLikeStatusUpdated(let hookId, let isLiked):
                    state.hooksMetadata.clipLikeStatus[hookId] = isLiked
                    return .none

                case .hookUpdated:
                    return .none

                case .scrollToIndex(let index):
                    guard index >= 0 && index < state.hooks.count else { return .none }
                    state.shouldAnimateIndexChange = true
                    state.currentIndex = index
                    return .none

                case .playHookInFeed(let hook):
                    // Make sure we're on the main feed and the index is valid
                    guard state.isOnHooksTabFeed,
                          state.currentIndex >= 0,
                          state.currentIndex <= state.hooks.count
                    else {
                        return .none
                    }
                    guard state.currentIndex >= 0 && state.currentIndex <= state.hooks.count else {
                        return .none
                    }

                    var effects: [Effect<Action>] = []

                    // If the user has the app open and hasn't unmuted before deeplinking
                    state.didTapToUnmute = true
                    state.isFocused = true

                    // Handle the case where a user who hasn't opened Hooks has the app open
                    // and clicks on a Hooks deeplink
                    if state.showOnboarding {
                        state.showOnboarding = false
                        effects.append(.send(.delegate(.hideOnboardingAfterHookDeeplink)))
                    }

                    // Check if hook already exists at current index
                    if state.currentIndex < state.hooks.count && state.hooks[state.currentIndex].id == hook.id {
                        // Hook is already at the current position, no need to insert
                        return .none
                    }

                    // Check if hook exists elsewhere and remove it first (TikTok-style behavior)
                    if let existingIndex = state.hooks.firstIndex(where: { $0.id == hook.id }) {
                        state.hooks.remove(at: existingIndex)
                        // Adjust current index if the removed hook was before our insertion point
                        let adjustedIndex = existingIndex < state.currentIndex ? state.currentIndex - 1 : state.currentIndex
                        state.hooks.insert(hook, at: adjustedIndex)
                        // Update currentIndex to point to where the hook was inserted
                        state.currentIndex = adjustedIndex
                    } else {
                        state.hooks.insert(hook, at: state.currentIndex)
                        // currentIndex already points to the correct position
                    }
                    let hooksMetadata = state.hooksMetadata.hydrateIfNeeded(hooks: state.hooks, currentIndex: state.currentIndex)
                    state.refreshType = .reloadCurrentIndex

                    // Sync metadata if needed
                    if !hooksMetadata.newHookIds.isEmpty || !hooksMetadata.newHandles.isEmpty {
                        effects.append(.send(.syncFromCache(hookIds: hooksMetadata.newHookIds, handles: hooksMetadata.newHandles)))
                    }

                    return .merge(effects)
                }

            case let .setCurrentIndex(newIndex):
                guard newIndex >= 0 && newIndex < state.hooks.count else {
                    return .none
                }
                state.currentIndex = newIndex
                let hooksMetadata = state.hooksMetadata.hydrateIfNeeded(hooks: state.hooks, currentIndex: newIndex)

                var effects: [Effect<Action>] = []
                // Only sync if needed
                if !hooksMetadata.newHookIds.isEmpty || !hooksMetadata.newHandles.isEmpty {
                    effects.append(.send(.syncFromCache(hookIds: hooksMetadata.newHookIds, handles: hooksMetadata.newHandles)))
                }
                if state.shouldLoadNext {
                    effects.append(.send(.loadNext))
                }
                return .merge(effects)

            case .syncFromCache(let hookIds, let handles):
                // Early check for empty sync data
                guard !hookIds.isEmpty || !handles.isEmpty else { return .none }

                return .run { send in
                    @Dependency(\.hooksPlayerClient) var client
                    let metadata = await client.syncHooksMetadata(hookIds, handles)
                    await send(.internal(.syncMetadata(
                        followStatuses: metadata.followStatuses,
                        likeStatuses: metadata.likeStatuses,
                        likeCounts: metadata.likeCounts,
                        commentCounts: metadata.commentCounts,
                        clipLikeStatuses: metadata.clipLikeStatuses,
                        clipPlayCounts: metadata.clipPlayCounts,
                        dislikeStatuses: metadata.dislikeStatuses,
                        lyrics: metadata.lyrics
                    )))
                }

            case .authorTapped(let handle):
                let currentHook = state.hooks[state.currentIndex]
                let metadata = HooksRecommendationMetadata(
                    contextType: state.source.analyticsContext.contextType,
                    hookId: currentHook.id,
                    recommendationItemId: currentHook.recommendationItemId
                )
                hooksPlayerClient.pauseCurrentHook(.navigation(.profile))
                navigationRouter.send(route: .profile(handle, recommendationMetadata: metadata, simpleProfile: currentHook.user))
                return .none

            case .followTapped(let handle, let unfollow, let hook):
                let recommendationMetadata = HooksRecommendationMetadata(
                    contextType: state.source.analyticsContext.contextType,
                    hookId: hook.id,
                    recommendationItemId: hook.recommendationItemId
                )
                hooksPlayerClient.toggleFollow(handle, recommendationMetadata)
                return .none

            case .likeTapped(let hook):
                hooksPlayerClient.toggleLike(hook, .single)
                return .none

            case .remixTapped(let hook):
                guard let clip = hook.clip else {
                    // TODO: (JY) Should `log.error` + toast if we continue to pipe Hook here. This pipeline could change.
                    log.warning("[HOOKS] Missing Clip for Hook while trying to show remix actions.")
                    return .none
                }
                getCreateChannel().queue(.showRemixActions(clip: clip, hook: hook))
                return .none

            case .shareTapped(let hook):
                sendHookEvent(.shareHook(hook, source: state.source))
                return .none

            case .commentsTapped(let hook):
                return openCommentsSheet(state: &state, for: hook)

            case .commentsDismissed:
                return dismissCommentsSheet(state: &state)

            case .songTapped(let clip):
                return prepareOmniPlayerForPlayback(state: &state, clip: clip)

            case .showReportedHook(let hookId):
                return .run { _ in
                    @Dependency(\.hookActionsClient) var hookActionsClient
                    hookActionsClient.showReportedHook(hookId)
                }

            case .didDoubleTap(let hook):
                // Only toggle like, don't untoggle
                guard state.hooksMetadata.getIsLiked(for: hook.id) == false else { return .none }
                hooksPlayerClient.toggleLike(hook, .double)
                return .none

            case .didSwipeDownOmniPlayerToDismiss:
                // Only used for analytics
                return .none

            case .teardownOmniPlayer(let autoPlayHook):
                guard state.currentIndex < state.hooks.count else { return .none }
                let hookId = state.hooks[state.currentIndex].id
                state.isShowingOmniPlayer = false
                state.$isHooksOmniPlayerVisible.withLock { $0 = false }
                handlePlaybackAfterClosingOmniPlayer(autoPlayHook: autoPlayHook, hookId: hookId)
                state.expandedState = nil
                return .none

            case .omniPlayer(.delegate(.closeTapped)):
                state.isShowingOmniPlayer = false
                return .none

            case .navigateToProfileFromOmniPlayer(let handle):
                guard state.currentIndex < state.hooks.count else { return .none }
                state.isShowingOmniPlayer = false
                return .run { [sleepTime = state.omniPlayerAnimationDuration] send in
                    // Give enough time for the animation to complete
                    try? await Task.sleep(for: .seconds(sleepTime))
                    await send(.teardownOmniPlayer(autoPlayHook: false))
                    navigationRouter.send(route: .profile(handle))
                }

            case .moreTapped:
                guard state.currentIndex < state.hooks.count else { return .none }
                var currentHook = state.hooks[state.currentIndex]
                if let clip = currentHook.clip {
                    var updatedClip = clip
                    updatedClip.isLiked = state.hooksMetadata.clipLikeStatus[currentHook.id] ?? clip.isLiked
                    currentHook.clip = updatedClip
                }
                // Update hook's disliked status from metadata
                currentHook.isDisliked = state.hooksMetadata.dislikeStatus[currentHook.id] ?? currentHook.isDisliked
                currentHook.allowComments = state.hooksMetadata.allowCommentsStatus[currentHook.id] ?? currentHook.allowComments

                sendHookEvent(.showHooksMoreMenu(currentHook, source: state.source))
                return .none

            case .addSongToLikesTapped(let hook, _):
                guard let currentHook = state.hooks[id: hook.id],
                      let currentClip = currentHook.clip else { return .none }

                // Use the metadata as the source of truth for like status
                let isCurrentlyLiked = state.getClipLikeStatus(for: hook.id) ?? currentClip.isLiked

                guard isCurrentlyLiked == true else {
                    // Use the current clip and explicitly set liked to true
                    songActionsClient.setLiked(clip: currentClip, liked: true, showToast: false, hook: currentHook, source: state.source)
                    return .none
                }
                return .send(.internal(.showAddToPlaylist(currentClip)))

            case .internal(.showAddToPlaylist(let clip)):
                var localClip = clip
                localClip.isLiked = true
                guard state.currentIndex < state.hooks.count else { return .none }
                let currentHook = state.hooks[state.currentIndex]
                state.destination = .addToPlaylist(.init(clip: localClip, hook: currentHook, hooksFeedSource: state.source))
                return .none

            case .internal(.showComments(let commentId)):
                guard state.currentIndex < state.hooks.count else { return .none }
                let currentHook = state.hooks[state.currentIndex]
                if let replyId = commentId {
                    state.navigationOptions = .openComments(.replyToComment(replyId))
                } else {
                    state.navigationOptions = .openComments(nil)
                }
                return openCommentsSheet(state: &state, for: currentHook)

            case .changePlaylistTapped(let hook):
                guard let clip = hook.clip else { return .none }
                return .send(.internal(.showAddToPlaylist(clip)))

            case .internal(.syncMetadata(let followStatuses, let likeStatuses, let likeCounts, let commentCounts, let clipLikeStatuses, let clipPlayCounts, let dislikeStatuses, let lyrics)):
                for (handle, isFollowing) in followStatuses {
                    state.hooksMetadata.followStatus[handle] = isFollowing
                }
                for (hookId, isLiked) in likeStatuses {
                    state.hooksMetadata.likeStatus[hookId] = isLiked
                }
                for (hookId, likeCount) in likeCounts {
                    let oldCount = state.hooksMetadata.likeCount[hookId]
                    state.hooksMetadata.likeCount[hookId] = likeCount
                }
                for (hookId, commentCount) in commentCounts {
                    let oldCount = state.hooksMetadata.commentCount[hookId]
                    state.hooksMetadata.commentCount[hookId] = commentCount
                }
                for (hookId, isLiked) in clipLikeStatuses {
                    state.hooksMetadata.clipLikeStatus[hookId] = isLiked
                }
                for (hookId, playCount) in clipPlayCounts {
                    state.hooksMetadata.clipPlayCount[hookId] = playCount
                }
                for (hookId, isDisliked) in dislikeStatuses {
                    state.hooksMetadata.dislikeStatus[hookId] = isDisliked
                }
                for (hookId, lyricsData) in lyrics {
                    state.hooksMetadata.lyrics[hookId] = lyricsData
                }
                return .none

            case .clipEvents(.toggledLike(let clip)):
                return handleClipLikedFromOutsideHooksFeed(state: &state, clip: clip)

            case .clipEvents(.updateClip(let updatedClip)):
                return handleClipUpdatedFromOutsideHooksFeed(state: &state, clip: updatedClip)

            case .commentsSheetClient(let event):
                return handleCommentsSheetClientEvent(event, state: &state)

            case .internal(.clearShouldRefreshFlag):
                state.refreshType = .none
                return .none

            case .internal(.setShouldRefreshFlag):
                state.refreshType = .initialLoad
                return .none

            case .internal(.clearAnimationFlag):
                state.shouldAnimateIndexChange = false
                return .none

            case .internal(.playReloadedHooks(let hooks)):
                state.hapticProxy += 1
                state.loadingState = .loaded
                state.refreshType = .reloadFeed
                state.currentIndex = 0
                state.hooks = hooks.deduplicatedIdentifiedArray()
                let hooksMetadata = state.hooksMetadata.hydrateIfNeeded(hooks: state.hooks, currentIndex: state.currentIndex, forceSync: true)
                return .send(.syncFromCache(hookIds: hooksMetadata.newHookIds, handles: hooksMetadata.newHandles))

            case .internal(.showOmniPlayer(let clip)):
                return prepareOmniPlayerForPlayback(state: &state, clip: clip)

            case .destination(.presented(.comments(.delegate(.showUserProfile(let handle))))):
                navigationRouter.send(route: .profile(handle))
                state.destination = nil
                return .none

            case .swipeToShowProfile(.start):
                state.isSwipingToShowProfile = true
                guard state.hooks.isEmpty == false else { return .none }
                let currentHook = state.hooks[state.currentIndex]
                let handle = currentHook.user?.handle ?? ""
                let metadata = state.currentHookRecommendationMetadata
                state.destination = .profile(.init(me: state.$me, handle: handle, isTopNavBarVisible: false, recommendationMetadata: metadata, simpleProfile: state.hooks[state.currentIndex].user))
                hooksPlayerClient.pauseCurrentHook(.navigation(.profile))
                return .none

            case .swipeToShowProfile(.cancel):
                state.isSwipingToShowProfile = false
                state.destination = nil
                return .none

            case .swipeToShowProfile(.finish):
                state.isSwipingToShowProfile = false
                state.$isOnHooksFeed.withLock { $0 = false }
                return .none

            case .swipeToHideProfile(.start):
                state.isSwipingToHideProfile = true
                return .none

            case .swipeToHideProfile(.cancel):
                state.isSwipingToHideProfile = false
                return .none

            case .swipeToHideProfile(.finish):
                state.isSwipingToHideProfile = false
                state.$isOnHooksFeed.withLock { $0 = true }
                state.destination = nil
                return .none

            case .dismissProfile:
                state.isSwipingToHideProfile = false
                state.isSwipingToShowProfile = false
                state.$isOnHooksFeed.withLock { $0 = true }
                state.destination = nil
                // The player will handle whether to play based on reported/hidden status
                hooksPlayerClient.playCurrentHook(.resume(.profile))
                return .none

            case .tapToUnmute:
                guard case .hooksFeed = state.source else { return .none }
                state.didTapToUnmute = true
                hooksPlayerClient.setMuted(false)
                return .none

            case .appDidBackground:
                hooksPlayerClient.pauseCurrentHook(.appBackground)
                return .none

            case .appDidBecomeInactive:
                hooksPlayerClient.pauseCurrentHook(.appInactive)
                return .none

            case .appDidForeground:
                // Make sure nothing is being presented on top of the Hooks feed before playing
                guard state.expandedState == nil,
                      state.destination == nil,
                      !state.isHooksOmniPlayerVisible,
                      !state.isHooksCreateVisible
                else {
                    return .none
                }
                hooksPlayerClient.playCurrentHook(.appForeground)
                return .none

            case .createHookTapped:
                hooksPlayerClient.pauseCurrentHook(.navigation(.createHook))
                sendHookEvent(.showCreateHook())
                return .none

            case .omniPlayer(.delegate(.navigateToProfile(let handle))):
                return .send(.navigateToProfileFromOmniPlayer(handle: handle))

            case .omniPlayer(.delegate(.nextTapped)):
                omniPlayerClient.playNextClip()
                return .none

            case .omniPlayer(.delegate(.prevTapped)):
                omniPlayerClient.playPreviousClip()
                return .none

            case .delegate(.goToFullSongTapped(let clip)):
                return prepareOmniPlayerForPlayback(state: &state, clip: clip)

            case .hookEvents(.hookReported(let hookId)):
                // Mark the hook as reported in metadata
                state.hooksMetadata.reportedStatus[hookId] = true
                // Update reported hooks in the player
                return .run { [reportedStatus = state.hooksMetadata.reportedStatus] _ in
                    await hooksPlayerClient.updateReportedHooks(reportedStatus)
                    // The player will automatically handle play/pause based on the updated status
                }

            case .hookEvents(.showReportedHook(let hookId)):
                // Remove the hook from reported status to enable playback
                state.hooksMetadata.reportedStatus[hookId] = false
                // Update reported hooks in the player
                return .run { [reportedStatus = state.hooksMetadata.reportedStatus] _ in
                    await hooksPlayerClient.updateReportedHooks(reportedStatus)
                    // The player will automatically handle play/pause based on the updated status
                }

            case .hookEvents(.creatorHidden(let hookId, let handle)):
                // Mark the creator as hidden
                state.hooksMetadata.hiddenCreatorHandles[handle] = true
                state.loadingState = .loading
                // Update hidden creator handles in the player
                return .run { [hiddenCreatorHandles = state.hooksMetadata.hiddenCreatorHandles, source = state.source] _ in
                    await hooksPlayerClient.updateHiddenCreatorHandles(hiddenCreatorHandles)
                }

            case .hookEvents(.hookDisliked(let hookId)):
                state.hooksMetadata.dislikeStatus[hookId] = true
                return .none

            case .hookEvents(.hookUndisliked(let hookId)):
                state.hooksMetadata.dislikeStatus[hookId] = false
                return .none

            case .hookEvents(.hookDeleted(let hookId)):
                let deletedIndex = state.hooks.firstIndex(where: { $0.id == hookId })
                let deletedHook = deletedIndex.map { state.hooks[$0] }
                let creatorHandle = deletedHook?.user?.handle
                let oldCurrentIndex = state.currentIndex

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

                state.hooksMetadata.likeStatus.removeValue(forKey: hookId)
                state.hooksMetadata.likeCount.removeValue(forKey: hookId)
                state.hooksMetadata.commentCount.removeValue(forKey: hookId)
                state.hooksMetadata.clipLikeStatus.removeValue(forKey: hookId)
                state.hooksMetadata.clipPlayCount.removeValue(forKey: hookId)
                state.hooksMetadata.dislikeStatus.removeValue(forKey: hookId)
                state.hooksMetadata.reportedStatus.removeValue(forKey: hookId)
                state.hooksMetadata.lyrics.removeValue(forKey: hookId)
                state.hooksMetadata.allowCommentsStatus.removeValue(forKey: hookId)

                let hasOtherHooks = creatorHandle.map { handle in
                    state.hooks.contains { $0.user?.handle == handle }
                } ?? false

                if let handle = creatorHandle, !hasOtherHooks {
                    state.hooksMetadata.followStatus.removeValue(forKey: handle)
                }

                state.hooksMetadata.trackedHookIds.remove(hookId)
                if let handle = creatorHandle, !hasOtherHooks {
                    state.hooksMetadata.trackedHandles.remove(handle)
                }

                // Adjust currentIndex based on where the deleted hook was
                if let deletedIndex = deletedIndex {
                    if deletedIndex < oldCurrentIndex {
                        // Deleted hook was before current - decrement to stay on same hook
                        state.currentIndex = max(0, oldCurrentIndex - 1)
                    }
                    // For deletedIndex == oldCurrentIndex or > oldCurrentIndex, no change needed

                    if state.hooks.isEmpty {
                        state.currentIndex = 0
                    } else if state.currentIndex >= state.hooks.count {
                        state.currentIndex = max(0, state.hooks.count - 1)
                    }
                }

                // Clean up player assignments and caches
                hooksPlayerClient.removeHook(hookId, creatorHandle)

                if state.source != .hooksFeed, state.hooks.isEmpty {
                    return .send(.delegate(.exitContextualFeed))
                }
                return .none

            case .hookEvents(.hookCommentsToggled(let hookId, let canComment)):
                state.hooksMetadata.allowCommentsStatus[hookId] = canComment
                return .none

            case .didStartScrolling:
                state.hideTopNavBar = true
                return .none

            case .scrollDidBecomeNeutral:
                state.hideTopNavBar = false
                return .none

            case .destination,
                 .clipEvents,
                 .omniPlayer,
                 .hookEvents,
                 .delegate:
                return .none
            }
        }
        .ifLet(\.$destination, action: \.destination)
        .ifLet(\.expandedState, action: \.omniPlayer) {
            ExpandedPlayerReducer()
        }
        Analytics()
    }
}
