import AnalyticsClient
import APIClient
import ComponentLibrary
import ComposableArchitecture
import DeeplinkIntents
import EventBusClient
import FeatureBanner
import FeatureCatalog
import FeatureDiscover
import FeatureHooksFeed
import FeatureHooksGrid
import FeatureOmniPlayer
import FeaturePlayer
import FeatureProfile
import FeatureSocial
import FeatureToasts
import HooksPlayerClient
import InAppNotificationClient
import Localization
import NavigationRouterClient
import OmniPlayerClient
import StatsigClient
import SunoModelClient
import SwiftUI
import TabBarUtilities
import Utilities

// swiftlint:disable file_length

@Reducer
public struct RootTabCoordinator {
    @ObservableState
    public struct State: Equatable {
        @ObservationStateIgnored @ObservedBox var discover: Discover.State
        @ObservationStateIgnored @ObservedBox var library: Library.State
        @ObservationStateIgnored @ObservedBox var notifications: Notifications.State
        @ObservationStateIgnored @ObservedBox var profile: PublicProfileV1.State
        @ObservationStateIgnored @ObservedBox var bannerState: BannerReducer.State
        @ObservationStateIgnored @ObservedBox var discoverStack: NavigationStackCoordinator.State
        @ObservationStateIgnored @ObservedBox var libraryStack: NavigationStackCoordinator.State
        @ObservationStateIgnored @ObservedBox var notificationsStack: NavigationStackCoordinator.State
        @ObservationStateIgnored @ObservedBox var profileStack: NavigationStackCoordinator.State
        @ObservationStateIgnored @ObservedBox var hooksStack: NavigationStackCoordinator.State
        @ObservationStateIgnored @ObservedBox var hooks: HooksFeedScreenReducer.State?
        @Shared(.inMemory(.selectedSunoModel)) var selectedSunoModel: SunoModelMetaData = .modelDefault
        @Shared(.inMemory(.hasUnreadNotifications)) var hasUnreadNotifications: Bool = false
        @Shared(.inMemory(.selectedTab)) var selectedTab: TabBarTab = .defaultSelection
        @Shared(.inMemory(.readyNewGensCount)) var readyNewGensCount: Int = 0
        @Shared(.inMemory(.isHooksFeedFocused)) var isHooksFeedFocused: Bool = false

        // This gets set in the reducer's initializer and is used to determine tab order
        public let orderedTabs: [TabBarTab]

        public var currentScreen: NavigationStackCoordinator.State.Screen.State? {
            switch selectedTab {
            case .discover:
                return discoverStack.path.last
            case .library:
                return libraryStack.path.last
            case .notifications:
                return notificationsStack.path.last
            case .profile:
                return profileStack.path.last
            case .hooks:
                guard FeatureFlag.hooks.isFeedEnabled else { return nil }
                return hooksStack.path.last
            }
        }

        public var isHooksContextualFeedActive: Bool {
            if case .hooksContextualFeed = currentScreen {
                return true
            }
            return false
        }

        private var isOnHooksTabRoot: Bool {
            return selectedTab == .hooks && hooksStack.path.ids.isEmpty
        }

        public var shouldShowMaskedTabBar: Bool {
            // We show this when playing Hooks or when on the Hooks tab with the carousel
            // when the feed isn't focused/active.
            // This makes sure we're not showing the compact or expanded player as well.
            guard let hooks, isOnHooksFeed, !isHooksOmniPlayerVisible else { return false }

            return !isOnHooksTabRoot || hooks.didPlayHooksInHooksTabSession || !isCompactPlayerVisible || isHooksFeedFocused
        }

        public var hideOmniPlayerOverHooks: Bool {
            guard let hooks, isOnHooksFeed else { return false }

            return !isOnHooksTabRoot || hooks.didPlayHooksInHooksTabSession || isHooksFeedFocused || isHooksOmniPlayerVisible
        }

        public var isOnLibraryScreen: Bool {
            return selectedTab == .library && libraryStack.path.isEmpty
        }

        public var showNewGensBadge: Bool {
            readyNewGensCount > 0 && !isOnLibraryScreen
        }

        @Shared var me: Me
        @Shared(.inMemory(.isOnHooksFeed)) var isOnHooksFeed: Bool = false
        @Shared(.inMemory(.isHooksOmniPlayerVisible)) var isHooksOmniPlayerVisible: Bool = false
        @Shared(.inMemory(.isCompactPlayerVisible)) var isCompactPlayerVisible: Bool = false
        @Shared(.appStorage(.hasSeenFirstTimeTabSelection)) var hasSeenFirstTimeTabSelection: Bool = false

