import APIClient
import BlendedCreateClient
import ClipPollingClient
import ComponentLibrary
import ComposableArchitecture
import DeeplinkIntents
import EventBusClient
import FeatureAnnouncements
import FeatureBanner
import FeatureBrandedAlert
import FeatureClipDetail
import FeatureClipList
import FeatureCreateClip
import FeatureEditClip
import FeatureHookDownload
import FeatureHooksCreate
import FeatureHooksMoreMenu
import FeatureModals
import FeatureOmniPlayer
import FeaturePaywall
import FeaturePlaylistDetail
import FeatureProfile
import FeatureRatingTracker
import FeatureRemix
import FeatureShareSheet
import FeatureSocial
import FeatureToasts
import Foundation
import HookActionsClient
import Localization
import NavigationRouterClient
import OmniPlayerClient
import PlayerClient
import ShareAssetClient
import SnippetPlayerClient
import SongActionsClient
import StatsigClient
import SunoModelClient
import SwiftUI
import UserDefaultsClient
import Utilities

// swiftlint:disable file_length

@Reducer
public struct RootCoordinatorV1 {
    @Reducer(state: .equatable)
    public enum PlayerDestination {
        case omniPlayer(OmniPlayerReducer)
    }

    @Reducer(state: .equatable)
    public enum Destination {
        case subscriptions(PaywallV1)
        case blendedCreate(BlendedCreate)
        case chatCreate(Chat)
        case modalCarousel(ModalReducer)
        case creditsPopup(CreditsPopup)
        case alert(AlertState<Alert>)
        case brandedAlert(BrandedAlert)
        case ratingTrackerState(RatingTracker)
        case editClip(EditClipCoordinator)
        case orpheusCustomCreate(OrpheusCustomCreate)
        case coverClip(CoverClip)
        case remixAnnouncement(RemixAnnouncement)
        case bluejayAnnouncement(BluejayAnnouncement)
        case v5Announcement(V5Announcement)
        case remixActions(RemixActions)
        case hooksCreate(HooksCreateCoordinator)
        case mediaPicker(RootMediaPicker)

        case hooksMoreMenu(HookActions)
        case reportHookInappropriate(ReportHookInappropriateReducer)

        // SongActionsMenu & Sub-Menus
        case songActions(SongActions)
        case selectPlaylist(SelectPlaylist)
        case shareV2(ShareSheetReducer<Clip>)
        case shareHookV2(ShareSheetReducer<Hook>)

        public enum Alert {
            case dismiss
            case upgrade
        }
    }

    @ObservableState
    public struct State: Equatable {
        struct GenerationStatus {
            var message: String
            var completionRatio: Double
        }

        static let generationMessages: [GenerationStatus] = [
            .init(message: L10n.FeatureRoot.generationMessage1, completionRatio: 0.3),
            .init(message: L10n.FeatureRoot.generationMessage2, completionRatio: 0.45),
            .init(message: L10n.FeatureRoot.generationMessage3, completionRatio: 0.6),
            .init(message: L10n.FeatureRoot.generationMessage4, completionRatio: 0.7),
            .init(message: L10n.FeatureRoot.generationMessage5, completionRatio: 0.8),
            .init(message: L10n.FeatureRoot.generationMessage6, completionRatio: 0.9),
        ]

        static let featureArtistTimbalandPlaylistId = "2479ec84-fc53-4611-b014-0ffc90c030dd"

        @Presents public var destination: Destination.State?
        @Presents public var playerDestination: PlayerDestination.State?

        @ObservationStateIgnored @ObservedBox var rootTab: RootTabCoordinator.State
        @ObservationStateIgnored @ObservedBox var ratingTrackerState = RatingTracker.State()
        @ObservationStateIgnored @ObservedBox var bottomToastState = ToastReducer.State()
        @ObservationStateIgnored @ObservedBox var topToastState = ToastReducer.State()

        @ObservationStateIgnored @ObservedBox var brandedAlert = BrandedAlert.State()

        @Shared public internal(set) var me: Me
        var didCreateSongs: Bool = false
        let isFTUX: Bool
        var recentAction: Action.Undoable?

        var recentGeneratedClips: [Clip]?
        var serviceStatus: ServiceStatus?
        @Shared(.inMemory(.billingInfo)) var billingInfo: SubscriptionInfoResponse?
        var isLoadingBillingInfo: Bool = false
        var pendingCreateHookClip: Clip?

        var hasConfirmedDeleteHook: Bool = false
        var lastKnownAlertType: BrandedAlertStyle? = .noAlert
        var moreMenuLastHook: Hook?
        var moreMenuLastSource: HooksFeedSource?

        /*
         Since we don't have an /upgrade/ API
         and only rely on RevCat webhooks to call our backend,
         we can't tie the upgrade status to a successful `PaywallClient.purchase()`
         call. Instead, we rely on /billing/info/ to come back with an updated
         `subscription_platform` value. Then, we set `didUpgradeAndRefresh`
         if it was updated properly.
         */
        var didUpgradeAndShouldRefresh = false

        /*
         If we're waiting to display a "Create with V4" alert,
         or any alert that relies on the most up-to-date billing,
         we'll need to use this and call the appropriate
         announcement/banner action to trigger the correct alert.
         */

        @Shared(.inMemory(.readyNewGensCount)) var readyNewGensCount: Int = 0
        @Shared(.appStorage(.hasSeenFeaturedArtist)) var hasSeenFeaturedArtist: Bool = false
        @Shared(.appStorage(.hasSeenCreateFirstSong)) var hasSeenCreateFirstSong: Bool = false
        @Shared(.appStorage(.hasSeenVideoSongArtWalkthroughAlert)) var hasSeenVideoSongArtWalkthroughAlert: Bool = false
        @Shared(.appStorage(.hasSeenV5Announcement)) var hasSeenV5Announcement: Bool = false

        @Shared(.fileStorage(.savedPrompts)) var savedPrompts: [Clip.ID: Prompt] = [:]
        @Shared(.inMemory(.playingClipKey)) var playingClip: Clip?
        @Shared(.inMemory(.isPlayingKey)) var isPlaying: Bool = false
        @Shared(.inMemory(.selectedSunoModel)) var selectedSunoModel: SunoModelMetaData = .modelDefault
        @Shared(.inMemory(.selectedSunoRemasterModel)) var selectedSunoRemasterModel: RemasterModelMetaData?
        @Shared(.inMemory(.sunoModelUserAccessCategory)) var sunoModelUserAccessCategory: SunoModelUserAccess = .defaultLimitedAccess
        @Shared(.inMemory(.isHooksCreateVisible)) var isHooksCreateVisible: Bool = false

        var isFeaturedArtistEnabled: Bool {
            FeatureFlag.legacy.featuredArtist
        }

        var shouldShowV5Announcement: Bool {
            return FeatureFlag.legacy.v5Launch == true // Feature flag is on
                && hasSeenV5Announcement == false // User has not seen the announcement before
        }

        var shouldShowV5ModelUpsellPopUp: Bool {
            return FeatureFlag.legacy.v5Launch == true // Feature flag is on
        }

        // Don't show an announcement if we've already seen one this session. Tracked in-memory
        var announcementSeenThisSession = false

        var modalsToShow: [Modal] = []

        var shouldShowModalCarousel: Bool {
            return FeatureFlag.legacy.useAppLaunchModalCarousel && !modalsToShow.isEmpty
        }

        var bottomToastVerticalOffset: CGFloat {
            @Shared(.inMemory(.isCompactPlayerVisible)) var isCompactPlayerVisible = false
            let tabBarHeight: CGFloat = CustomBottomBarConstants.tabBarHeight
            let compactPlayerHeight: CGFloat = OmniPlayerConstants.compactPlayerHeight
            let bottomSafeAreaHeight: CGFloat = isCompactPlayerVisible ? tabBarHeight + compactPlayerHeight : tabBarHeight
            return bottomSafeAreaHeight + 4
        }

        public init(me: Me, isFTUX: Bool, launchDeeplinkIntent: DeeplinkIntent?) {
            let sharedMe = Shared(value: me)
            self._me = sharedMe
            self.isFTUX = isFTUX
            self.rootTab = .init(me: sharedMe, launchDeeplinkIntent: launchDeeplinkIntent)
        }
    }

