import AnalyticsClient
import APIClient
import AppSessionCountClient
import AppTrackingTransparency
import ATTClient
import AttributionAndDeeplinkingClient
import BrazeClient
import ClerkClient
import Combine
import ComponentLibrary
import ComposableArchitecture
import DebugFeatureClient
import DeeplinkIntents
import FeatureBanner
import FeatureOnboarding
import FeatureOnboardingModels
import FeatureRoot
import FirebaseClient
import FirebaseCore
import FirebaseAppDistributionClient
import InAppNotificationClient
import InstanceIDClient
import Localization
import Network
import PaywallClient
import StatsigClient
import SunoModelClient
import SwiftUI
import TabBarUtilities
import UserEventBusClient
import UserNotificationsClient
import UserSessionTransitionClient
import Utilities

// swiftlint:disable file_length

enum AppError: LocalizedError {
    case invalidToken
    case promoCodeGenerationError(String)
    case promoCodeGenerationFailed

    var errorDescription: String? {
        switch self {
        case .invalidToken: "Logging out due to an invalid jwt token."
        case .promoCodeGenerationError(let errorMessage): "Error generating a promo code: \(errorMessage)"
        case .promoCodeGenerationFailed: "Tried and failed to generate a promo code for the user: User still missing a promo code or promo code metadata"
        }
    }
}