        public init(me: Shared<Me>, launchDeeplinkIntent: DeeplinkIntent?) {
            self._me = me
            self.discover = .init(me: me)
            self.library = .init(me: me)
            self.notifications = .init(me: me)
            self.profile = .init(me: me, handle: nil, isRootView: true) // passing `handle: nil` defaults to using `me.user.handle`
            self.discoverStack = .init()
            self.libraryStack = .init()
            self.notificationsStack = .init()
            self.profileStack = .init()
            self.bannerState = .init()
            self.hooksStack = .init()

            // Since this is the first time we check for this `main flag` (that's really driven by a helper inside `ios-hooks`)
            // we mark our experiment exposures directly
            ParameterStores.Hooks.markHooksExperimentExposure()

            if FeatureFlag.hooks.showOnFirstTab {
                self.orderedTabs = [.hooks, .discover, .library, .profile]
            } else if FeatureFlag.hooks.showOnSecondTab {
                self.orderedTabs = [.discover, .hooks, .library, .profile]
            } else {
                self.orderedTabs = [.discover, .library, .notifications, .profile]
            }
            
            // Observed box optimization flag
            ParameterStores.General.markExposed(flag: \.$viewUpdateOptimization)
            ObservedBoxConfig.overrideObserveChanges = {
                return !FeatureFlag.general.viewUpdateOptimization
            }

            if FeatureFlag.hooks.isFeedEnabled {
                @Shared(.appStorage(.hasSeenHooksOnboarding)) var hasSeenHooksOnboarding: Bool = false
                let shouldSkipOnboarding = launchDeeplinkIntent != nil && FeatureFlag.hooks.hooksDeeplinkingEnabled

                self.hooks = .init(
                    me: me,
                    showCarousel: FeatureFlag.hooks.showCarousel,
                    skipOnboarding: shouldSkipOnboarding,
                    hasSeenHooksOnboarding: hasSeenHooksOnboarding,
                    launchDeeplinkIntent: launchDeeplinkIntent
                )

                let shouldShowFirstTimeExperience = FeatureFlag.hooks.showOnFirstTab && !self.hasSeenFirstTimeTabSelection
                let initialSelectedTab: TabBarTab

                if shouldShowFirstTimeExperience {
                    // First-time users start on discover tab
                    initialSelectedTab = .discover
                    self.$hasSeenFirstTimeTabSelection.withLock { $0 = true }
                } else {
                    // Use feature flag to determine starting tab
                    initialSelectedTab = FeatureFlag.hooks.showOnFirstTab ? .hooks : .discover
                }
                self.$selectedTab.withLock { $0 = initialSelectedTab }
                self.$isOnHooksFeed.withLock { $0 = initialSelectedTab == .hooks }

                if !FeatureFlag.hooks.showCarousel {
                    // If we're not in the Carousel treatment, and only show
                    // the feed on Hooks tab, set this flag on launch
                    self.$isHooksFeedFocused.withLock { $0 = true }
                }
            }
        }
    }

    public enum Action: BindableAction {
        public enum Delegate {
            case triggerPushNotificationRequest
            case showGeneratedClips
            case navigateToExplore
            case navigateToHooksTab
            case showHooksCreate
            case resumeHookIfNeeded(HookNavigationCause)
            case hooksOnboardingDismissed
        }

        public enum Internal {
            case pollRemoteNotifications
            case getRemoteNotifications
            case setTabHasUnseenNotifications(Bool)
            case notificationsResponse(Result<UserNotification, Error>)
        }

        case onAppear
        case task
        case navigationRouter(NavigationRouterClient.Route)
        case createTapped
        case meUpdated(Me)
        case tabSelected(TabBarTab)
        case popToRoot(tab: TabBarTab)
        case scrollToTop(tab: TabBarTab)
        case longPressAction(tab: TabBarTab)
        case bannerAction(BannerReducer.Action)
        case hooks(HooksFeedScreenReducer.Action)
        case discover(Discover.Action)
        case library(Library.Action)
        case notifications(Notifications.Action)
        case profile(PublicProfileV1.Action)
        case discoverStack(NavigationStackCoordinator.Action)
        case libraryStack(NavigationStackCoordinator.Action)
        case notificationsStack(NavigationStackCoordinator.Action)
        case profileStack(NavigationStackCoordinator.Action)
        case hooksStack(NavigationStackCoordinator.Action)
        case binding(BindingAction<State>)
        case `internal`(Internal)
        case delegate(Delegate)
    }

    @Dependency(APIClient.self) var apiClient
    @Dependency(\.continuousClock) var clock
    @Dependency(NavigationRouterClient.self) var navigationRouter
    @Dependency(\.eventBus.getOmniplayerChannel) var getOmniplayerChannel
    @Dependency(\.eventBus.getCreateChannel) var getCreateChannel
    @Dependency(\.inAppNotificationClient) var inAppNotificationClient
    @Dependency(\.omniplayerClient.pauseCurrentClip) var pauseCurrentClip
    @Dependency(\.omniplayerClient.playCurrentClip) var playCurrentClip
    @Dependency(\.hooksPlayerClient.pauseCurrentHook) var pauseCurrentHook
    @Dependency(\.hooksPlayerClient.playCurrentHook) var playCurrentHook
    @Dependency(\.toastClient.show) var showToast

    private var discoverStackCoordinator: some ReducerOf<Self> {
        Scope(state: \.discoverStack, action: \.discoverStack) {
            NavigationStackCoordinator()
        }
    }

    private var catalogStackCoordinator: some ReducerOf<Self> {
        Scope(state: \.libraryStack, action: \.libraryStack) {
            NavigationStackCoordinator()
        }
    }

    private var notificationsStackCoordinator: some ReducerOf<Self> {
        Scope(state: \.notificationsStack, action: \.notificationsStack) {
            NavigationStackCoordinator()
        }
    }

    private var profileStackCoordinator: some ReducerOf<Self> {
        Scope(state: \.profileStack, action: \.profileStack) {
            NavigationStackCoordinator()
        }
    }

    private var hooksStackCoordinator: some ReducerOf<Self> {
        Scope(state: \.hooksStack, action: \.hooksStack) {
            NavigationStackCoordinator()
        }
    }