    public enum Action: BindableAction {
        public enum Delegate {
            case triggerPushNotificationRequest
        }

        public enum Internal {
            case checkCompletion([Clip])
            case checkCompletionResponse(Result<[Clip], Error>)
            case serviceStatusResponse(Result<ServiceStatus?, Error>)
            case billingInfoResponse(Result<SubscriptionInfoResponse, Error>)
            case reachabilityDidChange(Bool?)
            case playClipsAt(Clip, [Clip], _ context: SessionContext)
            case playNewClips
            case updateGenerationMessage(index: Int)
            case updateVideoProcessingMessage(String)
            case generationComplete([Clip])
            case checkCreatedSongsResponse(Result<Bool, Error>)
            case remasterClipResponse(Result<[Clip], Error>)
            case teardownCoverSheet
            case queueDestination(clip: Clip, destination: QueueDestinationType)
            case dismiss
        }

        public enum Deeplink {
            case clipResponse(Result<Clip, Error>)
            case hookResponse(urlString: String?, Result<Hook, Error>)
            case playlistResponse(Result<Playlist, Error>)
            case contentShortlinkResponse(Result<ShareCodeResponse, Error>)
            case remixClipResponse(Result<Clip, Error>, type: String?, style: String?, lyrics: String?)
        }

        /// These delegate clients only allow for a many-to-one producer -> consumer behavior.
        /// Note that if `RootCoordinatorV1` is establishing a stream with these dependencies, establishing another
        /// stream elsewhere will overwrite `RootCoordinatorV1`'s ownership.
        /// For context see https://github.com/suno-ai/app-ios/pull/802
        case clipListItemDelegate(ClipListItem.Action.DelegateClient)

        case destination(PresentationAction<Destination.Action>)
        case playerDestination(PresentationAction<PlayerDestination.Action>)
        case brandedAlert(BrandedAlert.Action)
        case rootTab(RootTabCoordinator.Action)
        case omniPlayer(OmniPlayerReducer.Action)
        case ratingTrackerAction(RatingTracker.Action)
        case bottomToastAction(ToastReducer.Action)
        case topToastAction(ToastReducer.Action)
        case onAppear
        case task
        case createTapped
        case handleDeeplink(DeeplinkIntent)
        case deleteClip(Clip)

        case showBrandedAlert(BrandedAlertStyle)
        case showAnnouncementIfNeeded // TODO: Make generic to use queue of announcements
        case showFeaturedArtistAnnouncement
        case showCreateFirstSongAnnouncement
        case showModalCarousel
        case showV5Announcement
        case showHooksCreate
        case showHooksCreateWithSong(Clip?)
        case showSongActions(clip: Clip, fromPlaylist: Playlist?)
        case showOrpheusCustomCreate
        case showOrpheusCustomCreateCover

        case checkForUpgrades

        case pauseAndDismissPlayer

        case binding(BindingAction<State>)
        case `internal`(Internal)
        case delegate(Delegate)

        case getServiceStatus
        case getModals
        case getBillingInfo
        case checkCreatedSongs

        // MARK: Event bus channels

        case omniplayerChannel(EventBusClient.OmniPlayerEvent)
        case createChannel(EventBusClient.CreateEvent)
        case billingChannel(EventBusClient.BillingEvent)
        case shareAssetEvent(ShareAssetClient.ShareAssetClientEvent)
        case hookDownloadEvent(HookDownloadClient.HookDownloadClientEvent)
        case blendedResultStream(BlendedCreateClient.BlendedGenerationResult)
        case toastEvent(ToastClient.ToastEvent)
        case clipEvents(EventBusClient.ClipEvent)
        case hookEvent(EventBusClient.HookEvent)
        case modalEvent(EventBusClient.ModalEvent)
        case deeplink(Deeplink)
        case deeplinkEvent(EventBusClient.DeeplinkEvent)

        case undo(Undoable)

        // For toast: delete clip undo
        public enum Undoable: Equatable {
            case deleteClip(Clip)
        }
    }

    @Dependency(\.apiClientV2) var api
    @Dependency(PlayerClient.self) var playerClient
    @Dependency(APIClient.self) var apiClient
    @Dependency(\.shareAssetClient.stream) var shareAssetEventStream
    @Dependency(\.hookDownloadClient.stream) var hookDownloadEventStream
    @Dependency(\.continuousClock) var clock
    @Dependency(\.telemetryClient) var telemetry
    @Dependency(\.mainQueue) var mainQueue
    @Dependency(UserDefaultsClient.self) var userDefaults
    @Dependency(SunoModelClient.self) var sunoModelClient
    @Dependency(ClipListItemDelegateClient.self) private var clipListItemDelegate
    @Dependency(NavigationRouterClient.self) var navigationRouter
    @Dependency(\.eventBus.getOmniplayerChannel) private var getOmniplayerChannel
    @Dependency(\.eventBus.getCreateChannel) private var getCreateChannel
    @Dependency(\.eventBus.getDeeplinkPublisher) private var getDeeplinkPublisher
    @Dependency(\.eventBus.sendClipEvent) private var sendClipEvent
    @Dependency(\.eventBus.sendHookEvent) private var sendHookEvent
    @Dependency(\.eventBus.getBillingChannel) private var getBillingChannel
    @Dependency(\.toastClient) var toastClient
    @Dependency(\.toastClient.stream) private var toastStream
    // Migrate to event bus v2
    @Dependency(\.blendedCreateClient.generateResultStream) private var blendedCreateResultStream
    @Dependency(VideoCoverClient.self) var videoCoverClient
    @Dependency(\.eventBus.getClipPublisher) private var getClipPublisher
    @Dependency(\.eventBus.getHookPublisher) private var getHookPublisher
    @Dependency(OmniPlayerClient.self) var omniplayerClient
    @Dependency(SongActionsClient.self) var songActionsClient
    @Dependency(ClipPollingClient.self) var clipPollingClient
    @Dependency(\.hookActionsClient) var hookActionsClient
    @Dependency(\.hooksPlayerClient) var hooksPlayerClient
    @Dependency(\.modalClient) var modalClient
    @Dependency(\.eventBus.getModalPublisher) private var getModalPublisher

    public init() {}