@Reducer
public struct AppCoordinator {
    @ObservableState
    public struct State: Equatable {
        @ObservationStateIgnored @ObservedBox public var appDelegate = AppDelegateReducer.State()
        @ObservationStateIgnored @ObservedBox var banner = BannerReducer.State()

        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),
        ]

        /// On first login, we should present the create form -- we can track if it's first login if the update username step was presented
        var isFTUX = false
        var isNetworkAvailable = true
        /// `.userNotifications(.didReceiveResponse)` gets called before `.onAppear` when the app launches from the suspended state via a push notication, so we need to store it to process it properly afterwards.
        var launchDeeplinkIntent: DeeplinkIntent?

        /// Used to cache `Me` to speed up going to the RootCoordinator on a cold launch. Without this we need to wait for `~0.5s * 2` to move off the splash screen
        @Shared(.fileStorage(FilePathKeys.ApplicationSupport.meOnLastAppSession.url())) var meFromPreviousAppSession: Me? = .none

        /// Used by `userSessionChanged(_:state:)`, so we don't replay sign in actions on duplicate calls
        var previousMe: Me? {
            didSet {
                /// Leaving assumeIsolated in, since `withLock` is scary. especially on the main launch path
                MainActor.assumeIsolated {
                    $meFromPreviousAppSession.withLock { $0 = previousMe }
                }
            }
        }

        @Presents public var destination: Destination.State?

        public init() {}
    }

    @Reducer(state: .equatable)
    public enum Destination {
        case notificationPrePrompt
        case offline(Offline)
        case welcome(WelcomeCoordinator)
        case loggedInV1(RootCoordinatorV1)
        case forceUpdate(ForceUpdate)
    }

    public enum Action {
        @CasePathable
        @dynamicMemberLookup
        public enum Internal {
            case updateRequired
            case showUpdateWarning
            case reachabilityDidChange(Bool)
            case attAuthorizationStatus(ATTrackingManager.AuthorizationStatus)
            case didFetchMe(Me)

            case debugShowNotificationScreenTapped
        }

        public enum AppRootViewAction: Equatable {
            case cached(FromCache)
            case network(NetworkOnLoad)
            case userAction(UserAction)
            case debug(DebugAction)
            case delegate(Delegate)

            public enum FromCache: Equatable {
                case authedUser(Me, isFTUX: Bool)
            }

            public enum UserAction: Equatable {
                case performedAuth(Me, isFTUX: Bool)
                case tappedLogout
                case goToWelcome
                case completedOnboarding(OnboardingCompletedResult)
            }

            public enum NetworkOnLoad: Equatable {
                case fetched(Me, isFTUX: Bool)
                case failedToFetch

                case forceUpdate
            }

            public enum DebugAction: Equatable {
                case showOnboarding(Me)
                case showMadlibsOnboarding(Me)
                case showHooksCreate
                case showNotificationQuestion
                case showOrpheusCustomCreate
                case showOrpheusCustomCreateCover
            }

            public enum Delegate: Equatable {
                case didGoToRootView(Me)

                case showNativeNotificationPrompt
                case didFinishNotificationPrePrompt
            }
        }

        case appDelegate(AppDelegateReducer.Action)
        case banner(BannerReducer.Action)
        case rootView(AppRootViewAction)

        case `internal`(Internal)

        case onAppear
        case task
        case pauseAndSignOut(Error?)
        case signOut(Error?)
        case clearBanner

        case userEvent(UserEventBusClient.UserEvent)
        case attributionAndDeeplinkingEvent(AttributionAndDeeplinkingClient.DelegateEvent)

        case destination(PresentationAction<Destination.Action>)
    }

    @Dependency(ClerkClient.self) var clerk
    @Dependency(APIClient.self) var apiClient
    @Dependency(APIClientV2.self) var api
    @Dependency(\.telemetryClient) var telemetry
    @Dependency(NetworkMonitor.self) var networkMonitor
    @Dependency(PaywallClient.self) var paywallClient
    @Dependency(\.openURL) var openURL
    @Dependency(AnalyticsClient.self) var analyticsClient
    @Dependency(StatsigClient.self) var statsigClient
    @Dependency(SunoModelClient.self) var sunoModelClient
    @Dependency(InAppNotificationClient.self) var inAppNotificationClient
    @Dependency(AttributionAndDeeplinkingClient.self) var attributionAndDeeplinkingClient
    @Dependency(BrazeClient.self) var brazeClient
    @Dependency(\.userEventBus.stream) var userEventStream
    @Dependency(\.continuousClock) private var clock
    @Dependency(ATTClient.self) private var attClient
    @Dependency(\.firebaseClient) var firebase
    @Dependency(UserNotificationClient.self) var userNotificationClient
    @Dependency(VideoCoverClient.self) var videoCoverClient
    @Dependency(\.appSessionCountClient) var appSessionCountClient
    @Dependency(\.userSessionTransitionClient) var userSessionTransitionClient
    @Dependency(InstanceIDClient.self) var instanceIdClient
    @Dependency(FirebaseAppDistributionClient.self) var appDistributionClient

    public init() {
        networkMonitor.start()
    }

    public var body: some ReducerOf<Self> {
        Scope(state: \.appDelegate, action: \.appDelegate) {
            AppDelegateReducer()
        }
        Scope(state: \.banner, action: \.banner) {
            BannerReducer()
        }
        Reduce<State, Action> { state, action in
            struct OnAppearCancellable: Hashable {}
            struct UserEventStreamCancellable: Hashable {}
            struct AttributionAndDeeplinkingCancellable: Hashable {}
            struct CheckCompletionCancellableId: Hashable {}

            switch action {
            case let .appDelegate(.userNotifications(.didReceiveResponse(response, completionHandler))):
                var deeplinkIntent: DeeplinkIntent?
                let userInfo = response.notification.request.content.userInfo
                /// `"url"` is what we expect to receive from Klaviyo and other notification sources (Klaviyo has been removed, leaving in `url` handling for "other notification sources")
                /// `"ab_uri"` refers to "AppBoy URI", which is Braze's URI format.
                /// This is the least stable part of the Braze integration, so it might be worth looking into using their recommended automated flow for typesafety
                /// https://www.braze.com/docs/developer_guide/platforms/swift/push_notifications/?tab=automatic
                let urlString: String? = userInfo["url"] as? String ?? userInfo["ab_uri"] as? String
                if let urlString, let url = URL(string: urlString), let parsed = DeeplinkIntent(url: url) {
                    // If the user is already logged in, handle the deeplink
                    if case .loggedInV1 = state.destination {
                        deeplinkIntent = parsed
                    } else if state.destination == nil {
                        // state.destination is only nil right after the app launches but before `.onAppear` finishes
                        // In this case, store the deeplink to be handled in `.onAppear`
                        state.launchDeeplinkIntent = parsed
                    }
                } else {
                    log.telemetry.assertionFailure("couldn't parse URL \(urlString ?? "nil")")
                }

                return .run { [deeplinkIntent] send in
                    await brazeClient.handleUserNotification(response, completionHandler)

                    guard let deeplinkIntent else { return }
                    await send(.destination(.presented(.loggedInV1(.handleDeeplink(deeplinkIntent)))))
                }

            case let .appDelegate(.userNotifications(.willPresentNotification(notification, completionHandler: completionHandler))):
                return .run { _ in
                    await brazeClient.handleForegroundNotification(notification, completionHandler)
                }

            case .appDelegate(.delegate(.completedNotificationRegistration(let granted))):
                /// If we're logged out, have the onboarding workflow handle this notification registration
                if case .welcome = state.destination {
                    // Welcome coordinator doesn't handle notification registration directly
                    // The notification registration will be handled by the child onboarding coordinators
                    return .none
                } else {
                    /// If we're logged in, prompt for ATT right after notifications registration
                    /// This guarantees that the ATT prompt will show right after the notifications prompt—in both onboarding and after log-in—and that we fetch ATT at startup along with notifications registration.
                    /// Debounce for `0.5` seconds to avoid system auth collision
                    struct ATTAuthorizationCancellable: Hashable {}
                    return getAttAuthorizationIfEnabled(state: &state)
                        .debounce(id: ATTAuthorizationCancellable(), for: 0.5, scheduler: DispatchQueue.main)
                }

            case .appDelegate(.delegate(.hasAlreadyCompletedNotificationRegistration)):
                /// If we're logged out, have the onboarding workflow handle this notification registration
                if case .welcome = state.destination {
                    // Welcome coordinator doesn't handle notification registration directly
                    // The notification registration will be handled by the child onboarding coordinators
                    return .none
                } else {
                    /// If we're logged in, prompt for ATT right after notifications registration
                    /// This guarantees that we fetch ATT at startup and after login along with notifications registration.
                    /// No need to debounce in this case, as it's implied that we didn't show the notifications prompt.
                    return getAttAuthorizationIfEnabled(state: &state)
                }

            case .appDelegate(.onOpenURL(let url)):
                attributionAndDeeplinkingClient.handleOpenUrl(url, [:])

                guard let parsedDeeplink = DeeplinkIntent(url: url) else {
                    return .none
                }

                switch state.destination {
                case .loggedInV1:
                    return .send(.destination(.presented(.loggedInV1(.handleDeeplink(parsedDeeplink)))))
                default:
                    state.launchDeeplinkIntent = parsedDeeplink
                    return .none
                }

            case .attributionAndDeeplinkingEvent(.didResolveDeeplink(let route)):
                if case .loggedInV1 = state.destination {
                    return .send(.destination(.presented(.loggedInV1(.handleDeeplink(route)))))
                } else {
                    state.launchDeeplinkIntent = route
                    return .none
                }

            case .onAppear:
                return .merge(
                    .concatenate(
                        /// 2025-05: temporarily disabling using the `me` from the previous app session to speed up showing a main view
                        /// https://suno-main.slack.com/archives/C08GSK94AQK/p1744848646539589 for context
//                        state.meFromPreviousAppSession.map { userSessionChanged($0, state: &state) } ?? .none,
//                        state.meFromPreviousAppSession.map { .send(.rootView(.cached(.authedUser($0, isFTUX: state.isFTUX)))) } ?? .none,
                        .run(operation: { @MainActor [isFTUX = state.isFTUX /* , meFromPrevious = state.previousMe */ ] send in
                            // Critical SDKs are now initialized before UI is shown, so we can proceed directly

                            let isLoggedIn = if firebase.isPerformanceReady() {
                                try await telemetry.withSpan(.clerkLoggedInCheck) { try await clerk.isLoggedIn() }
                            } else {
                                try await clerk.isLoggedIn()
                            }

                            if isLoggedIn {
                                let me = if firebase.isPerformanceReady() {
                                    try await telemetry.withSpan(.fetchMeOnAppOpen) { try await api.getMe() }
                                } else {
                                    try await api.getMe()
                                }
//                                guard me != meFromPrevious else { return }
                                send(.internal(.didFetchMe(me)))

                                send(.rootView(.network(.fetched(me, isFTUX: isFTUX))))
                            } else {
                                send(.rootView(.network(.failedToFetch)))
                            }
                        }, catch: { error, send in
                            log.telemetry.error(error)
                            await send(.rootView(.network(.failedToFetch)))
                        }).cancellable(id: OnAppearCancellable())
                    ),
                    .run { send in
                        log.debug("Checking manifest")
                        let manifest = try await apiClient.manifest()

                        if let info = Bundle.main.infoDictionary,
                           let bundleVersion = info["CFBundleShortVersionString"] as? String,
                           let forceVersion = manifest.forceVersion,
                           bundleVersion.isVersion(lessThan: forceVersion)
                        {
                            print("App Version is older than the force version")
                            await send(.internal(.updateRequired))
                        }

                        if let info = Bundle.main.infoDictionary,
                           let bundleVersion = info["CFBundleShortVersionString"] as? String,
                           let warnVersion = manifest.warnVersion,
                           bundleVersion.isVersion(lessThanOrEqualTo: warnVersion)
                        {
                            print("App Version is older than the warn version")
                            await send(.internal(.showUpdateWarning))
                        }
                    } catch: { error, _ in
                        log.telemetry.error(error)
                    },
                    .run { send in
                        let monitor = NWPathMonitor()
                        for await path in monitor {
                            await send(.internal(.reachabilityDidChange(path.status == .satisfied)))
                        }
                    }.cancellable(id: OnAppearCancellable())
                )

            case .task:
                videoCoverClient.setup()
                return .merge(
                    .stream(
                        userEventStream(),
                        send: Action.userEvent,
                        cancellableId: UserEventStreamCancellable()
                    ),
                    .stream(
                        attributionAndDeeplinkingClient.delegate(),
                        send: Action.attributionAndDeeplinkingEvent,
                        cancellableId: AttributionAndDeeplinkingCancellable()
                    ),
                    .run { send in
                        guard appDistributionClient.isAvailable() else { return }
                        await appDistributionClient.ensureTesterIsSignedIn()

                        do {
                            let status = try await appDistributionClient.checkForUpdate()
                            switch status {
                            case .none:
                                break
                            case .available:
                                await send(.internal(.showUpdateWarning))
                            }
                        } catch {
                            log.error("Failed to check for update: \(error)")
                        }
                    }
                )

            case .destination(.presented(.welcome(.onboardingV1(.getMeResponse(.success))))):
                guard let pendingDeeplinkIntent = state.launchDeeplinkIntent,
                      case let .song(_, sh) = pendingDeeplinkIntent,
                      let sh else { return .none }
                /// When onboarding is able to get us an authenticated user, we want to immediately attribute that share
                return .run(
                    operation: { _ in try await api.postShareAttribute(.init(shareId: sh, source: "ios")) },
                    catch: { error, _ in log.telemetry.error(error) }
                )

            case .rootView(.delegate(.didGoToRootView(let me))):
                // If we have a stored launch deeplink, store it in context and
                // clear it from State
                let launchDeeplinkIntent = state.launchDeeplinkIntent
                state.launchDeeplinkIntent = nil

                if state.isFTUX {
                    analyticsClient.track(.onboardingAction(.userSignedUp))
                    attributionAndDeeplinkingClient.track(.signUp(sunoUserId: me.user.id))
                }
                /// `userLoggedIn` is called for both sign-up and log-in
                analyticsClient.track(.onboardingAction(.userLoggedIn))

                return .merge(
                    .run { send in
                        // Navigate to any stored deeplink, which covers cases in which the user needs to log in again, or when the user is already logged in
                        if let launchDeeplinkIntent {
                            await send(.destination(.presented(.loggedInV1(.handleDeeplink(launchDeeplinkIntent)))))
                        }
                    },
                    .run { send in
                        await send(.clearBanner)
                    },
                    .run { send in
                        try await Task.sleep(for: .seconds(1))
                        await send(.appDelegate(.registerForRemoteNotifications))
                    },
                    .run { send in
                        for await _ in apiClient.invalidToken() {
                            log.telemetry.error(AppError.invalidToken, message: "APIClientV1")
                            await send(.pauseAndSignOut(nil))
                        }
                    },
                    .run { send in
                        for await _ in api.invalidToken() {
                            log.telemetry.error(AppError.invalidToken, message: "APIClientV2")
                            await send(.pauseAndSignOut(nil))
                        }
                    }
                )

            case .rootView(.delegate(.didFinishNotificationPrePrompt)):
                /// In debug, we allow navigating to the notification question from being logged in. So we want to take you back to being logged in once you're done with the demo
                /// In prod, we should only show this on the very first launch. so the user should be nil & we take you to welcome
                if let previousMe = state.previousMe {
                    return .send(.rootView(.network(.fetched(previousMe, isFTUX: state.isFTUX))))
                } else {
                    return .send(.rootView(.userAction(.goToWelcome)))
                }

            case .rootView(.delegate(.showNativeNotificationPrompt)):
                return .run { _ in
                    // TODO: add analytics
                    _ = try await userNotificationClient.requestAuthorization([.alert, .badge, .sound])
                }

            case .internal(.didFetchMe(let me)):
                state.previousMe = me
                return userSessionChanged(me, state: &state)

            case .clearBanner:
                return .concatenate(
                    .send(.banner(.dismiss), animation: .default),
                    .cancel(id: CheckCompletionCancellableId())
                )

            case .destination(.presented(.loggedInV1(.delegate(.triggerPushNotificationRequest)))):
                return .send(.appDelegate(.registerForRemoteNotifications))

            case .destination(.presented(.welcome(.onboardingV1(.delegate(.signOut))))):
                /*
                 Same as below but just moving to keep
                 event bus logic separate to reducer delegate logic
                 */
                return .merge(
                    .send(.destination(.presented(.loggedInV1(.pauseAndDismissPlayer)))),
                    .send(.signOut(nil))
                )

            case .userEvent(.signOut):
                return .merge(
                    // Send a message to pause and dismiss player
                    .send(.destination(.presented(.loggedInV1(.pauseAndDismissPlayer)))),
                    .send(.signOut(nil))
                )

            case .destination(.presented(.welcome(.onboardingV1(.path(.element(_, .usernameEntry(.delegate(.updateUsernameComplete)))))))):
                state.isFTUX = true
                return .none

            // Delegate actions are handled in handleWelcomeDelegateAction

            case .pauseAndSignOut(let error):
                return .merge(
                    // Send a message to pause and dismiss player
                    .send(.destination(.presented(.loggedInV1(.pauseAndDismissPlayer)))),
                    .send(.signOut(error))
                )

            case .signOut(let error):
                if let error {
                    log.telemetry.error(error)
                }
                guard state.isNetworkAvailable else {
                    print("Tried to sign out the user but is offline. should show an error message?")
                    return .none
                }

                return .concatenate(
                    .send(.banner(.dismiss), animation: .default),
                    MainActor.assumeIsolated { userSessionChanged(.none, state: &state) },
                    .send(.rootView(.userAction(.tappedLogout))),
                    .cancel(id: CheckCompletionCancellableId()),
                    .run { _ in
                        // Reset the selected tab to default when signing out.
                        // Otherwise, we could end up on the same the tab as the last logged in user,
                        // if a user is signing back into the same account.
                        @Shared(.inMemory(.selectedTab)) var selectedTab: TabBarTab = .defaultSelection
                        $selectedTab.withLock { $0 = .defaultSelection }

                        // Ensures that the next user to sign in doesn't see the previous user's treatment.
                        statsigClient.clearCachedUser()

                        try? await clerk.signOut()
                    }
                )

            case .internal(.updateRequired):
                return .merge(
                    .send(.rootView(.network(.forceUpdate))),
                    .cancel(id: OnAppearCancellable())
                )

            case .internal(.showUpdateWarning):
                return .send(.banner(.show(type: .custom(.Icon.arrowUp, .string(L10n.FeatureApp.warnUpdateMessage), destination: .appStore), autoDismiss: true)))

            case .internal(.reachabilityDidChange(let isNetworkAvailable)):
                state.isNetworkAvailable = isNetworkAvailable
                return .none

            case .internal(.debugShowNotificationScreenTapped):
                return .run { send in
                    let notifPermission: PushNotificationAllowStatus = await self.userNotificationClient.getNotificationStatus()
                    if notifPermission == .unknown {
                        await send(.rootView(.debug(.showNotificationQuestion)))
                    } else {
                        await send(.banner(.setBanner(.warning(.string("your device has already responded to the native notification request, go to iOS settings and reset it. then try again")))))
                    }
                }

            case .banner(.tapped):
                if let destination = state.banner.banner?.destination {
                    switch destination {
                    case .appStore:
                        if appDistributionClient.isAvailable() {
                            return .run { _ in
                                await appDistributionClient.openLatestRelease()
                            }
                        } else {
                            let url = URL(string: "https://www.suno.com/ios")!
                            return .run { _ in
                                await openURL(url)
                            }
                        }

                    case .newClipsInOmniPlayer:
                        return .none

                    case .shareAsset:
                        return .none

                    case .hookDownload:
                        return .none
                    }
                }

                return .none

            case .internal(.attAuthorizationStatus(let status)):
                guard status == .authorized, let idfa = attClient.idfa() else { return .none }

                // Refresh RevenueCat's idfa collection
                paywallClient.collectDeviceIdentifiers()

                // Set the idfa on Braze and enable tracking
                brazeClient.setIdfa(idfa)

                // Creates a link in our backend tables between suno_user_id <-> IDFA
                return .run { _ in
                    do {
                        try await attClient.uploadIdfaIfAppropriate(idfa)
                    } catch {
                        // TODO: we need this to succeed. how do we want to persist, and then retry this? WIP solution here https://github.com/suno-ai/app-ios/pull/1478
                        log.telemetry.error(error)
                    }
                }

            case .rootView(let appRootViewAction):
                switch appRootViewAction {
                case let .cached(cachedAction):
                    switch cachedAction {
                    case let .authedUser(me, isFTUX):
                        updateRootView(
                            from: state.destination,
                            to: .loggedInV1(.init(
                                me: me,
                                isFTUX: isFTUX,
                                launchDeeplinkIntent: state.launchDeeplinkIntent
                            )),
                            state: &state
                        )
                        return .none
                    }

                case .network(let networkAction):
                    switch networkAction {
                    case let .fetched(me, isFTUX):
                        updateRootView(
                            from: state.destination,
                            to: .loggedInV1(.init(
                                me: me,
                                isFTUX: isFTUX,
                                launchDeeplinkIntent: state.launchDeeplinkIntent
                            )),
                            state: &state
                        )
                        return .none

                    case .failedToFetch:
                        updateRootView(from: state.destination, to: getOnboardingDestination(error: nil, state: state), state: &state)
                        return .none

                    case .forceUpdate:
                        updateRootView(from: state.destination, to: .forceUpdate(.init()), state: &state)
                        return .none
                    }

                case let .userAction(userAction):
                    switch userAction {
                    case .performedAuth(let me, let isFTUX):
                        updateRootView(
                            from: state.destination,
                            to: .loggedInV1(.init(
                                me: me,
                                isFTUX: isFTUX,
                                launchDeeplinkIntent: state.launchDeeplinkIntent
                            )),
                            state: &state
                        )
                        return .none

                    case .completedOnboarding(let result):
                        updateRootView(
                            from: state.destination,
                            to: .loggedInV1(.init(
                                me: result.me,
                                isFTUX: result.isFirstTimeUser,
                                launchDeeplinkIntent: state.launchDeeplinkIntent
                            )),
                            state: &state
                        )
                        return .none

                    case .tappedLogout:
                        updateRootView(from: state.destination, to: getOnboardingDestination(error: nil, state: state), state: &state)
                        return .none

                    case .goToWelcome:
                        updateRootView(from: state.destination, to: getOnboardingDestination(error: nil, state: state), state: &state)
                        return .none
                    }

                case let .debug(debugAction):
                    switch debugAction {
                    case .showOnboarding(let me):
                        // Show welcome coordinator when debugging onboarding
                        updateRootView(
                            from: state.destination,
                            to: .welcome(WelcomeCoordinator.State(
                                me: me,
                                isFTUX: state.isFTUX,
                                isReturningUser: true,
                                launchDeeplinkIntent: state.launchDeeplinkIntent,
                                forceDebugRoute: .default
                            )),
                            state: &state
                        )
                        return .none

                    case .showMadlibsOnboarding(let me):
                        // Show welcome coordinator when debugging madlibs onboarding
                        updateRootView(
                            from: state.destination,
                            to: .welcome(WelcomeCoordinator.State(
                                me: me,
                                isFTUX: state.isFTUX,
                                isReturningUser: true,
                                launchDeeplinkIntent: state.launchDeeplinkIntent,
                                forceDebugRoute: .madlibs
                            )),
                            state: &state
                        )
                        return .none

                    case .showHooksCreate:
                        return .send(.destination(.presented(.loggedInV1(.showHooksCreate))))

                    case .showNotificationQuestion:
                        updateRootView(from: state.destination, to: .notificationPrePrompt, state: &state)
                        return .none

                    case .showOrpheusCustomCreate:
                        return .send(.destination(.presented(.loggedInV1(.showOrpheusCustomCreate))))

                    case .showOrpheusCustomCreateCover:
                        return .send(.destination(.presented(.loggedInV1(.showOrpheusCustomCreateCover))))
                    }

                case .delegate:
                    return .none /* Catch all s*/
                }

            case .destination(.presented(.welcome(.delegate(let delegateAction)))):
                return handleWelcomeDelegateAction(delegateAction, state: &state)

            case .banner, .userEvent, .appDelegate, .destination:
                // Catch-all
                return .none
            }
        }
        .ifLet(\.$destination, action: \.destination)
        .dependency(\.telemetryClient, firebase)

        Analytics()
    }

    private func getAttAuthorizationIfEnabled(state _: inout State) -> Effect<Action> {
        guard FeatureFlag.general.showAttPrompt else { return .none }
        return .run { send in
            let status = await attClient.requestTrackingAuthorization()
            /// Handle the status result in an `Action` for analytics
            await send(.internal(.attAuthorizationStatus(status)))
        }
    }
}