    public var body: some ReducerOf<Self> {
        Scope(state: \.discover, action: \.discover) {
            Discover()
        }
        Scope(state: \.library, action: \.library) {
            Library()
        }
        Scope(state: \.notifications, action: \.notifications) {
            Notifications()
        }
        Scope(state: \.profile, action: \.profile) {
            PublicProfileV1()
        }
        Scope(state: \.bannerState, action: \.bannerAction) {
            BannerReducer()
        }
        discoverStackCoordinator
        catalogStackCoordinator
        notificationsStackCoordinator
        profileStackCoordinator
        hooksStackCoordinator
        BindingReducer()
        Reduce<State, Action> { state, action in
            struct CheckNotificationsCancellableId: Hashable {}
            struct NavigationRouterChannelCancellableId: Hashable {}
            switch action {
            case .onAppear:
                return .merge(
                    .send(.discover(.task)),
                    .send(.internal(.pollRemoteNotifications)),
                    .publisher { state.$me.publisher.map(Action.meUpdated) }
                )

            case .task:
                return .channel(navigationRouter.getChannel(), send: Action.navigationRouter, cancellableId: NavigationRouterChannelCancellableId())

            case .navigationRouter(let route):
                return self.handleRoute(state: &state, route: route)

            case .createTapped:
                getCreateChannel().queue(.createClip())
                return .none

            case .notifications(.delegate(.triggerPushNotificationRequest)):
                return .send(.delegate(.triggerPushNotificationRequest))

            case .internal(.pollRemoteNotifications):
                return .run { send in
                    // Initial send on launch
                    await send(.internal(.getRemoteNotifications))
                    await withTaskCancellation(id: CheckNotificationsCancellableId(), cancelInFlight: true) {
                        for await _ in clock.timer(interval: .seconds(60)) {
                            await send(.internal(.getRemoteNotifications))
                        }
                    }
                }

            case .internal(.getRemoteNotifications):
                inAppNotificationClient.enqueueGetNotifications(before: nil, shouldRefreshInMemoryMap: false)
                return .none

            case .meUpdated:
                // TODO: delete this action?
                return .none

            case .popToRoot(let tab):
                // Make sure we're on the correct tab first
                guard state.selectedTab == tab else { return .none }

                return self.popToRoot(state: &state)

            case .scrollToTop(let tab):
                // Make sure we're on the correct tab first
                guard state.selectedTab == tab else { return .none }

                switch tab {
                case .discover:
                    return .send(.discover(.scrollToTop))

                case .library:
                    return .send(.library(.scrollToTop))

                case .notifications:
                    return .send(.notifications(.scrollToTop))

                case .profile:
                    // Check if I'm looking at my own profile
                    guard state.me.user.handle == state.profile.handle else { return .none }
                    return .send(.profile(.scrollToTop))

                case .hooks:
                    // Use the same behavior as popToRoot for Hooks,
                    // which refreshes the feed and shows the carousel when necessary
                    return self.popToRoot(state: &state)
                }

            case .longPressAction(let tab):
                // Only switch tabs if needed
                let didSwitchTabs = state.selectedTab != tab
                if didSwitchTabs {
                    let previousTab = state.selectedTab
                    state.$selectedTab.withLock { $0 = tab }

                    // If navigating to hooks screen, pause current clip
                    if tab == .hooks {
                        pauseCurrentClip()
                    }
                }

                switch tab {
                case .discover:
                    // Navigate to Search if we're not already there
                    if case .search = state.currentScreen { return .none }
                    navigationRouter.send(route: .search(.publicSong))
                    return .none

                case .library:
                    // Navigate to Search if we're not already there
                    if case .search = state.currentScreen { return .none }
                    navigationRouter.send(route: .search(.librarySong))
                    return .none

                case .notifications:
                    // Navigate to Creators to Follow if we're not already there
                    if case .creatorsToFollow = state.currentScreen { return .none }
                    navigationRouter.send(route: .creatorsToFollow)
                    return .none

                case .profile:
                    // Navigate to Settings if we're not already there
                    if case .settings = state.currentScreen { return .none }
                    navigationRouter.send(route: .settings)
                    return .none

                case .hooks:
                    return .send(.hooks(.createHookFromLongPress))
                }

            case .bannerAction(.dismiss):
                if let destination = state.bannerState.banner?.destination, case .newClipsInOmniPlayer = destination {
                    state.bannerState.banner = nil
                }
                return .none

            case .bannerAction(.tapped):
                guard let destination = state.bannerState.banner?.destination else { return .none }
                switch destination {
                case .appStore:
                    return .none

                case .newClipsInOmniPlayer:
                    // Navigate to Library tab first if on Hooks tab
                    if state.selectedTab == .hooks {
                        _ = self.setTab(state: &state, tab: .library, popToRoot: true)
                    }
                    // Reset the new clips count
                    state.$readyNewGensCount.withLock { $0 = 0 }
                    return .send(.delegate(.showGeneratedClips))

                case .shareAsset(let assetTarget, let localAssetURL, let attributionURL):
                    return .run { @MainActor [assetTarget, localAssetURL, attributionURL] send in
                        @Dependency(\.shareAssetClient) var shareAssetClient
                        switch assetTarget {
                        case .downloadVideo:
                            await shareAssetClient.saveToDownloadPhotos(localAssetURL)
                            send(.bannerAction(.dismiss), animation: .default)
                            showToast(.success(L10n.FeatureShareAssets.savedShareableToPhotos))

                        case .instagramStories, .facebookStories, .tiktokVideo:
                            shareAssetClient.shareToDestination(
                                shareURL: attributionURL,
                                videoURL: localAssetURL,
                                pngStickerData: .init(),
                                linkMessage: L10n.FeatureShare.shareSongSubject,
                                shareDestination: assetTarget
                            )
                            send(.bannerAction(.dismiss), animation: .default)
                        }
                    }

                case .hookDownload(let hook, let localVideoURL):
                    showToast(.success(L10n.FeatureHooks.hookDownloadComplete))
                    return .send(.bannerAction(.dismiss), animation: .default)
                }

            case .delegate(.navigateToExplore):
                return self.setTab(state: &state, tab: .discover, popToRoot: true)

            case .delegate(.resumeHookIfNeeded(let cause)):
                if state.selectedTab == .hooks, state.hooksStack.path.ids.isEmpty {
                    state.$isOnHooksFeed.withLock { $0 = true }
                    playCurrentHook(.resume(cause))
                }
                return .none

            case .delegate(.navigateToHooksTab):
                // Navigate to the Hooks tab without automatically resuming playback since
                // that's handled by the `playHookInFeed` method in `HooksPlayerClient`
                return self.setTab(state: &state, tab: .hooks, popToRoot: true, resumePlayback: false)

            case .tabSelected(let tab):
                let hasNewGensToPlay = state.readyNewGensCount > 0
                if state.isOnLibraryScreen, hasNewGensToPlay {
                    state.$readyNewGensCount.withLock { $0 = 0 }
                }
                return self.setTab(state: &state, tab: tab, popToRoot: false)

            case .delegate(.hooksOnboardingDismissed):
                // When onboarding is dismissed, enable hooks feed playback if we're on the hooks tab
                if state.selectedTab == .hooks && state.hooksStack.path.ids.isEmpty {
                    state.$isOnHooksFeed.withLock { $0 = true }
                    playCurrentHook(.resume(.onboarding))
                }
                return .none

            case .hooks(.dismissHooksOnboarding):
                return .send(.delegate(.hooksOnboardingDismissed))

            case .binding, .discover, .hooks, .library, .internal, .notifications, .profile, .discoverStack, .libraryStack, .notificationsStack, .profileStack, .hooksStack, .delegate, .bannerAction:
                return .none
            }
        }
        .ifLet(\.hooks, action: \.hooks) {
            HooksFeedScreenReducer()
        }
        Analytics()
    }