    public var body: some ReducerOf<Self> {
        // Helper methods for Omniplayer operations
        Scope(state: \.rootTab, action: \.rootTab) {
            RootTabCoordinator()
        }
        Scope(state: \.bottomToastState, action: \.bottomToastAction) {
            ToastReducer()
        }
        Scope(state: \.topToastState, action: \.topToastAction) {
            ToastReducer()
        }
        Scope(state: \.ratingTrackerState, action: \.ratingTrackerAction) {
            RatingTracker()
        }
        Scope(state: \.brandedAlert, action: \.brandedAlert) {
            BrandedAlert()
        }
        BindingReducer()
        Reduce<State, Action> {
            state,
                action in
            struct CheckCompletionCancellableId: Hashable {}
            struct ServiceStatusCancellableId: Hashable {}
            struct ReachabilityDidChangeCancellableId: Hashable {}
            struct ClipListItemDelegateStreamCancellableId: Hashable {}
            struct OmniplayerChannelCancellableId: Hashable {}
            struct CreateChannelCancellableId: Hashable {}
            struct BillingChannelCancellableId: Hashable {}
            struct BlendedCreateCancellableId: Hashable {}
            struct ShareAssetCancellableId: Hashable {}
            struct HookDownloadCancellableId: Hashable {}
            struct ToastCancellableId: Hashable {}
            struct HookEventCancellableId: Hashable {}
            struct ClipEventCancellableId: Hashable {}
            struct ModalEventCancellableId: Hashable {}
            struct DeeplinkEventCancellableId: Hashable {}

            switch action {
            case .destination(.presented(.mediaPicker(.delegate(.didPick(let url))))):
                if let clip = state.pendingCreateHookClip {
                    state.destination = .hooksCreate(.init(me: state.$me, initialClip: clip, initialLocalVideoURL: url))
                }
                return .none

            case .destination(.presented(.mediaPicker(.delegate(.didCancel)))):
                state.pendingCreateHookClip = nil
                state.destination = nil
                return .none

            case .onAppear:
                return .concatenate(
                    .send(.getModals),
                    .send(.getServiceStatus),
                    .send(.getBillingInfo),
                    .send(.checkCreatedSongs),
                    .publisher {
                        NotificationCenter.default.publisher(for: reachabilityDidChangeNotification, object: nil)
                            .receive(on: mainQueue)
                            .map { notification in .internal(.reachabilityDidChange(notification.userInfo?["status"] as? Bool)) }
                    }.cancellable(id: ReachabilityDidChangeCancellableId())
                )

            case .task:
                return .merge(
                    .stream(clipListItemDelegate.getStream(), send: Action.clipListItemDelegate, cancellableId: ClipListItemDelegateStreamCancellableId()),
                    .channel(getOmniplayerChannel(), send: Action.omniplayerChannel, cancellableId: OmniplayerChannelCancellableId()),
                    .channel(getCreateChannel(), send: Action.createChannel, cancellableId: CreateChannelCancellableId()),
                    .channel(getBillingChannel(), send: Action.billingChannel, cancellableId: BillingChannelCancellableId()),
                    .stream(blendedCreateResultStream(), send: Action.blendedResultStream, cancellableId: BlendedCreateCancellableId()),
                    .stream(shareAssetEventStream(), send: Action.shareAssetEvent, cancellableId: ShareAssetCancellableId()),
                    .stream(hookDownloadEventStream(), send: Action.hookDownloadEvent, cancellableId: HookDownloadCancellableId()),
                    .stream(toastStream(), send: Action.toastEvent, cancellableId: ToastCancellableId()),
                    .subscribe(getClipPublisher(), send: Action.clipEvents, cancellableId: ClipEventCancellableId()),
                    .subscribe(getHookPublisher(), send: Action.hookEvent, cancellableId: HookEventCancellableId()),
                    .subscribe(getModalPublisher(), send: Action.modalEvent, cancellableId: ModalEventCancellableId()),
                    .subscribe(getDeeplinkPublisher(), send: Action.deeplinkEvent, cancellableId: DeeplinkEventCancellableId()),
                )

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

            case .rootTab(.delegate(.showGeneratedClips)):
                return .send(.internal(.playNewClips), animation: .default)

            case .omniplayerChannel(let omniplayerEvent):
                switch omniplayerEvent {
                case .setExpanded(let isExpanded):
                    return setExpandedOmniPlayer(state: state, expanded: isExpanded)

                case .playClip(let clip, let queue, let context):
                    return .send(.internal(.playClipsAt(clip, queue, context)))

                case .present(let clip, let autoPlay, let context):
                    if state.playingClip?.id == clip.id,
                       let _ = state.playerDestination
                    {
                        // If we're already playing the clip, handle comments if requested
                        var effects: [Effect<Action>] = []
                        if case .openComments(let targetCommentId) = context.navigationIntent {
                            if let commentId = targetCommentId {
                                effects.append(.send(.playerDestination(.presented(.omniPlayer(.expanded(.setDeepLinkCommentId(commentId)))))))
                            }
                            effects.append(.send(.playerDestination(.presented(.omniPlayer(.openCommentsInExpandedPlayer(context: context))))))
                            return .concatenate(effects)
                        } else {
                            // Otherwise just toggle play/pause
                            return togglePlayPauseOmniPlayer(state: state)
                        }
                    } else {
                        guard !clip.audioUrl.isEmpty else { return .none }
                        var effects: [Effect<Action>] = []
                        // Launch player with clip
                        let launchEffect = launchOmniPlayer(
                            state: &state,
                            clip: clip,
                            queue: [clip],
                            isExpanded: true,
                            autoStart: autoPlay,
                            context: context
                        )
                        effects.append(launchEffect)

                        if case .openComments(let targetCommentId) = context.navigationIntent {
                            if let commentId = targetCommentId {
                                effects.append(.send(.playerDestination(.presented(.omniPlayer(.expanded(.setDeepLinkCommentId(commentId)))))))
                            }
                            effects.append(.send(.playerDestination(.presented(.omniPlayer(.openCommentsInExpandedPlayer(context: context))))))
                        }

                        return .concatenate(effects)
                    }

                case .refresh:
                    // Resets the player state to the previous queue if it exists
                    guard let clip = state.playingClip,
                          let playerDestination = state.playerDestination,
                          let omniPlayer = playerDestination.omniPlayer,
                          let index = omniPlayer.queue.firstIndex(where: { $0.clip == clip })
                    else {
                        return .none
                    }
                    return .send(.playerDestination(.presented(.omniPlayer(.refreshQueueFrom(index: index)))))
                }

            case let .playerDestination(.presented(.omniPlayer(.delegate(.editPromptTapped(_, prompt))))):
                // First collapse the omniplayer, then open create
                createClip(prompt, state: &state)
                return setExpandedOmniPlayer(state: state, expanded: false)

            case .shareAssetEvent(let shareAssetEvent):
                return effectForShareAssetEvent(shareAssetEvent)

            case .hookDownloadEvent(let hookDownloadEvent):
                return effectForHookDownloadEvent(hookDownloadEvent)

            case .clipEvents(.showSongActions(let clip, let playlist)):
                return .send(.showSongActions(clip: clip, fromPlaylist: playlist))

            case .showSongActions(let clip, let playlist):
                state.destination = .songActions(.init(
                    clip: clip,
                    playlist: playlist,
                    me: state.$me
                ))
                return .none

            case .createChannel(let createEvent):
                switch createEvent {
                case .createClip(let prompt, let didUseAppShortcut):
                    // Save the state before we modify it so we only
                    // call `pauseCurrentHook` if we were on a Hooks feed
                    // before switching over
                    let wasOnHooksFeed = state.rootTab.isOnHooksFeed
                    if FeatureFlag.hooks.isFeedEnabled {
                        state.rootTab.$isOnHooksFeed.withLock { $0 = false }
                        if wasOnHooksFeed {
                            hooksPlayerClient.pauseCurrentHook(.navigation(.createSong))
                        }
                    }
                    createClip(prompt, didUseAppShortcut: didUseAppShortcut, state: &state)
                    return .none

                case .remasterClip(let clip):
                    if state.sunoModelUserAccessCategory.remasterAccess.hasAccess {
                        return .run { [remasterModel = state.selectedSunoRemasterModel?.externalKey ?? SunoModelClient.ModelConstants.defaultModel.rawValue] send in
                            await send(.internal(.remasterClipResponse(Result(catching: {
                                try await api.upsampleClipWithModel(clipId: clip.id, modelName: remasterModel)
                            }))))
                        }
                    } else {
                        if state.shouldShowV5ModelUpsellPopUp {
                            return .send(.showV5Announcement, animation: .default)
                        } else {
                            return .send(.showBrandedAlert(.versioningAlert(.preset(.v4_5FreeUserUpgradeInfoPush))))
                        }
                    }

                case .extendClip(let clip, let prompt, let hook):
                    guard case .editClip = state.destination else {
                        state.destination = .editClip(.init(clip: clip, me: state.$me, prompt: prompt, hook: hook))
                        return .run { _ in
                            // Pause any clips on omni player
                            @Dependency(\.omniplayerClient.pauseCurrentClip) var pauseCurrentClip
                            pauseCurrentClip()
                        }
                    }
                    return .send(.destination(.presented(.editClip(.loadClipIntoEditor(clip)))))

                case let .coverClip(clip, prompt, hook):
                    state.destination = .coverClip(.init(clip: clip, me: state.$me, prompt: prompt, hook: hook))
                    return .none

                case .reusePrompt(let prompt):
                    let isRemix = FeatureFlag.create.remixAndAttribution ? true : nil
                    createClip(prompt, isRemix: isRemix, state: &state)
                    return .none

                case .getFullClip(let clip):
                    return .run { send in
                        await withTaskCancellation(id: CheckCompletionCancellableId(), cancelInFlight: true) {
                            await withTaskGroup(of: Void.self) { group in
                                group.addTask {
                                    for await _ in clock.timer(interval: .seconds(3)) {
                                        await send(.internal(.checkCompletion([clip])), animation: .default)
                                    }
                                }

                                group.addTask {
                                    await send(.rootTab(.bannerAction(.show(
                                        type: .progress(.string(L10n.FeatureRoot.makingFullSongMessage), completionRatio: 0.2),
                                        autoDismiss: false
                                    ))))

                                    try? await Task.sleep(for: .seconds(3))

                                    var index = State.generationMessages.startIndex
                                    await send(.internal(.updateGenerationMessage(index: index)))

                                    for await _ in clock.timer(interval: .seconds(2)) {
                                        index = State.generationMessages.index(after: index)
                                        await send(.internal(.updateGenerationMessage(index: index)))
                                    }
                                }

                                group.addTask {
                                    // If we're on the Library screen, reset filters and
                                    // refresh to show the new clips
                                    await send(.rootTab(.library(.showNewClips)))
                                }
                                await group.waitForAll()
                            }
                        }
                    }

                case .showRemixActions(let clip, let hook):
                    state.destination = .remixActions(.init(
                        me: state.$me,
                        clip: clip,
                        hook: hook
                    ))
                    return .none
                }

            case .destination(.presented(.brandedAlert(.delegate(.didTriggerWithIntentionToUpgrade)))):
                UIImpactFeedbackGenerator(style: .medium).impactOccurred()
                state.destination = .subscriptions(.init())
                return .send(.brandedAlert(.setStyle(.noAlert)))

            case .destination(.presented(.brandedAlert(.delegate(.didTriggerWithIntentionToRemasterSong)))):
                state.destination = nil
                navigationRouter.send(.library(tooltipToShow: .preset(.remasterLaunch), showNewClips: false))
                return .none

            case .destination(.presented(.brandedAlert(.delegate(.didTriggerWithIntentionToAddVideoSongArt)))):
                state.destination = nil
                navigationRouter.send(.library(tooltipToShow: .preset(.videoSongArtWalkthroughRedirect), showNewClips: false))
                return .none

            case .internal(.remasterClipResponse(.failure(let error))):
                log.telemetry.error(error)
                return .send(.topToastAction(.show(.warning(L10n.FeatureRoot.errorMessage))))

            case .internal(.remasterClipResponse(.success(let clips))):
                return .run { send in
                    await withTaskCancellation(id: CheckCompletionCancellableId(), cancelInFlight: true) {
                        await withTaskGroup(of: Void.self) { group in
                            group.addTask {
                                for await _ in clock.timer(interval: .seconds(3)) {
                                    await send(.internal(.checkCompletion(clips)), animation: .default)
                                }
                            }
                            group.addTask {
                                await send(.rootTab(.bannerAction(.show(
                                    type: .progress(.string(L10n.FeatureRoot.remasteringClipMessage), completionRatio: 0.2),
                                    autoDismiss: false
                                ))))

                                try? await Task.sleep(for: .seconds(2))

                                var index = State.generationMessages.startIndex
                                await send(.internal(.updateGenerationMessage(index: index)))

                                for await _ in clock.timer(interval: .seconds(4)) {
                                    index = State.generationMessages.index(after: index)
                                    await send(.internal(.updateGenerationMessage(index: index)))
                                }
                            }

                            group.addTask {
                                getOmniplayerChannel().queue(.setExpanded(false))
                            }

                            await group.waitForAll()
                        }
                    }
                }

            case .internal(.teardownCoverSheet):
                @Dependency(\.snippetPlayerClient) var snippetPlayerClient
                snippetPlayerClient.teardown()
                if state.rootTab.isOnHooksFeed == true {
                    sendHookEvent(.playCurrentHook(cause: .resume(.cover)))
                }
                return .none

            case .createTapped:
                return .none

            case .destination(.presented(.ratingTrackerState(.dismiss))):
                state.destination = nil
                return .none

            case .ratingTrackerAction(.delegate(.rateRequestTriggered)):
                state.destination = .ratingTrackerState(.init())
                return .none

            case .showBrandedAlert(let style):
                state.destination = .brandedAlert(.init(style: style))
                return .none

            case .showHooksCreate:
                state.destination = .hooksCreate(.init(me: state.$me))
                return .none

            case .showHooksCreateWithSong(let clip):
                state.destination = .hooksCreate(.init(me: state.$me, initialClip: clip))
                return .none

            case .showOrpheusCustomCreate:
                state.destination = .orpheusCustomCreate(.init(mode: .default, me: state.$me))
                return .none

            case .showOrpheusCustomCreateCover:
                #if DEBUG
                    let clip = Clip.internalMenuPlaceholderClip()
                    state.destination = .orpheusCustomCreate(.init(mode: .remix(clip), me: state.$me))
                #endif
                return .none

            case let .modalEvent(event):
                switch event {
                case .showModals(let modals):
                    state.modalsToShow = modals
                    return .none
                }

            case let .hookEvent(event):
                switch event {
                case .showHooksMoreMenu(let hook, let source):
                    state.destination = .hooksMoreMenu(.init(hook: hook, me: state.$me, source: source))
                    return .none

                case .showCreateHook(with: let clip):
                    if let clip {
                        state.pendingCreateHookClip = clip
                        state.destination = .mediaPicker(.init())
                    } else {
                        state.destination = .hooksCreate(.init(me: state.$me))
                    }
                    return .none

                case .shareHook(let hook, let source):
                    // Use the new shareHookV2 destination for proper hook sharing
                    let isCreator = hook.user != nil && hook.authorUserHandle == state.me.user.handle
                    state.destination = .shareHookV2(ShareSheetReducer<Hook>.State(item: hook, isCreator: isCreator, source: source))
                    return .none

                case .showHideCreatorAlert(let hook, let source):
                    state.moreMenuLastHook = hook
                    state.moreMenuLastSource = source
                    state.destination = nil
                    state.lastKnownAlertType = .multiButtonAlert(.preset(.hideCreatorConfirmation))
                    state.destination = .brandedAlert(.init(style: .multiButtonAlert(.v2(.custom(.init(
                        title: L10n.FeatureHooks.hideCreatorAlertTitle,
                        description: L10n.FeatureHooks.hideCreatorAlertMessage,
                        primaryButtonLabel: L10n.FeatureHooks.hideCreatorAlertConfirm,
                        secondaryButtonLabel: L10n.FeatureHooks.buttonAlertCancel
                    ))))))
                    return .none

                case .confirmHideCreator(let hook, let source):
                    // This is called when the user confirms hiding the creator
                    return .run { _ in
                        hookActionsClient.hideCreator(hook, source)
                    }

                case .pauseCurrentHook(let cause):
                    hooksPlayerClient.pauseCurrentHook(cause)
                    return .none

                case .playCurrentHook(let cause):
                    hooksPlayerClient.playCurrentHook(cause)
                    return .none

                case .showHookStatusPopup(let reasons):
                    state.destination = .brandedAlert(.init(style: .hookStatusAlert(HookStatusAlertStyle(reasons: reasons))))
                    return .none

                case .showPublishSongAlert(let hook):
                    state.lastKnownAlertType = .multiButtonAlert(.preset(.publishSongConfirmation(hook)))
                    state.destination = .brandedAlert(.init(style: .multiButtonAlert(.v2(.custom(.init(
                        title: L10n.FeatureHooksGrid.publishYourSong,
                        description: L10n.FeatureHooksGrid.publishYourSongDescription,
                        primaryButtonLabel: L10n.FeatureHooksGrid.publish,
                        secondaryButtonLabel: L10n.FeatureHooksGrid.no
                    ))))))
                    return .none

                case .hookUpdated,
                     .hookReported,
                     .hookCreated,
                     .hookDeleted,
                     .creatorHidden,
                     .showReportedHook,
                     .triggeredHookCreation,
                     .hookLiked,
                     .hookUnliked,
                     .hookDisliked,
                     .hookUndisliked,
                     .hookCommentsToggled:
                    return .none
                }

            case .showAnnouncementIfNeeded:
                /*
                 Sequence of announcements should be:
                 1. invites
                 2. create first song
                 3. modal carousel (backend modals)
                 4. hardcoded modals for v5/bluejay
                 5. video song art walkthrough
                 6. featured artist (timbaland)
                 */

                // Don't show an announcement if we've already seen one this session
                guard !state.announcementSeenThisSession else { return .none }

                if FeatureFlag.legacy.showLegacyCreateYourFirstSongModal && !state.hasSeenCreateFirstSong {
                    return .send(.showCreateFirstSongAnnouncement)
                }

                if state.shouldShowModalCarousel {
                    return .send(.showModalCarousel)
                }

                if state.shouldShowV5Announcement {
                    return .send(.showV5Announcement, animation: .default)
                }

                if !state.hasSeenVideoSongArtWalkthroughAlert,
                   FeatureFlag.promo.videoSongArtWalkthrough,
                   state.didCreateSongs
                {
                    // Only show the video song art walkthrough if the user has made songs before
                    state.$hasSeenVideoSongArtWalkthroughAlert.withLock { $0 = true }
                    return .send(.showBrandedAlert(.walkthroughAlert(.preset(.introducingVideoSongArt))))
                }

                return .none

            case .showV5Announcement:
                state.$hasSeenV5Announcement.withLock { $0 = true }
                state.announcementSeenThisSession = true
                let isPaidUser: Bool = state.billingInfo?.plan != nil
                state.destination = .v5Announcement(.init(style: isPaidUser ? .pro : .free))
                return .none

            case .showModalCarousel:
                state.announcementSeenThisSession = true
                state.destination = .modalCarousel(.init(modals: state.modalsToShow))
                return .none

            case .destination(.presented(.bluejayAnnouncement(.delegate(.openCreate)))):
                state.destination = nil
                return .run { send in
                    /// Set the model to the bluejay model
                    await sunoModelClient.setModelWithMarketingLevelUnderstanding(.bluejay)
                    await send(.createChannel(.createClip()))
                }

            case .destination(.presented(.bluejayAnnouncement(.delegate(.openSubscriptions)))):
                state.destination = .subscriptions(.init())
                return .none

            case .destination(.presented(.v5Announcement(.delegate(.openCreate)))):
                state.destination = nil
                return .run { send in
                    /// Set the model to the v5 model
                    await sunoModelClient.setModelWithMarketingLevelUnderstanding(.v5)
                    await send(.createChannel(.createClip()))
                }

            case .destination(.presented(.v5Announcement(.delegate(.openSubscriptions)))):
                state.destination = .subscriptions(.init())
                return .none

            case .destination(.presented(.modalCarousel(.delegate(.handleURL(let url))))):
                if let urlObject = URL(string: url),
                   let deeplinkIntent = DeeplinkIntent.createDeeplinkIntent(from: urlObject)
                {
                    state.destination = nil
                    return .send(.handleDeeplink(deeplinkIntent))
                }

                // If not a recognized deeplink, just dismiss the modal
                state.destination = nil
                return .none

            case .destination(.presented(.modalCarousel(.delegate(.dismissCarousel)))):
                state.destination = nil
                return .none

            case .rootTab(.discover(.showRemixAnnouncement)):
                state.destination = .remixAnnouncement(.init())
                state.announcementSeenThisSession = true
                return .none

            case .rootTab(.discover(.delegate(.handleDeeplink(let deeplinkIntent)))):
                return handleDeeplink(deeplinkIntent, state: &state)

            case .rootTab(.discover(.delegate(.handlePromoSectionDeeplink(let deeplinkIntent, let promoType, let promoId)))):
                return handleDeeplinkFromPromoSection(deeplinkIntent, promoType: promoType, promoId: promoId, state: &state)

            case .destination(.presented(.remixAnnouncement(.remixability(.optIntoRemixability)))):
                state.destination = nil
                return .send(.rootTab(.discover(.remixBanner(.delegate(.dismissTapped)))), animation: .default)

            case .destination(.presented(.remixAnnouncement(.delegate(.dismiss)))):
                state.destination = nil
                return .none

            case .showCreateFirstSongAnnouncement:
                guard !state.didCreateSongs else {
                    state.$hasSeenCreateFirstSong.withLock { $0 = true }
                    return .send(.showAnnouncementIfNeeded)
                }

                state.announcementSeenThisSession = true
                state.$hasSeenCreateFirstSong.withLock { $0 = true }
                return .send(.showBrandedAlert(.announceAlert(.preset(.createFirstSong))))

            case .showFeaturedArtistAnnouncement:
                state.$hasSeenFeaturedArtist.withLock { $0 = true }
                return .concatenate(
                    .send(.showBrandedAlert(.featuredArtist(.preset(.timbaland)))), // Show featured artist announcement
                    .send(.rootTab(.discover(.getFeaturedArtistPlaylist(State.featureArtistTimbalandPlaylistId))))
                )

            case .destination(.presented(.brandedAlert(.delegate(.didTriggerCreate)))):
                createClip(state: &state)
                return .none

            case .destination(.presented(.brandedAlert(.destination(.presented(.featuredArtist(.delegate(.tappedButton))))))):
                // Show featured artist from discover
                return .run { send in
                    try await Task.sleep(for: .seconds(0.5))
                    await send(.rootTab(.discover(.showFeaturedArtistPlaylist)))
                }

            case .destination(.presented(.blendedCreate(.delegate(.cancelOngoingVideoUploads)))):
                return .merge(
                    .send(.rootTab(.bannerAction(.dismiss)), animation: .default)
                )

            case .destination(.presented(.blendedCreate(.camera(.delegate(.submitVideo(let caption, let hCaptchaToken, let isRemix)))))):
                return .none

            case .destination(.presented(.blendedCreate(.delegate(.startedGenerating)))):
                return .run { send in
                    var index = State.generationMessages.startIndex
                    await send(.internal(.updateGenerationMessage(index: index)))

                    for await _ in clock.timer(interval: .seconds(4)) {
                        index = State.generationMessages.index(after: index)
                        await send(.internal(.updateGenerationMessage(index: index)))
                    }
                }
                .cancellable(id: CheckCompletionCancellableId(), cancelInFlight: true)

            case let .destination(.presented(.coverClip(.delegate(.generationResponse(_, .success(clips)))))),
                 let .destination(.presented(.blendedCreate(.delegate(.generationResponse(_, .success(clips)))))),
                 let .blendedResultStream(.success(clips)):
                // Poll clip until status is complete
                clipPollingClient.pollClipsForStatusComplete(clips: clips)

                return .merge(
                    .send(.getBillingInfo),
                    .run { send in
                        await withTaskCancellation(id: CheckCompletionCancellableId(), cancelInFlight: true) {
                            await withTaskGroup(of: Void.self) { group in
                                group.addTask {
                                    for await _ in clock.timer(interval: .seconds(3)) {
                                        await send(.internal(.checkCompletion(clips)), animation: .default)
                                    }
                                }
                                group.addTask {
                                    var index = State.generationMessages.startIndex
                                    await send(.internal(.updateGenerationMessage(index: index)))

                                    for await _ in clock.timer(interval: .seconds(4)) {
                                        index = State.generationMessages.index(after: index)
                                        await send(.internal(.updateGenerationMessage(index: index)))
                                    }
                                }

                                group.addTask {
                                    getOmniplayerChannel().queue(.setExpanded(false))
                                }

                                await group.waitForAll()
                            }
                        }
                    }
                )

            case .pauseAndDismissPlayer:
                playerClient.pause()
                state.playerDestination = nil
                return .none

            case .internal(.playNewClips):
                let dismissToastsEffect = Effect<Action>.run { send in
                    await send(.rootTab(.bannerAction(.dismiss)), animation: .default)
                    await send(.topToastAction(.dismiss), animation: .default)
                }

                guard let clips = state.recentGeneratedClips,
                      !clips.isEmpty
                else {
                    return dismissToastsEffect
                }

                let clip = clips[0]
                let prompt = state.savedPrompts[clip.id]

                var effects: [Effect<Action>] = []
                effects.append(dismissToastsEffect)
                state.recentGeneratedClips = nil

                // Launch omniplayer with clips
                let launchEffect = launchOmniPlayer(
                    state: &state,
                    clip: clip,
                    queue: clips,
                    isExpanded: true,
                    prompt: prompt,
                    autoStart: true,
                    context: SessionContext(source: .topToast)
                )

                // Always add the launch effect to the array of effects
                effects.append(launchEffect)

                return .concatenate(effects)

            case let .internal(.playClipsAt(clip, queue, context)):
                guard !clip.audioUrl.isEmpty else { return .none }

                // Check if we have an existing player with a matching queue
                if let playerDestination = state.playerDestination,
                   let omniPlayer = playerDestination.omniPlayer
                {
                    // Check if provided queue is different from current omniplayer queue, update if true
                    if omniPlayer.queue.map({ $0.id }).sorted() != queue.map({ $0.id.remoteId }).sorted() {
                        return launchOmniPlayer(
                            state: &state,
                            clip: clip,
                            queue: queue,
                            autoStart: true,
                            context: context
                        )
                    }
                    if let index = omniPlayer.queue.firstIndex(where: { $0.clip == clip }) {
                        return playClipAtIndexOmniPlayer(state: state, index: index)
                    }
                }

                return launchOmniPlayer(
                    state: &state,
                    clip: clip,
                    queue: queue,
                    autoStart: true,
                    context: context
                )

            case .internal(.checkCompletion(let clips)):
                let ids = clips.map { $0.id.remoteId }
                return .run { send in
                    await send(.internal(.checkCompletionResponse(Result(catching: { try await api.getFeedByIds(ids) }))))
                }

            case .internal(.checkCompletionResponse(.success(let clips))):
                // Catch pooling errors
                if let first = clips.first,
                   first.status == .error
                {
                    return .concatenate(
                        .cancel(id: CheckCompletionCancellableId()),
                        .send(.rootTab(.bannerAction(.show(type: .warning(.string(first.errorMessage ?? L10n.FeatureRoot.errorMessage)), autoDismiss: true))))
                    )
                }

                // Reverse to match order returned from API when listed in library
                let clips: [Clip] = clips.reversed().filter { $0.type != .preview }

                // Pre-fetch clip images to display on preview screen
                for clip in clips {
                    guard !clip.largeImageUrl.isEmpty,
                          let url = URL(string: clip.largeImageUrl) else { continue }
                    RemoteImagePrefetcher.shared.loadImages(urls: [url])
                }

                let totalReadyToPlay = clips
                    .filter { ($0.status == .streaming || $0.status == .complete) && !$0.largeImageUrl.isEmpty }
                    .count

                if totalReadyToPlay == clips.count {
                    return .concatenate(
                        .send(.internal(.generationComplete(clips)), animation: .default),
                        .send(.ratingTrackerAction(.create(clips.count))),
                        .send(.rootTab(.library(.silentRefresh)))
                    )
                } else {
                    return .none
                }

            case .internal(.checkCompletionResponse(.failure(let error))),
                 .blendedResultStream(.failure(let error)):
                log.telemetry.error(error)
                return .concatenate(
                    .cancel(id: CheckCompletionCancellableId()),
                    .send(.rootTab(.bannerAction(.show(type: .warning(.string(error.underlyingError)), autoDismiss: true))))
                )

            case .internal(.updateGenerationMessage(let index)):
                guard State.generationMessages.indices.contains(index) else { return .none }

                return .send(.rootTab(.bannerAction(.show(
                    type: .progress(.string(State.generationMessages[index].message),
                                    completionRatio: State.generationMessages[index].completionRatio),
                    autoDismiss: false
                ))))

            case .internal(.updateVideoProcessingMessage(let message)):
                return .send(.rootTab(.bannerAction(.show(
                    type: .progress(.string(message),
                                    completionRatio: 0.2),
                    autoDismiss: false
                ))))

            case .internal(.generationComplete(let clips)):
                state.recentGeneratedClips = clips
                if !state.rootTab.isOnLibraryScreen {
                    // Only set the badge if we're not in the Library already
                    state.$readyNewGensCount.withLock { $0 = $0 + clips.count }
                }
                let message: String
                if clips.count == 2 {
                    message = L10n.FeatureToasts.songsReady
                } else if clips.count == 1 {
                    message = L10n.FeatureToasts.songReady
                } else {
                    // Not supported, and shouldn't happen, exit
                    return .none
                }
                UINotificationFeedbackGenerator().notificationOccurred(.success)
                var effects: [Effect<Action>] = [
                    .cancel(id: CheckCompletionCancellableId()),
                    .send(.rootTab(.bannerAction(.show(
                        type: .progress(
                            .string(message),
                            completionRatio: 1.0,
                            destination: .newClipsInOmniPlayer,
                            previewImageURLs: clips.compactMap { URL(string: $0.imageUrl) }
                        ),
                        autoDismiss: false
                    )))),
                ]

                return .merge(effects)

            case .getServiceStatus:
                return .run { send in
                    await send(.internal(.serviceStatusResponse(Result(catching: { try await apiClient.getServiceStatus() }))))
                }.cancellable(id: ServiceStatusCancellableId(), cancelInFlight: true)

            case .internal(.serviceStatusResponse(.success(let serviceStatus))):
                state.serviceStatus = serviceStatus
                return .none

            case .internal(.serviceStatusResponse(.failure(let error))):
                log.telemetry.error(error)
                return .none

            case .internal(.reachabilityDidChange(let status)):
                guard let status = status else { return .send(.bottomToastAction(.dismiss)) }
                return .send(.bottomToastAction(.setConnected(status)))

            case .getModals:
                modalClient.fetchModals()
                return .none

            case .getBillingInfo:
                state.isLoadingBillingInfo = true
                return .run { send in
                    await send(.internal(.billingInfoResponse(Result(catching: { try await APIClientV2.underlying.send(Paths.billing.info.get).value }))))
                }

            case .internal(.billingInfoResponse(.success(let billingInfo))):
                state.$billingInfo.withLock { $0 = billingInfo }
                state.isLoadingBillingInfo = false

                // TODO: Remove this after SunoModelClient async method cleanup
                sunoModelClient.setUserAccess(billingInfo.sunoModelUserAccess)

                // TODO: Remove this after SunoModelClient async method cleanup
                state.$sunoModelUserAccessCategory.withLock { $0 = billingInfo.sunoModelUserAccess }

                return .run { [userId = state.me.user.id] send in
                    // Sets available models
                    await sunoModelClient.configureWithSubscriptionInfoResponse(userId: userId, response: billingInfo)
                    await send(.showAnnouncementIfNeeded)
                }

            case .internal(.billingInfoResponse(.failure(let error))):
                state.isLoadingBillingInfo = false
                log.telemetry.error(error)
                return .none

            case .deeplink(let deeplinkAction):
                return handleDeeplinkAction(deeplinkAction, state: &state)

            case .billingChannel(.billingInfoUpdated(let billingInfo)):
                state.$billingInfo.withLock { $0 = billingInfo }
                return .run { [userId = state.me.user.id] _ in
                    // Sets available models
                    await sunoModelClient.configureWithSubscriptionInfoResponse(userId: userId, response: billingInfo)
                }

            case .deeplinkEvent(.handleDeeplink(let intent)):
                return .send(.handleDeeplink(intent))

            case .destination(.presented(.subscriptions(.billingInfoResult(.success(let updatedBillingInfo))))):
                let previousBillingInfo = state.billingInfo
                state.$billingInfo.withLock { $0 = updatedBillingInfo }
                if previousBillingInfo?.subscriptionPlatform == nil,
                   updatedBillingInfo.subscriptionPlatform == "apple"
                { // only show the alert for upgrades
                    state.didUpgradeAndShouldRefresh = true
                    return .none
                } else {
                    state.didUpgradeAndShouldRefresh = false
                    return .none
                }

            case .destination(.presented(.subscriptions(.dismiss))):
                return .run { send in
                    try await Task.sleep(for: .seconds(0.5))
                    await send(.checkForUpgrades)
                }

            case .handleDeeplink(let route):
                return handleDeeplink(route, state: &state)

            case .checkForUpgrades:
                guard state.didUpgradeAndShouldRefresh else { return .none }
                state.didUpgradeAndShouldRefresh = false

                // Check for v5 announcement first (newest model)
                if state.shouldShowV5Announcement {
                    return .send(.showV5Announcement, animation: .default)
                }

                return .none

            case .destination(.presented(.hooksMoreMenu(.delegate(.deleteHookTapped(let hook))))):
                if !state.hasConfirmedDeleteHook {
                    state.moreMenuLastHook = hook
                    state.destination = .none
                    state.lastKnownAlertType = .multiButtonAlert(.preset(.areYouSureYouWantToDeleteHook))
                    state.destination = .brandedAlert(.init(style: .multiButtonAlert(.preset(.areYouSureYouWantToDeleteHook))))
                    return .none
                }
                return .none

            case .destination(.presented(.brandedAlert(.destination(.presented(.multiButtonAlert(.delegate(.tappedPrimaryButton))))))):
                if state.lastKnownAlertType == .multiButtonAlert(.preset(.areYouSureYouWantToDeleteHook)) {
                    state.hasConfirmedDeleteHook = false
                    state.lastKnownAlertType = nil
                    state.destination = nil
                    guard let hook = state.moreMenuLastHook else {
                        return .none
                    }

                    state.moreMenuLastHook = nil
                    return .run { _ in
                        hookActionsClient.deleteHook(hook)
                    }

                } else if state.lastKnownAlertType == .multiButtonAlert(.preset(.hideCreatorConfirmation)) {
                    state.lastKnownAlertType = nil
                    state.destination = nil
                    guard let hook = state.moreMenuLastHook else {
                        return .none
                    }

                    let source = state.moreMenuLastSource
                    state.moreMenuLastHook = nil
                    state.moreMenuLastSource = nil
                    return .run { _ in
                        sendHookEvent(.confirmHideCreator(hook: hook, source: source))
                    }
                } else if case .multiButtonAlert(.v1(.preset(.publishSongConfirmation(let hook)))) = state.lastKnownAlertType {
                    state.lastKnownAlertType = nil
                    state.destination = nil
                    hookActionsClient.confirmPublishClip(hook)
                    return .none
                } else {
                    return .none
                }

            case .destination(.presented(.brandedAlert(.destination(.presented(.multiButtonAlert(.delegate(.tappedSecondaryButton))))))):
                if state.lastKnownAlertType == .multiButtonAlert(.preset(.areYouSureYouWantToDeleteHook)) {
                    guard let hook = state.moreMenuLastHook else {
                        return .none
                    }
                    state.destination = nil
                    state.lastKnownAlertType = nil
                    state.hasConfirmedDeleteHook = false
                    return .run { _ in
                        try await Task.sleep(for: .seconds(0.4))
                        sendHookEvent(.showHooksMoreMenu(hook, source: nil))
                    }
                } else if state.lastKnownAlertType == .multiButtonAlert(.preset(.hideCreatorConfirmation)) {
                    state.destination = nil
                    state.lastKnownAlertType = nil
                    state.moreMenuLastHook = nil
                    state.moreMenuLastSource = nil
                    return .none
                } else if case .multiButtonAlert(.v1(.preset(.publishSongConfirmation))) = state.lastKnownAlertType {
                    state.destination = nil
                    state.lastKnownAlertType = nil
                    return .none
                }
                return .none

            case .destination(.presented(.hooksMoreMenu(.delegate(.openOmniPlayerFromHooksFeed(let clip))))):
                guard state.rootTab.selectedTab == .hooks else { return .none }

                state.destination = nil

                return .run { send in
                    try? await Task.sleep(for: .seconds(0.5))
                    await send(.rootTab(.hooks(.hooksFeed(.delegate(.goToFullSongTapped(clip))))))
                }

            case .destination(.presented(.hooksMoreMenu(.delegate(.queueDestination(let clip, let destination))))):
                state.destination = nil
                switch destination {
                case .addToPlaylist:
                    state.destination = .selectPlaylist(.init(clip: clip))
                    return .none

                case .shareSheet:
                    state.destination = .shareV2(
                        ShareSheetState(item: clip)
                    )
                    return .none

                case .remixActions:
                    state.destination = .remixActions(.init(
                        me: state.$me,
                        clip: clip
                    ))
                    return .none
                }

            case .destination(.presented(.blendedCreate(.delegate(.dismiss)))):
                state.destination = nil
                return .send(.rootTab(.delegate(.resumeHookIfNeeded(.createSong))))

            case .destination(.presented(.hooksCreate(.delegate(.dismiss(let resumeHook, let resumeMusic))))):
                state.destination = nil
                state.$isHooksCreateVisible.withLock { $0 = false }
                if resumeHook {
                    return .send(.rootTab(.delegate(.resumeHookIfNeeded(.createHook))))
                } else if resumeMusic {
                    @Dependency(OmniPlayerClient.self) var omniplayerClient
                    omniplayerClient.playCurrentClip()
                }
                return .none

            case .checkCreatedSongs:
                guard !state.didUpgradeAndShouldRefresh else { return .none }
                return .run { send in
                    await send(.internal(.checkCreatedSongsResponse(Result(catching: { try await api.getFeedV2(0, nil, nil, nil, nil, nil, nil).totalResults > 0 }))))
                }

            case .internal(.checkCreatedSongsResponse(.success(let hasSongs))):
                state.didCreateSongs = hasSongs
                return .send(.showAnnouncementIfNeeded)

            case .internal(.checkCreatedSongsResponse(.failure(let error))):
                log.telemetry.error(error)
                return .send(.showAnnouncementIfNeeded)

            case .toastEvent(let toastEvent):
                switch toastEvent {
                case .show(let toast):
                    switch toast.position {
                    case .top:
                        return .send(.topToastAction(.show(toast)))
                    case .bottom:
                        return .send(.bottomToastAction(.show(toast)))
                    }

                case .dismiss(let position):
                    switch position {
                    case .top:
                        return .send(.topToastAction(.dismiss))
                    case .bottom:
                        return .send(.bottomToastAction(.dismiss))
                    }

                case .undo:
                    guard case let .deleteClip(clip) = state.recentAction else { return .none }
                    state.recentAction = nil
                    sendClipEvent(.undoDeleteClip(clip))
                    return .none

                default:
                    break
                }
                return .none

            case .destination(.presented(.hooksMoreMenu(.delegate(.reportInappropriateTapped(let hook))))):
                state.destination = nil
                state.destination = .reportHookInappropriate(.init(hook: hook))
                return .none

            case let .destination(.presented(.songActions(.delegate(.deleteClip(clip))))):
                state.recentAction = .deleteClip(clip)
                state.destination = nil
                return .run { _ in
                    omniplayerClient.removeClipFromQueue(clip.id)
                    songActionsClient.deleteClip(clip, true)
                }

            case let .undo(action):
                switch action {
                case let .deleteClip(clip):
                    sendClipEvent(.undoDeleteClip(clip))
                    return .none
                }

            case .destination(.presented(.songActions(.destination(.presented(.replaceSongArt(.delegate(.songArtUpdateSuccess(let clip)))))))):
                sendClipEvent(.updateClip(clip))
                guard let coverUrl = clip.videoCoverUrl,
                      let url = URL(string: coverUrl) else { return .none }
                // Load and play the new video cover
                return .run { _ in
                    _ = await videoCoverClient.replaceCurrentItem(url)
                    videoCoverClient.play()
                }

            case .destination(.presented(.songActions(.delegate(.queueDestination(let clip, let destination))))):
                return .send(.internal(.queueDestination(clip: clip, destination: destination)))

            case .internal(.queueDestination(let clip, let destination)):
                state.destination = nil
                switch destination {
                case .addToPlaylist:
                    state.destination = .selectPlaylist(.init(clip: clip))
                    return .none

                case .shareSheet:
                    state.destination = .shareV2(
                        ShareSheetState(item: clip)
                    )
                }

                return .none

            case .internal(.dismiss):
                state.destination = nil
                return .none

            case .brandedAlert,
                 .bottomToastAction,
                 .topToastAction,
                 .omniPlayer,
                 .binding,
                 .destination,
                 .ratingTrackerAction,
                 .clipListItemDelegate,
                 .playerDestination,
                 .rootTab,
                 .blendedResultStream,
                 .delegate,
                 .clipEvents:
                // Catch-all
                return .none

            case .deleteClip:
                return .none
            }
        }
        .ifLet(\.$destination, action: \.destination)
        .ifLet(\.$playerDestination, action: \.playerDestination)
        Analytics()
    }
}