private extension AppCoordinator {
    private func getOnboardingDestination(error: Error? = nil, state: State) -> Destination.State {
        .welcome(WelcomeCoordinator.State(
            me: nil,
            isFTUX: state.isFTUX,
            isReturningUser: false,
            launchDeeplinkIntent: state.launchDeeplinkIntent,
            authError: {
                if let error {
                    WelcomeCoordinator.AuthError(error: error)
                } else {
                    nil
                }
            }()
        ))
    }

    private func updateRootView(from: Destination.State?, to: Destination.State, state: inout State) {
        switch (from, to) {
        case (.forceUpdate, _):
            // When we're set to forceUpdate, don't move off the screen. the app is bricked
            break

        case (.loggedInV1, .loggedInV1):
            /// A cold launch of the app, where we have a cached user, will result in this being called twice (once with the cached user, once with the user loaded from the network
            /// when that happens, we want to update `cachedMe`, but not actually issue the routing change a second time
            break

        default:
            state.destination = to
        }
    }

    /// This should be called whenever a user's authentication status changes (login/logout)
    func userSessionChanged(_ me: Me?, state: inout State) -> Effect<Action> {
        let previousMe = state.previousMe
        state.previousMe = me
        APIClientV2.anonymousID = analyticsClient.anonymousId()

        return .run { _ in
            // TODO: - IOS-1161, log this transition result
            // https://linear.app/sunomusic/issue/IOS-1161/log-user-session-change-transition-result
            let result = try await userSessionTransitionClient.performUserTransition(
                .init(from: previousMe, to: me)
            )
        }
    }