    private func reduceIntoCurrentNavigationStack(state: inout State, with action: NavigationStackCoordinator.Action) -> Effect<Action> {
        let coordinator: any ReducerOf<RootTabCoordinator>
        let coordinatorAction: RootTabCoordinator.Action
        switch state.selectedTab {
        case .discover:
            coordinator = discoverStackCoordinator
            coordinatorAction = Action.discoverStack(action)

        case .library:
            coordinator = catalogStackCoordinator
            coordinatorAction = Action.libraryStack(action)

        case .notifications:
            coordinator = notificationsStackCoordinator
            coordinatorAction = Action.notificationsStack(action)

        case .profile:
            coordinator = profileStackCoordinator
            coordinatorAction = Action.profileStack(action)

        case .hooks:
            guard FeatureFlag.hooks.isFeedEnabled else { return .none }
            coordinator = hooksStackCoordinator
            coordinatorAction = Action.hooksStack(action)
        }
        return coordinator.reduce(into: &state, action: coordinatorAction)
    }

    private func setTab(
        state: inout State,
        tab: TabBarTab,
        popToRoot: Bool = false,
        resumePlayback: Bool = true // Resume Hook or OmniPlayer playback
    ) -> Effect<Action> {
        let previousTab = state.selectedTab

        state.$selectedTab.withLock { $0 = tab }

        // If navigating to hooks screen, pause current clip
        if tab == .hooks {
            // Only reset the `didPlayHooksInHooksTabSession` toggle
            // if we're switching from another tab to Hooks and the feed isn't focused.
            // If we're going back directly into a Hooks feed, we don't want to show the OmniPlayer again.
            let switchingToHooksFeedFromAnotherTab = previousTab != .hooks && !state.isHooksFeedFocused
            if switchingToHooksFeedFromAnotherTab {
                state.hooks?.didPlayHooksInHooksTabSession = false
            }

            // Pause music if we're switching directly into a focused Hooks feed
            if state.isHooksFeedFocused {
                pauseCurrentClip()
            }

            // Handle second tab experiment: auto-unmute when tab becomes active
            if FeatureFlag.hooks.showOnSecondTab {
                guard let hooksState = state.hooks, !hooksState.showHooksOnboarding else {
                    // Hooks not initialized or onboarding active - use normal flow
                    guard !state.isHooksContextualFeedActive, resumePlayback else {
                        updateHooksFeedState(state: &state)
                        return .none
                    }
                    updateHooksFeedState(state: &state)
                    playCurrentHook(.resume(.tab))
                    return .none
                }

                // Second tab + no onboarding: unmute and resume
                updateHooksFeedState(state: &state)
                return .concatenate([
                    .send(.hooks(.setMuted(false))),
                    .send(.hooks(.hooksFeed(.tapToUnmute))),
                    .run { _ in playCurrentHook(.resume(.tab)) },
                ])
            }

            // Only resume hooks playback if we're not about to enter a contextual feed
            guard !state.isHooksContextualFeedActive, resumePlayback else {
                updateHooksFeedState(state: &state)
                return .none
            }
            playCurrentHook(.resume(.tab))
        }

        // If leaving hooks screen, resume playback but check the override parameter `resumePlayback`
        if previousTab == .hooks && tab != .hooks {
            // Only send pause analytics if hooks were actually playing (not during onboarding)
            if let hooksState = state.hooks, !hooksState.showHooksOnboarding {
                pauseCurrentHook(.navigation(.tab))
            }
            guard resumePlayback else { return .none }
            playCurrentClip()
        }

        updateHooksFeedState(state: &state)

        return popToRoot ? self.reduceIntoCurrentNavigationStack(state: &state, with: .popToRoot) : .none
    }