public extension RootCoordinatorV1 {
    func createClip(
        _ reusePrompt: Prompt? = nil,
        didUseAppShortcut: Bool = false,
        isRemix: Bool? = nil,
        state: inout State
    ) {
        if let serviceStatus = state.serviceStatus,
           serviceStatus.status == .maintenance,
           serviceStatus.mode == .all || (serviceStatus.mode == .pro && state.me.roles[.sub] == false)
        {
            state.destination = .alert(.init(
                title: { TextState(serviceStatus.status == .maintenance ? L10n.FeatureRoot.maintenance : L10n.FeatureRoot.scheduledMaintenance) },
                actions: {
                    ButtonState(action: .dismiss) { TextState(L10n.FeatureRoot.gotIt) }
                },
                message: { TextState(serviceStatus.message) }
            ))
        } else if let billingInfo = state.billingInfo,
                  billingInfo.totalCreditsLeft < SubscriptionInfoResponse.generateSongCost
        {
            state.destination = .creditsPopup(.init(me: state.$me, billingInfo: billingInfo))
        } else {
            ParameterStores.Orpheus.markExposed(flag: \.$orpheusDogfooding)
            if FeatureFlag.orpheus.orpheusDogfooding {
                if case .chatCreate = state.destination { return }
                state.destination = .chatCreate(.init(me: state.$me))
            } else {
                if case .blendedCreate = state.destination { return }
                state.destination = .blendedCreate(.init(
                    me: state.$me,
                    reusePrompt: reusePrompt,
                    billingInfo: state.$billingInfo,
                    didUseAppShortcut: didUseAppShortcut,
                    isRemix: isRemix
                ))
            }
        }
    }
}