    private func setupOnboardingDestination(
        me: Me,
        isHandleUpdated _: Bool,
        isFTUX: Bool,
        state: inout State
    ) -> Effect<Action> {
        updateRootView(
            from: state.destination,
            to: .welcome(WelcomeCoordinator.State(
                me: me,
                isFTUX: isFTUX,
                isReturningUser: true,
                launchDeeplinkIntent: state.launchDeeplinkIntent
            )),
            state: &state
        )
        return .none
    }
}

private extension AppCoordinator {
    func handleWelcomeDelegateAction(
        _ action: WelcomeCoordinator.Action.Delegate,
        state: inout State
    ) -> Effect<Action> {
        switch action {
        case .onboardingV1(let onboardingV1DelegateAction):
            switch onboardingV1DelegateAction {
            case .signUpResult(.failure(let error)):
                return .send(.signOut(error))

            case .signUpResult(.success(let me)):
                return .concatenate(
                    MainActor.assumeIsolated { userSessionChanged(me, state: &state) },
                    .send(.rootView(.userAction(.performedAuth(me, isFTUX: state.isFTUX))))
                )

            case .signOut:
                return .merge(
                    .send(.destination(.presented(.loggedInV1(.pauseAndDismissPlayer)))),
                    .send(.signOut(nil))
                )

            case .registerForRemoteNotifications:
                return .send(.appDelegate(.registerForRemoteNotifications))

            case .backTappedOnSignUp:
                // Handled in WelcomeCoordinator
                return .none
            }

        case .onboardingV2(let onboardingV2DelegateAction):
            switch onboardingV2DelegateAction {
            case .onboardingCompleted(let result):
                return .concatenate(
                    MainActor.assumeIsolated { userSessionChanged(result.me, state: &state) },
                    .send(.rootView(.userAction(.completedOnboarding(result))))
                )

            case .onboardingSkippedForReturningUser(let me):
                return .concatenate(
                    MainActor.assumeIsolated { userSessionChanged(me, state: &state) },
                    .send(.rootView(.userAction(.performedAuth(me, isFTUX: state.isFTUX))))
                )

            case .onboardingFailed:
                return .send(.signOut(nil))

            case .backTappedOnRoot:
                // Handled in WelcomeCoordinator
                return .none
            }
        }
    }
}