    private func updateHooksFeedState(state: inout State) {
        let shouldBeOnHooksFeed = (state.selectedTab == .hooks && state.hooksStack.path.ids.isEmpty) ||
            state.isHooksContextualFeedActive
        state.$isOnHooksFeed.withLock { $0 = shouldBeOnHooksFeed }
    }

    private func popToRoot(state: inout State) -> Effect<Action> {
        // If we're on the main Hooks feed (tab), reload the feed
        // when the user taps the Hooks tab icon once
        if state.selectedTab == .hooks, state.hooksStack.path.ids.isEmpty {
            guard let hooks = state.hooks else { return .none }
            var effects: [Effect<Action>] = []
            if FeatureFlag.hooks.showCarousel, hooks.isHooksFeedFocused {
                // If the current user is in the Carousel treatment,
                // minimize the feed/show the carousel as well
                effects.append(.send(.hooks(.setFeedInFocus(false))))
                effects.append(.send(.hooks(.setMuted(true))))
            }
            effects.append(.send(.hooks(.refreshFeedTapped)))
            return .merge(effects)
        }

        let effect = self.reduceIntoCurrentNavigationStack(state: &state, with: .popToRoot)

        if state.selectedTab == .hooks {
            let shouldBeOnHooksFeed = (state.selectedTab == .hooks && state.hooksStack.path.ids.isEmpty) || state.isHooksContextualFeedActive
            state.$isOnHooksFeed.withLock { $0 = shouldBeOnHooksFeed }
        }

        return effect
    }

    private func pushScreen(state: inout State, screen: NavigationStackCoordinator.State.Screen.State) -> Effect<Action> {
        return self.reduceIntoCurrentNavigationStack(state: &state, with: .push(screen: screen))
    }

    // MARK: Navigation Routing