public struct RootCoordinatorV1Screen: View {
    @Bindable var store: StoreOf<RootCoordinatorV1>
    @Namespace var namespace
    @Environment(\.scenePhase) var scenePhase

    public init(store: StoreOf<RootCoordinatorV1>) {
        self.store = store
    }

    public var body: some View {
        overlayPresentations
    }

    private var sheetPresentations: some View {
        basicSheetPresentations
    }

    // Primary sheets - core functionality
    private var basicSheetPresentations: some View {
        navigationV2
            .sheet(item: $store.scope(state: \.destination?.creditsPopup, action: \.destination.creditsPopup)) { store in
                CreditsPopupScreen(store: store)
                    .selfSizingPresentation()
            }
            .sheet(item: $store.scope(state: \.destination?.songActions, action: \.destination.songActions)) { store in
                SongActionsMenu(store: store)
            }
            .sheet(item: $store.scope(state: \.destination?.selectPlaylist, action: \.destination.selectPlaylist)) { store in
                SelectPlaylistScreen(store: store)
                    .presentationDetents([.large])
            }
            .sheet(item: $store.scope(state: \.destination?.mediaPicker, action: \.destination.mediaPicker)) { store in
                RootMediaPickerView(store: store)
            }
            .sheet(item: $store.scope(state: \.destination?.orpheusCustomCreate, action: \.destination.orpheusCustomCreate)) { store in
                OrpheusCustomCreateSheetView(store: store, isNestedInCustomSheet: false)
            }
            .modifier(SharingSheetPresentations(store: store))
            .modifier(HooksSheetPresentations(store: store))
    }