public struct AppCoordinatorScreen: View {
    let store: StoreOf<AppCoordinator>
    @Environment(\.scenePhase) var scenePhase
    @Dependency(DebugFeatureClient.self) var debugFeatureClient

    public init(store: StoreOf<AppCoordinator>) {
        self.store = store
        configureNavigationBar()
        configureTabBar()
    }

    public var body: some View {
        ZStack {
            switch store.destination {
            case .none:
                launchTransitionView

            case .some(let destination):
                VStack(spacing: 0) {
                    if !FeatureFlag.legacy.songGenerationBannerV2 {
                        BannerView(store: store.scope(state: \.banner, action: \.banner))
                    }
                    switch destination {
                    case .notificationPrePrompt:
                        BlackNotificationScreen(
                            didFinish: {
                                store.send(.rootView(.delegate(.didFinishNotificationPrePrompt)))
                            },
                            requestAuthorization: {
                                store.send(.rootView(.delegate(.showNativeNotificationPrompt)))
                            }
                        )
                        .onAppear {
                            @Dependency(AnalyticsClient.self) var analytics
                            analytics.trackV2(event: Event(category: .general, actionName: .rootDestinationChanged, context: "notification_pre_prompt"), source: "app_coordinator")
                        }

                    case .welcome:
                        if let store = store.scope(state: \.destination?.welcome, action: \.destination.welcome) {
                            WelcomeCoordinatorScreen(store: store)
                                .transition(.opacity)
                                .onAppear {
                                    @Dependency(AnalyticsClient.self) var analytics
                                    analytics.trackV2(event: Event(category: .general, actionName: .rootDestinationChanged, context: "logged_out"), source: "app_coordinator")
                                }
                        }

                    case .loggedInV1:
                        if let rootStore = store.scope(state: \.destination?.loggedInV1, action: \.destination.loggedInV1) {
                            RootCoordinatorV1Screen(store: rootStore)
                                .transition(.opacity)
                                .onAppear {
                                    @Dependency(AnalyticsClient.self) var analytics
                                    store.send(.rootView(.delegate(.didGoToRootView(rootStore.me))))
                                    analytics.trackV2(event: Event(category: .general, actionName: .rootDestinationChanged, context: "logged_in"), source: "app_coordinator")
                                }
                        }

                    case .forceUpdate:
                        if let store = store.scope(state: \.destination?.forceUpdate, action: \.destination.forceUpdate) {
                            ForceUpdateScreen(store: store)
                                .transition(.opacity)
                                .onAppear {
                                    @Dependency(AnalyticsClient.self) var analytics
                                    analytics.trackV2(event: Event(category: .general, actionName: .rootDestinationChanged, context: "force_update"), source: "app_coordinator")
                                }
                        }

                    case .offline:
                        if let store = store.scope(state: \.destination?.offline, action: \.destination.offline) {
                            OfflineScreen(store: store)
                                .transition(.opacity)
                                .onAppear {
                                    @Dependency(AnalyticsClient.self) var analytics
                                    analytics.trackV2(event: Event(category: .general, actionName: .rootDestinationChanged, context: "offline"), source: "app_coordinator")
                                }
                        }
                    }
                }
                .onAppear {
                    // We don't want to cancel the tti trace when we go down the start up orchestrator route
                    // This ttiTrace is managed inside the start up orchestrator codepath
                    // And we capture the initial feature flag value here to prevent a race condition that may crash the app
                    let startUpOrchestratorEnabled = store.appDelegate.startUpOrchestratorEnabled
                    guard startUpOrchestratorEnabled == false else { return }

                    // the first time this happens, we want to report this trace back to firebase
                    store.appDelegate.ttiTrace?.cancel()
                    store.send(.appDelegate(.internal(.clearTTITrace)))
                }
            }
        }
        .onAppear {
            store.send(.onAppear)
        }
        .task {
            store.send(.task)
        }
        .overlay(alignment: .top) {
            // Only used for 'App Update' banners
            BannerViewV2(store: store.scope(state: \.banner, action: \.banner))
        }
        .overlay {
            if let overlay = debugFeatureClient.makeOverlay(actions: .init(
                showOnboarding: {
                    guard let me = store.previousMe else {
                        store.send(.banner(.show(
                            type: .warning(.string("Can't show onboarding, you need to log in first")), autoDismiss: true
                        )))
                        return
                    }
                    store.send(.rootView(.debug(.showOnboarding(me))))
                },
                showMadlibsOnboarding: {
                    guard let me = store.previousMe else {
                        store.send(.banner(.show(
                            type: .warning(.string("Can't show onboarding, you need to log in first")), autoDismiss: true
                        )))
                        return
                    }
                    store.send(.rootView(.debug(.showMadlibsOnboarding(me))))
                },
                showHooksCreate: {
                    store.send(.rootView(.debug(.showHooksCreate)))
                },
                showNotificationQuestion: {
                    store.send(.internal(.debugShowNotificationScreenTapped))
                },
                showOrpheusCustomCreate: {
                    store.send(.rootView(.debug(.showOrpheusCustomCreate)))
                },
                showOrpheusCustomCreateCover: {
                    store.send(.rootView(.debug(.showOrpheusCustomCreateCover)))
                }
            )) {
                overlay
                    .ignoresSafeArea()
            }
        }
    }