    // swiftlint:disable:next cyclomatic_complexity
    private func handleRoute(state: inout State, route: NavigationRouterClient.Route) -> Effect<Action> {
        var effects: [Effect<Action>] = []
        switch route {
        case .explore:
            effects.append(self.setTab(state: &state, tab: .discover, popToRoot: true))

        case .hooksFeed:
            effects.append(self.setTab(state: &state, tab: .hooks, popToRoot: true))

        case .myHooks(let config, let likedHooksInitialState):
            effects.append(self.pushScreen(state: &state, screen: .myHooks(.init(
                showBackButton: config.showBackButton,
                hooksGrid: .init(
                    dependency: config
                ),
                likedHooksInitialState: likedHooksInitialState
            ))))

        case .userHooks(let config):
            guard case .profileGrid(let handle) = config.source else { return .none }
            effects.append(self.pushScreen(state: &state, screen: .userHooks(.init(
                showBackButton: config.showBackButton,
                userHandle: handle,
                hooksGrid: .init(
                    dependency: config
                )
            ))))

        case .clipHooks(let config):
            guard case .clip(let clipId) = config.source else { return .none }
            effects.append(self.pushScreen(state: &state, screen: .clipHooks(.init(
                showBackButton: config.showBackButton,
                clipId: clipId,
                hooksGrid: .init(
                    dependency: config
                )
            ))))

        case .hooksContextualFeed(let hooks, let startIndex, let navigationOptions, let source):
            let hook = hooks.indices.contains(startIndex) ? hooks[startIndex] : nil
            // Pause the song first before moving to a Contextual Feed
            pauseCurrentClip()
            effects.append(self.pushScreen(state: &state, screen: .hooksContextualFeed(.init(me: state.$me, hook: hook, startIndex: startIndex, hooks: hooks, navigationOptions: navigationOptions, source: source))))

        case .library(let tooltipToShow, let showNewClips):
            effects.append(self.setTab(state: &state, tab: .library, popToRoot: true))
            if let tooltipToShow {
                effects.append(.send(.library(.showTooltip(tooltipToShow))))
            } else if showNewClips {
                effects.append(.send(.library(.showNewClips)))
            }

        case .profile(let handle, let displayName, let avatarImageUrl, let recommendationMetadata, let simpleProfile):
            // If the profile is already presented in the current tab, no need to present it again
            // Difficult to turn this into a `guard`
            if case .profile(let publicProfileV1) = state.currentScreen,
               handle == publicProfileV1.handle
            {
                break
            }
            // If the profile is the user's own profile, navigate to the profile tab
            if handle == state.me.user.handle {
                effects.append(self.setTab(state: &state, tab: .profile, popToRoot: true))
            } else {
                effects
                    .append(
                        self.pushScreen(
                            state: &state,
                            screen:
                            .profile(
                                .init(me: state.$me, handle: handle, displayName: displayName, avatarImageUrl: avatarImageUrl, simpleProfile: simpleProfile)
                            )
                        )
                    )
            }

        case .playlist(let playlist):
            if case .playlistDetail(let playlistDetailV1) = state.currentScreen,
               playlist.id == playlistDetailV1.playlist?.id
            {
                break
            }
            effects.append(self.pushScreen(state: &state, screen: .playlistDetail(.init(me: state.$me, source: .playlist(playlist)))))

        case .playlistWithId(let playlistId, let title, let imageUrl):
            if case .playlistDetail(let playlistDetailV1) = state.currentScreen, playlistId == playlistDetailV1.playlist?.id {
                break
            }
            effects.append(self.pushScreen(state: &state, screen: .playlistDetail(.init(me: state.$me, source: .playlistId(playlistId, title: title, imageUrl: imageUrl)))))

        case .notifications:
            if FeatureFlag.hooks.isFeedEnabled {
                // When hooks tab bar is enabled, push notifications as a screen instead of switching tabs
                effects.append(self.pushScreen(state: &state, screen: .notifications(.init(me: state.$me))))
            } else {
                effects.append(self.setTab(state: &state, tab: .notifications, popToRoot: true))
            }

        case .playlistSection(let playlistSection):
            effects.append(self.pushScreen(state: &state, screen: .playlistDetail(.init(me: state.$me, source: .section(playlistSection)))))

        case .trendingPlaylistSection(let playlistSection):
            effects.append(self.pushScreen(state: &state, screen: .trendingPlaylistSection(.init(section: playlistSection, me: state.$me, showBackButton: false))))

        case .playlistListSection(let playlistListSection):
            effects.append(self.pushScreen(state: &state, screen: .playlistListSection(.init(section: playlistListSection, me: state.$me))))

        case .genreDetailSection(let styleItem):
            let genre = Genre(id: styleItem.id, name: styleItem.name, image: styleItem.imageUrl)
            effects.append(self.pushScreen(state: &state, screen: .playlistDetail(.init(me: state.$me, source: .genre(genre)))))

        case .listenHistory:
            let listenHistorySection = PlaylistSection(
                id: PlaylistSection.Constants.continueListeningSectionId,
                title: L10n.FeatureDiscover.listenHistoryTitle,
                items: [],
                previewItemsCount: 0
            )
            effects.append(self.pushScreen(state: &state, screen: .playlistDetail(.init(me: state.$me, source: .section(listenHistorySection)))))

        case .likedSongs:
            effects.append(self.pushScreen(state: &state, screen: .likedSongs(.init(me: state.$me, showBackButton: false))))

        case .likedPlaylists:
            effects.append(self.pushScreen(state: &state, screen: .likedPlaylists(.init(me: state.$me, showBackButton: false))))

        case .playlists:
            effects.append(self.pushScreen(state: &state, screen: .playlists(.init(me: state.$me, showBackButton: false))))

        case .followingPlaylist:
            let followingSection = PlaylistSection(
                id: PlaylistSection.Constants.followingFeedSectionId,
                title: L10n.FeatureDiscover.following,
                items: [],
                previewItemsCount: 0
            )
            effects.append(self.pushScreen(state: &state, screen: .playlistDetail(.init(me: state.$me, source: .section(followingSection)))))

        case .creatorsToFollow:
            effects.append(self.pushScreen(state: &state, screen: .creatorsToFollow(.init(me: state.$me, showBackButton: false))))

        case .followers(let handle):
            effects.append(self.pushScreen(state: &state, screen: .followers(.init(handle: handle, type: .followers))))

        case .following(let handle):
            effects.append(self.pushScreen(state: &state, screen: .following(.init(handle: handle, type: .following))))

        case .settings:
            effects.append(self.pushScreen(state: &state, screen: .settings(.init(me: state.$me))))

        case .account:
            effects.append(self.pushScreen(state: &state, screen: .account(.init(me: state.$me, showBackButton: false))))

        case .subscriptions:
            effects.append(self.pushScreen(state: &state, screen: .subscriptions(.init())))

        case .topUp:
            effects.append(self.pushScreen(state: &state, screen: .topUp(.init())))

        case .webView(let title, let url):
            effects.append(self.pushScreen(state: &state, screen: .webView(.init(url: url, title: title))))

        case .search(let searchType):
            effects.append(self.pushScreen(state: &state, screen: .search(.init(me: state.$me, searchType: searchType))))

        case .remixesList(let parentClip):
            effects.append(self.pushScreen(state: &state, screen: .remixesList(.init(me: state.$me, parentClip: parentClip))))

        case .appearance:
            effects.append(self.pushScreen(state: &state, screen: .appearance(.init())))

        case .weeklyHitsPlaylist:
            print("No-op: weekly hits playlist not implemented yet")
        }
        // All stack pushes should close the Omniplayer
        getOmniplayerChannel().queue(.setExpanded(false))
        // Needs to be `.concatenate` for proper ordering
        return .concatenate(effects)
    }
}

public struct RootTabView: View {
    @Bindable var store: StoreOf<RootTabCoordinator>
    var omniPlayerStore: StoreOf<OmniPlayerReducer>?
    @Namespace var transitionNamespace
    @Shared(.inMemory(.omniPlayerDragOffset)) var omniPlayerDragOffset: CGFloat = 0
    @Environment(\.safeAreaInsets) var safeAreaInsets

    public init(store: StoreOf<RootTabCoordinator>, omniPlayerStore: StoreOf<OmniPlayerReducer>? = nil) {
        self.store = store
        self.omniPlayerStore = omniPlayerStore
    }

    private var hideOmniPlayerOverHooks: Bool {
        return store.isOnHooksFeed
    }

    private var bottomScrollContentMargin: CGFloat {
        let padding = 12.0
        return bottomScrollIndicatorsContentMargin + padding
    }

    private var bottomScrollIndicatorsContentMargin: CGFloat {
        @Shared(.inMemory(.isCompactPlayerVisible)) var isCompactPlayerVisible = false

        if isCompactPlayerVisible {
            return CustomBottomBarConstants.tabBarHeight + CustomBottomBarConstants.compactPlayerHeight
        } else {
            return CustomBottomBarConstants.tabBarHeight
        }
    }