    private var fullScreenCoverPresentations: some View {
        sheetPresentations
            .fullScreenCover(item: $store.scope(state: \.destination?.blendedCreate, action: \.destination.blendedCreate)) { store in
                BlendedCreateViewV4(store: store)
            }
            .fullScreenCover(item: $store.scope(state: \.destination?.chatCreate, action: \.destination.chatCreate)) { store in
                ChatView(store: store)
            }
            .fullScreenCover(item: $store.scope(state: \.destination?.hooksCreate, action: \.destination.hooksCreate)) { store in
                HooksCreateView(store: store)
            }
            .fullScreenCover(item: $store.scope(state: \.destination?.subscriptions, action: \.destination.subscriptions)) { paywallStore in
                NavigationStack {
                    PaywallScreenV1(store: paywallStore)
                        .toolbar {
                            ToolbarItem(placement: .navigationBarLeading) {
                                ToolbarButton(.close, background: Material.ultraThin) {
                                    paywallStore.send(.dismiss)
                                }
                            }
                        }
                }
            }
            .modifier(if: FeatureFlag.legacy.remixV2) { view in
                view
                    .sheet(
                        item: $store.scope(state: \.destination?.coverClip, action: \.destination.coverClip),
                        onDismiss: {
                            /// Teardown needs to happen here in the SwiftUI presentation context
                            /// Ideally `RootCoordinatorV1` isn't privy to the inner workings of `CoverClipSheet`, so long-term it may be pertinent to build infra around sheet dismissal hooks w/ some UIKit constructs i.e. NotifcationCenter or PresentationController handlers
                            store.send(.internal(.teardownCoverSheet))
                        }
                    ) { store in
                        CoverClipSheet(store: store)
                    }
            }
    }