    // MARK: - Private

    @ViewBuilder
    private var launchTransitionView: some View {
        GeometryReader { geo in
            launchScreenImage
                .resizable()
                .scaledToFill()
                .ignoresSafeArea()
                .frame(width: geo.size.width, height: geo.size.height)
        }
    }

    private var launchScreenImage: Image {
        let bundleIdentifier = Bundle.main.bundleIdentifier ?? ""
        if bundleIdentifier.contains("staff") {
            return Image.Assets.launchScreenStaff
        } else {
            return Image.Assets.launchScreen
        }
    }

    private func configureNavigationBar() {
        let customAppearance = UINavigationBarAppearance()
        customAppearance.configureWithTransparentBackground()

        if let font = TypographyV1.navigationTitle.uiFont {
            customAppearance.titleTextAttributes = [.font: font, .foregroundColor: UIColor.SemanticV1.textPrimary]
        }

        if let font = TypographyV1.headline2.uiFont {
            customAppearance.largeTitleTextAttributes = [.font: font, .foregroundColor: UIColor.SemanticV1.textPrimary]
        }

        customAppearance.shadowColor = .clear
        UINavigationBar.appearance().standardAppearance = customAppearance
        UINavigationBar.appearance().compactAppearance = customAppearance
        UINavigationBar.appearance().scrollEdgeAppearance = customAppearance
    }

    private func configureTabBar() {
        let standardAppearance = UITabBarAppearance()
        standardAppearance.configureWithOpaqueBackground()
        standardAppearance.backgroundColor = UIColor.SemanticV1.backgroundPrimary
        standardAppearance.stackedLayoutAppearance.normal.iconColor = UIColor.SemanticV1.iconSecondary
        UITabBar.appearance().standardAppearance = standardAppearance
        UITabBar.appearance().scrollEdgeAppearance = standardAppearance
    }
}

#Preview {
    AppCoordinatorScreen(store: .init(initialState: AppCoordinator.State(), reducer: {
        AppCoordinator()
    }))
}