    public var body: some View {
        ZStack(alignment: .bottom) {
            VStack(spacing: 0) {
                if !FeatureFlag.legacy.songGenerationBannerV2 {
                    BannerView(store: store.scope(state: \.bannerState, action: \.bannerAction))
                        .opacity(store.isOnHooksFeed ? 0 : 1)
                }

                if #available(iOS 18.0, *) {
                    TabView(selection: $store.selectedTab) {
                        discoverStack
                        libraryStack
                        notificationsStack
                        profileStack
                        if FeatureFlag.hooks.isFeedEnabled {
                            hooksStack
                        }
                    }
                    .removeTransitions()
                } else {
                    ZStack {
                        discoverStack
                            .opacity(store.selectedTab == .discover ? 1 : 0)
                        libraryStack
                            .opacity(store.selectedTab == .library ? 1 : 0)
                        notificationsStack
                            .opacity(store.selectedTab == .notifications ? 1 : 0)
                        profileStack
                            .opacity(store.selectedTab == .profile ? 1 : 0)
                        if FeatureFlag.hooks.isFeedEnabled {
                            hooksStack
                                .opacity(store.selectedTab == .hooks ? 1 : 0)
                        }
                    }
                }
            }

            if FeatureFlag.legacy.songGenerationBannerV2 {
                songGenerationBannerV2Overlay
            }

            // Add OmniPlayer if present
            if let omniPlayerStore = omniPlayerStore, !store.hideOmniPlayerOverHooks {
                omniPlayer(omniPlayerStore: omniPlayerStore)
            }

            tabBar
                .offset(y: tabBarOffset)
                .animation(tabBarAnimation, value: tabBarOffset)
        }
        .contentMargins(.bottom, bottomScrollIndicatorsContentMargin, for: .scrollIndicators)
        .contentMargins(.bottom, bottomScrollContentMargin, for: .scrollContent)
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color.SemanticV1.backgroundPrimary)
        .task {
            store.send(.task)
        }
        .onAppear {
            store.send(.onAppear)
        }
        .accentColor(tabBarAccentColor)
        .animation(.default, value: tabBarAccentColor)
        .ignoresSafeArea(.keyboard)
    }

    @ViewBuilder
    private var songGenerationBannerV2Overlay: some View {
        VStack {
            BannerViewV2(store: store.scope(state: \.bannerState, action: \.bannerAction))
                .padding(.top, 58)
            Spacer()
        }
        .ignoresSafeArea()
    }

    @ViewBuilder
    private func omniPlayer(omniPlayerStore: StoreOf<OmniPlayerReducer>) -> some View {
        OmniPlayerView(store: omniPlayerStore, namespace: transitionNamespace)
            .animation(.easeInOut, value: omniPlayerStore.isCollapsed)
            .ignoresSafeArea(.keyboard)
    }

    @ViewBuilder
    private var tabBar: some View {
        if FeatureFlag.hooks.isFeedEnabled {
            HooksTabBar(store: store)
        } else {
            CustomTabBar(store: store)
        }
    }

    @ViewBuilder
    private var discoverStack: some View {
        NavigationStackView(
            store: store.scope(state: \.discoverStack, action: \.discoverStack),
            root: {
                DiscoverScreen(
                    store: store.scope(state: \.discover, action: \.discover)
                )
                .navigationBarTitleDisplayMode(.inline)
                .toolbar {
                    GlasslessToolbarItem(placement: .navigationBarLeading) {
                        Text(L10n.FeatureRoot.explore)
                            .typographyV1(.headline3)
                            .foregroundStyle(Color.SemanticV1.textPrimary)
                            .shadow(color: Color.SemanticV1.backgroundPrimary.opacity(0.75), radius: 8)
                            .compositingGroup()
                            .fixedSize()
                            .offset(x: -5, y: 2) // align with content
                    }
                }
            },
            transitionNamespace: transitionNamespace
        )
        .tag(TabBarTab.discover)
        .toolbar(.hidden, for: .tabBar)
        .contentMargins(.bottom, bottomScrollIndicatorsContentMargin, for: .scrollIndicators)
        .contentMargins(.bottom, bottomScrollContentMargin, for: .scrollContent)
    }

    @ViewBuilder
    private var libraryStack: some View {
        NavigationStackView(
            store: store.scope(state: \.libraryStack, action: \.libraryStack),
            root: {
                LibraryScreen(store: store.scope(state: \.library, action: \.library))
                    .navigationBarTitleDisplayMode(.inline)
                    .toolbar {
                        GlasslessToolbarItem(placement: .navigationBarLeading) {
                            Text(L10n.FeatureRoot.library)
                                .typographyV1(.headline3)
                                .foregroundStyle(Color.SemanticV1.textPrimary)
                                .shadow(color: Color.SemanticV1.backgroundPrimary.opacity(0.75), radius: 8)
                                .compositingGroup()
                                .fixedSize()
                                .offset(x: -5, y: 2) // align with content
                        }
                    }
            },
            transitionNamespace: transitionNamespace
        )
        .tag(TabBarTab.library)
        .toolbar(.hidden, for: .tabBar)
        .contentMargins(.bottom, bottomScrollIndicatorsContentMargin, for: .scrollIndicators)
        .contentMargins(.bottom, bottomScrollContentMargin, for: .scrollContent)
    }

    @ViewBuilder
    private var notificationsStack: some View {
        NavigationStackView(
            store: store.scope(state: \.notificationsStack, action: \.notificationsStack),
            root: {
                let notificationsStore = store.scope(state: \.notifications, action: \.notifications)
                NotificationsScreen(store: notificationsStore)
            },
            transitionNamespace: transitionNamespace
        )
        .tag(TabBarTab.notifications)
        .toolbar(.hidden, for: .tabBar)
        .contentMargins(.bottom, bottomScrollIndicatorsContentMargin, for: .scrollIndicators)
        .contentMargins(.bottom, bottomScrollContentMargin, for: .scrollContent)
    }

    @ViewBuilder
    private var profileStack: some View {
        NavigationStackView(
            store: store.scope(state: \.profileStack, action: \.profileStack),
            root: {
                PublicProfileScreenV1(
                    store: store.scope(state: \.profile, action: \.profile),
                    isVisibleProxy: store.selectedTab == .profile
                )
                .navigationBarTitleDisplayMode(.inline)
            },
            transitionNamespace: transitionNamespace
        )
        .tag(TabBarTab.profile)
        .toolbar(.hidden, for: .tabBar)
        .contentMargins(.bottom, bottomScrollIndicatorsContentMargin, for: .scrollIndicators)
        .contentMargins(.bottom, bottomScrollContentMargin, for: .scrollContent)
    }

    @ViewBuilder
    private var hooksStack: some View {
        if let hooksStore = store.scope(state: \.hooks, action: \.hooks) {
            NavigationStackView(
                store: store.scope(state: \.hooksStack, action: \.hooksStack),
                root: {
                    HooksFeedScreen(store: hooksStore)
                        .navigationBarTitleDisplayMode(.inline)
                },
                transitionNamespace: transitionNamespace
            )
            .tag(TabBarTab.hooks)
            .toolbar(.hidden, for: .tabBar)
            .contentMargins(.bottom, bottomScrollIndicatorsContentMargin, for: .scrollIndicators)
            .contentMargins(.bottom, bottomScrollContentMargin, for: .scrollContent)
        }
    }

    private var tabBarAccentColor: Color {
        switch store.selectedSunoModel.marketingLevelUnderstanding {
        case .previousToV4, .v3Dot5:
            Color.SemanticV1.auraPink
        case .v4:
            Color.SemanticV1.v4Blue
        case .auk:
            Color.SemanticV1.v4Blue
        case .bluejay:
            Color.SemanticV1.v4Blue
        // TODO: not confirmed for V5 (Azim)
        case .v5:
            Color.SemanticV1.v4Blue
        }
    }

    private var tabBarAccentCGColor: CGColor {
        switch store.selectedSunoModel.marketingLevelUnderstanding {
        case .previousToV4, .v3Dot5:
            return UIColor.SemanticV1.auraPink.cgColor
        case .v4:
            return UIColor.SemanticV1.v4Blue.cgColor
        case .auk:
            return UIColor.SemanticV1.v4Blue.cgColor
        case .bluejay:
            return UIColor.SemanticV1.v4Blue.cgColor
        // TODO: not confirmed for V5 (Azim)
        case .v5:
            return UIColor.SemanticV1.v4Blue.cgColor
        }
    }

    private var tabBarCreateBackground: some View {
        switch store.selectedSunoModel.marketingLevelUnderstanding {
        case .previousToV4, .v3Dot5:
            Image.Assets.createTabIconBackground
                .resizable()

        case .v4:
            Image.Assets.createTabIconBackgroundV4
                .resizable()

        case .auk:
            Image.Assets.createTabIconBackgroundV4
                .resizable()

        case .bluejay:
            Image.Assets.createTabIconBackgroundV4
                .resizable()

        // TODO: not confirmed for V5 (Azim)
        case .v5:
            Image.Assets.createTabIconBackgroundV4
                .resizable()
        }
    }

    // Fast, snappy animation
    private var tabBarAnimation: Animation {
        return .easeOut(duration: 0.15)
    }

    // Tab bar offset with drag support but simplified
    private var tabBarOffset: CGFloat {
        let tabBarHeight: CGFloat = CustomBottomBarConstants.tabBarHeight
        let safeAreaBottom = safeAreaInsets.bottom
        let tabBarFullOffset = tabBarHeight + safeAreaBottom

        // Check if we're displaying OmniPlayer from HooksFeedScreen
        if store.isOnHooksFeed && store.isHooksOmniPlayerVisible {
            return tabBarFullOffset
        }

        guard let omniPlayerStore = omniPlayerStore else { return 0 }

        // Handle drag offset for smooth swipe gestures
        if omniPlayerDragOffset != 0 {
            let maxDragRange: CGFloat = 300 // Simplified drag range
            if omniPlayerDragOffset < 0 {
                // Swiping up - show omniplayer, hide tab bar
                let progress = min(abs(omniPlayerDragOffset) / maxDragRange, 1.0)
                let currentOffset: CGFloat = omniPlayerStore.isCollapsed ? 0 : tabBarFullOffset
                return currentOffset + (progress * tabBarFullOffset)
            } else if omniPlayerDragOffset > 0 {
                // Swiping down - dismiss omniplayer, show tab bar
                let progress = min(omniPlayerDragOffset / maxDragRange, 1.0)
                let currentOffset: CGFloat = omniPlayerStore.isCollapsed ? 0 : tabBarFullOffset
                return currentOffset - (progress * tabBarFullOffset)
            }
        }

        return omniPlayerStore.isCollapsed ? 0 : tabBarFullOffset
    }
}