    private var overlayPresentations: some View {
        fullScreenCoverPresentations
            .overlay {
                if let store = store.scope(state: \.destination?.brandedAlert, action: \.destination.brandedAlert) {
                    BrandedAlertView(store)
                }
            }
    }

    private var navigationV2: some View {
        navigationV2Root
            .onAppear {
                store.send(.onAppear)
            }
            .task {
                store.send(.task)
            }
            .onChange(of: scenePhase) { _, phase in
                guard phase == .active else { return }
                store.send(.getServiceStatus)
            }
            .overlay(alignment: .top) {
                if let ratingStore = store.scope(state: \.destination?.ratingTrackerState, action: \.destination.ratingTrackerState) {
                    RatingTrackerScreen(store: ratingStore)
                        .frame(maxWidth: .infinity, maxHeight: .infinity)
                        .background {
                            Color.black.opacity(0.6)
                                .ignoresSafeArea()
                        }
                }
            }
            .overlay {
                if let store = store.scope(state: \.destination?.remixAnnouncement, action: \.destination.remixAnnouncement) {
                    RemixAnnouncementView(store: store)
                }
            }
            .overlay {
                if let store = store.scope(state: \.destination?.bluejayAnnouncement, action: \.destination.bluejayAnnouncement) {
                    BluejayAnnouncementView(store: store)
                }
            }
            .overlay {
                if let store = store.scope(state: \.destination?.v5Announcement, action: \.destination.v5Announcement) {
                    V5AnnouncementView(store: store)
                }
            }
            .overlay {
                if let store = store.scope(state: \.destination?.modalCarousel, action: \.destination.modalCarousel) {
                    ModalCarouselView(store: store)
                }
            }
            .alert($store.scope(state: \.destination?.alert, action: \.destination.alert))
            .modifier(EditClipPresentation(store: store.scope(state: \.destination?.editClip, action: \.destination.editClip)))
    }

    private var navigationV2Root: some View {
        ZStack(alignment: .bottom) {
            RootTabView(
                store: store.scope(state: \.rootTab, action: \.rootTab),
                omniPlayerStore: store.scope(state: \.playerDestination?.omniPlayer, action: \.playerDestination.omniPlayer)
            )
        }
        .overlay(alignment: .bottom) {
            ToastView(store: store.scope(state: \.bottomToastState, action: \.bottomToastAction))
                .padding(.bottom, store.bottomToastVerticalOffset)
        }
        .overlay(alignment: .top) {
            ToastView(store: store.scope(state: \.topToastState, action: \.topToastAction))
        }
    }
}

// MARK: - Sheet Presentation Helpers

struct SharingSheetPresentations: ViewModifier {
    @Bindable var store: StoreOf<RootCoordinatorV1>

    func body(content: Content) -> some View {
        content
            .sheet(item: $store.scope(state: \.destination?.shareV2, action: \.destination.shareV2)) { shareV2 in
                ShareSheet(store: shareV2)
            }
            .sheet(item: $store.scope(state: \.destination?.shareHookV2, action: \.destination.shareHookV2)) { shareHookV2 in
                ShareSheet(store: shareHookV2)
            }
    }
}

struct HooksSheetPresentations: ViewModifier {
    @Bindable var store: StoreOf<RootCoordinatorV1>

    func body(content: Content) -> some View {
        content
            .sheet(item: $store.scope(state: \.destination?.hooksMoreMenu, action: \.destination.hooksMoreMenu)) { store in
                HookActionsMenu(store: store)
            }
            .sheet(item: $store.scope(state: \.destination?.remixActions, action: \.destination.remixActions)) { store in
                if FeatureFlag.legacy.remixV2 {
                    RemixActionsSheetV2(store: store)
                } else {
                    RemixActionsSheetRedesign(store: store)
                }
            }
            .sheet(item: $store.scope(state: \.destination?.reportHookInappropriate, action: \.destination.reportHookInappropriate)) { store in
                ReportHookInappropriateSheet(store: store)
            }
    }
}
