import Adamantium
import APIClient
import AVFoundation
import ComponentLibrary
import ComposableArchitecture
import ContactsClient
import EventBusClient
import FeatureClipList
import FeatureHooksGrid
import FeatureHooksModels
import FeatureHooksMoreMenu
import FeatureSettingsV2
import FeatureShare
import FeatureSocial
import FeatureToasts
import InAppNotificationClient
import Localization
import NavigationRouterClient
import StatsigClient
import SwiftUI
import Utilities

// swiftlint:disable file_length

@Reducer
public struct PublicProfileV1 {
    @Reducer(state: .equatable)
    public enum Destination {
        case share(Share)
        case creatorsToFollow(CreatorsToFollow)
        case notifications(Notifications)
        case addPhoneNumber(PhoneAddNumberInPlatform)
        case settings(SettingsV2)
        case profile(PublicProfileV1)
    }

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

        public enum SectionType: Equatable {
            case recommendedUsers
            case hooks
        }

        @Shared var me: Me
        private var _handle: String?
        public var handle: String { _handle ?? me.user.handle }
        private var _displayName: String?
        public var displayName: String? { _displayName }
        public var avatarImageUrl: String?
        @ObservationStateIgnored @ObservedBox var clipList: ClipList.State
        var loadState: LoadValueState<Profile> = .loading
        var firstLoad = true
        var isUpdatingFollow = false
        var recommendedUsers: IdentifiedArrayOf<RecommendUser> = []
        var followRequestsInFlight: IdentifiedArrayOf<RecommendUser> = []
        var hooks: [Hook] = []
        var isBlocked: Bool { loadState.value?.isBlocked == true }
        var hooksHiddenByUser: Bool { loadState.value?.hooksHiddenByUser == true }

        var isMe: Bool { me.user.handle == handle }
        var isFollowing: Bool { loadState.value?.isFollowing == true }
        @Shared(.appStorage(.isCreatorsToFollowVisible)) var isCreatorsToFollowVisible: Bool = true

        var isNotificationsFeedEnabled: Bool {
            FeatureFlag.legacy.notificationsFeed
        }

        var localNotifications: [NotificationItem]?

        var isContactSyncEnabled: Bool {
            FeatureFlag.legacy.contactSync
        }

        @Shared(.appStorage(.showInviteFriends)) var showInviteFriends: Bool = true

        var inviteFriendsBanner = InviteFriendsBanner.State()

        @Shared(.appStorage(.tappedAddNumberButtonInProfile)) var tappedAddNumberButtonInProfile: Bool = false
        var showAddPhoneNumberButton: Bool {
            return isContactSyncEnabled && isMe && me.user.phoneNumber == nil && !tappedAddNumberButtonInProfile
        }

        // Used to display credits on the top-left for Me
        // If `nil`, then we don't need to show or fetch credits
        var billingInfoLoadState: LoadValueState<SubscriptionInfoResponse>?
        // These strings are detached from `billingInfoLoadState` in order to display them if we have them from a previous load
        var creditsRemainingString: String?
        var songsLeftString: String?
        // Should show Settings and Credits buttons in the navigation toolbar
        var isRootView: Bool
        var showSettingsAndCredits: Bool {
            return isRootView && isMe
        }

        var shouldScrollToTop: Bool = false

        // When the parent view has to hide the navigation bar,
        // like when swiping left to show Profile from the Hooks feed,
        // we need to add some top padding to the profile view
        var isTopNavBarVisible: Bool = true

        // Recommendation metadata for tracking context when user navigates from hooks feed
        var recommendationMetadata: HooksRecommendationMetadata?

        // Used when available; otherwise falls back to manual handle/displayName/avatarImageUrl params (typically when navigating from a Clip).
        var simpleProfile: SimpleProfile?

        public init(me: Shared<Me>, handle: String?, isRootView: Bool = false, isTopNavBarVisible: Bool = true, recommendationMetadata: HooksRecommendationMetadata? = nil, simpleProfile: SimpleProfile? = nil) {
            self._me = me
            self._handle = handle
            self.clipList = .init(
                me: me,
                firstPageIndex: 1,
                allowResortingOnPin: true,
                context: SessionContext(source: .profile(id: handle ?? "")) // ID gets set after successful profile fetch
            )
            self.isRootView = isRootView
            self.isTopNavBarVisible = isTopNavBarVisible
            self.recommendationMetadata = recommendationMetadata

            if handle == nil || handle == me.wrappedValue.user.handle {
                self.simpleProfile = SimpleProfile(from: me.wrappedValue.user)
                self.avatarImageUrl = me.wrappedValue.user.avatarImageUrl
            } else {
                self.simpleProfile = simpleProfile
            }
        }

        public init(me: Shared<Me>, handle: String, displayName: String?, avatarImageUrl: String?, simpleProfile: SimpleProfile? = nil) {
            self._me = me
            self._handle = handle
            self._displayName = displayName
            self.avatarImageUrl = avatarImageUrl
            self.clipList = .init(
                me: me,
                firstPageIndex: 1,
                allowResortingOnPin: true,
                context: SessionContext(source: .profile(id: handle))
            )
            self.isRootView = false
            self.simpleProfile = simpleProfile
        }
    }

    public enum Action {
        public enum Delegate {
            case playClipsAt(Clip, [Clip])
            case authorTapped(String)
            case didUpdateFollow(Profile, Bool)
        }

        public enum Internal {
            case profileResult(Result<Profile, Error>)
            case followToggleResult(Bool, Result<Void, Error>)
            case followResult(RecommendUser, Result<Void, Error>)
            case recommendUsersResult(Result<[RecommendUser], Error>)
            case removeRecommendedUser(RecommendUser)
            case notificationsResponse(Result<UserNotification, Error>)
            case fetchContactPhoneNumbersResult(Result<[String], Error>)
            case billingInfoResult(Result<SubscriptionInfoResponse, Error>)
            case setHooks([Hook])
        }

        case destination(PresentationAction<Destination.Action>)
        case `internal`(Internal)
        case delegate(Delegate)
        case clipList(ClipList.Action)
        case loadProfile
        case dismiss
        case task

        case followToggleTapped
        case followTapped(RecommendUser)
        case hideRecommendedUserTapped(RecommendUser)
        case moreTapped(State.SectionType)
        case editTapped
        case shareTapped
        case creatorsToggleTapped
        case settingsTapped
        case creatorTapped(SimpleProfile)
        case playAllTapped
        case notificationsTapped
        case getLocalNotifications
        case getRemoteNotifications
        case meUpdated(Me)
        case followingTapped(handle: String)
        case followersTapped(handle: String)
        case scrollToTop
        case didScrollToTop
        case inviteFriendsBanner(InviteFriendsBanner.Action)
        case clipEvents(EventBusClient.ClipEvent)
        case hookEvent(EventBusClient.HookEvent)

        case addPhoneNumberTapped
        case creditsToolbarTapped
        case blockUserToggleTapped
        case blockUserToggleTappedResponse(Result<Void, Error>)
        case hideUserToggleTapped
        case hideUserToggleTappedResponse(Result<Void, Error>)

        case hookCardTapped(Hook)
        case hookOptionsTapped(Hook)
    }

    @Dependency(\.dismiss) private var dismiss
    @Dependency(\.apiClientV2) private var api
    @Dependency(APIClient.self) private var apiClient
    @Dependency(ContactsClient.self) private var contactsClient
    @Dependency(NavigationRouterClient.self) private var navigationRouter
    @Dependency(\.eventBus.getOmniplayerChannel) private var getOmniplayerChannel
    @Dependency(\.inAppNotificationClient) var inAppNotificationClient
    @Dependency(\.toastClient.show) private var showToast
    @Dependency(\.eventBus.getClipPublisher) private var getClipPublisher
    @Dependency(\.eventBus.getHookPublisher) private var getHookPublisher
    @Dependency(\.eventBus.sendHookEvent) var sendHookEvent
    @Dependency(\.eventBus.sendProfileEvent) var sendProfileEvent

    public init() {}

    private var clipListReducer: some ReducerOf<Self> {
        Scope(state: \.clipList, action: \.clipList) {
            ClipList()
        }
    }

    public var body: some ReducerOf<Self> {
        clipListReducer
            .withClipListClient { state in
                .init(getClips: { page in
                    // Params: profile handle, page, sort by, is suno short, include hooks
                    // Don't include hooks in pagination calls - they're already loaded once in loadProfile
                    try await api.getProfile(state.handle, page, .playCount, false, false).clips
                })
            }
        Reduce<State, Action> { state, action in
            struct UpdateMeCancellableId: Hashable {}
            struct ClipEventCancellableId: Hashable {}
            struct HookEventCancellableId: Hashable {}
            switch action {
            case .task:
                var effects: [Effect<Action>] = []
                // Handle billingInfo fetch if needed
                if state.showSettingsAndCredits {
                    state.billingInfoLoadState = .loading
                    effects.append(
                        .run { send in
                            await send(.internal(.billingInfoResult(Result(catching: { try await APIClientV2.underlying.send(Paths.billing.info.get).value }))))
                        }
                    )
                }
                effects.append(
                    .publisher { state.$me.publisher.map(Action.meUpdated) }
                        .cancellable(id: UpdateMeCancellableId(), cancelInFlight: true)
                )
                effects.append(
                    .subscribe(getClipPublisher(), send: Action.clipEvents, cancellableId: ClipEventCancellableId())
                )

                if FeatureFlag.hooks.isFeedEnabled {
                    effects.append(.subscribe(getHookPublisher(), send: Action.hookEvent, cancellableId: HookEventCancellableId()))
                }

                return .merge(effects)

            case .clipEvents(.updateClip(let clip)):
                return .send(.loadProfile)

            case .clipEvents(.removeClip(let clip)):
                let deletedHooks = state.hooks.filter { $0.clip?.id == clip.id }

                let hookEffects = deletedHooks.map { hook in
                    Effect<Action>.send(.hookEvent(.hookDeleted(hookId: hook.id)))
                }
                let removeClipEffect = Effect<Action>.send(.clipList(.internal(.removeClip(clip.id))))

                return .concatenate(hookEffects + [removeClipEffect])

            case .hookEvent(let event):
                switch event {
                case .hookDeleted(hookId: let id):
                    state.hooks = state.hooks.filter { $0.id != id }
                    return .none

                case .hookUpdated(let hook):
                    guard let index = state.hooks.firstIndex(of: hook) else { return .none }
                    state.hooks[index] = hook
                    return .none

                case .hookCreated(hook: let hook):
                    if let index = state.hooks.firstIndex(of: hook) {
                        state.hooks[index] = hook
                    } else {
                        state.hooks = [hook] + state.hooks
                    }
                    return .none

                case .hookCommentsToggled(let hookId, let canComment):
                    guard let index = state.hooks.firstIndex(where: { $0.id == hookId }) else {
                        return .none
                    }
                    state.hooks[index].allowComments = canComment
                    return .none

                default:
                    return .none
                }

            case .loadProfile:
                if state.firstLoad { state.loadState = .loading }
                state.firstLoad = false

                let firstPageIndex = state.clipList.pages.firstPageIndex
                let handle = state.handle

                // Because the profile request gives us a profile _and_ the clips we forward the profile
                // response on to the inner ClipList feature so it can take over the paging of more clips.
                // If we passed the `.clipList(.loadClips)` action along to start the process it would result
                // in 2 calls to first page to get both the profile and clips.
                return .run(
                    operation: { send in
                        // Params: profile handle, page, sort by, is suno short
                        let includeHooks = FeatureFlag.hooks.isFeedEnabled
                        let profile = try await api.getProfile(handle, firstPageIndex, .playCount, false, includeHooks)
                        let firstPageClips = profile.clips
                        if includeHooks {
                            await send(.internal(.setHooks(profile.hooks)))
                        }
                        await send(.clipList(.internal(.clipsLoadResult(page: firstPageIndex, result: .success(firstPageClips)))))
                        await send(.internal(.profileResult(.success(profile))))
                    },
                    catch: { error, send in
                        await send(.clipList(.internal(.clipsLoadResult(page: firstPageIndex, result: .failure(error)))))
                        await send(.internal(.profileResult(.failure(error))))
                    }
                )
                .merge(with: .run { [isContactSyncEnabled = state.isContactSyncEnabled, isMe = state.isMe] send in
                    guard isContactSyncEnabled, isMe else { return }
                    if contactsClient.getAuthorizationStatus() == .authorized {
                        await send(.internal(.fetchContactPhoneNumbersResult(Result(catching: { try await contactsClient.getContactPhoneNumbers() }))))
                    } else {
                        await send(.internal(.recommendUsersResult(Result(catching: { try await api.getRecommendedUsers([]) }))))
                    }
                })
                .merge(with: .run { [isMe = state.isMe] send in
                    guard isMe else { return }
                    await send(.getLocalNotifications)
                })

            case .internal(.setHooks(let hooks)):
                state.hooks = hooks
                return .none

            case .internal(.recommendUsersResult(.success(let recommendUsers))):
                state.recommendedUsers = .init(uniqueElements: recommendUsers)
                return .none

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

            case .internal(.fetchContactPhoneNumbersResult(.success(let phoneNumbers))):
                return .run { send in
                    await send(.internal(.recommendUsersResult(Result(catching: { try await api.getRecommendedUsers(phoneNumbers) }))))
                }

            case .internal(.fetchContactPhoneNumbersResult(.failure(let error))):
                log.telemetry.error(error)
                // If contact fetch fails, still get recommended users without them
                return .run { send in
                    await send(.internal(.recommendUsersResult(Result(catching: { try await api.getRecommendedUsers([]) }))))
                }

            case .playAllTapped:
                return clipListReducer.reduce(into: &state, action: .clipList(.playAllClips))

            case .editTapped:
                navigationRouter.send(route: .account)
                return .none

            case .shareTapped:
                if case let .loaded(profile) = state.loadState {
                    state.destination = .share(.init(.profile(profile), me: state.$me))
                }
                return .none

            case .creatorsToggleTapped:
                state.$isCreatorsToFollowVisible.withLock { $0.toggle() }
                return .none

            case .settingsTapped:
                navigationRouter.sendIfNavV2(route: .settings, else: {
                    state.destination = .settings(.init(me: state.$me))
                })
                return .none

            case .followToggleTapped:
                guard let profile = state.loadState.value,
                      !state.handle.isEmpty else { return .none }
                state.isUpdatingFollow = true
                return .run { [handle = state.handle, following = profile.isFollowing] send in
                    await send(.internal(.followToggleResult(!following, Result(catching: { try await apiClient.profileFollow(handle, following) }))))
                }

            case .followTapped(let recommendedUser):
                state.followRequestsInFlight.append(recommendedUser)
                return .run { [handle = recommendedUser.user.handle] send in
                    await send(.internal(.followResult(recommendedUser, Result(catching: { try await apiClient.profileFollow(handle, false) }))))
                }

            case .internal(.followResult(let recommendedUser, .success)):
                state.followRequestsInFlight.remove(recommendedUser)
                var mutableRecommendedUser = recommendedUser
                mutableRecommendedUser.user.isFollowing = true
                state.recommendedUsers[id: mutableRecommendedUser.id] = mutableRecommendedUser
                return .run { send in
                    // add delay for smooth remove animation
                    try await Task.sleep(for: .seconds(0.1))
                    await send(.internal(.removeRecommendedUser(recommendedUser)), animation: .easeInOut)
                }

            case .internal(.removeRecommendedUser(let recommendedUser)):
                state.recommendedUsers.remove(recommendedUser)
                return .none

            case let .internal(.followResult(recommendedUser, .failure(error))):
                state.followRequestsInFlight.remove(recommendedUser)
                log.telemetry.error(error)
                return .none

            case .hideRecommendedUserTapped(let recommendedUser):
                // TODO: Connect to API
                state.recommendedUsers.remove(recommendedUser)
                return .none

            case .moreTapped(let sectionType):
                switch sectionType {
                case .recommendedUsers:
                    navigationRouter.sendIfNavV2(route: .creatorsToFollow, else: {
                        state.destination = .creatorsToFollow(.init(me: state.$me))
                    })
                    return .none

                case .hooks:
                    if state.isMe {
                        navigationRouter.send(route: .myHooks(config: .myHooks(me: state.$me, initialState: .none)))
                    } else {
                        guard !state.hooks.isEmpty else { return .none }
                        let initialState: HooksGridConfig.InitialState = .prefetched(hooks: state.hooks, pendingHooks: [], hasMore: true, pageSize: 20)
                        navigationRouter.send(route: .userHooks(config: .userHooks(me: state.$me, initialState: initialState, handle: state.handle)))
                    }
                    return .none
                }

            case .internal(.profileResult(.success(let profile))):
                state.loadState = .loaded(profile)
                state.clipList.context = SessionContext(source: .profile(id: profile.id))
                return .none

            case .internal(.profileResult(.failure(let error))):
                log.telemetry.error(error)
                state.loadState = .failed(L10n.FeatureProfile.profileLoadError)
                return .none

            case .internal(.followToggleResult(let isFollowing, .success)):
                guard var profile = state.loadState.value else { return .none }
                profile.isFollowing = isFollowing
                state.loadState = .loaded(profile)
                state.isUpdatingFollow = false
                sendProfileEvent(.profileUpdated(profile)) // Only connected to Hooks and newer flows
                return .send(.delegate(.didUpdateFollow(profile, isFollowing)))

            case .internal(.followToggleResult(_, .failure(let error))):
                state.isUpdatingFollow = false
                log.telemetry.error(error)
                return .none

            case .clipList(.delegate(.toastAfter(let toast))):
                showToast(toast)
                return .none

            case .destination(.presented(.creatorsToFollow(.internal(.followResult(let recommendedUser, .success))))):
                state.recommendedUsers.remove(recommendedUser)
                return .none

            case .destination(.presented(.notifications(.internal(.setNotificationsReadResponse(.success))))):
                // Clear badge on profile screen when notifications are read
                return .none

            case .dismiss:
                return .run { _ in await self.dismiss() }

            case .notificationsTapped:
                navigationRouter.sendIfNavV2(route: .notifications, else: {
                    state.destination = .notifications(.init(me: state.$me))
                })
                return .none

            case .getLocalNotifications:
                inAppNotificationClient.refreshOrderedNotificationMap()
                return .send(.getRemoteNotifications)

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

            case .inviteFriendsBanner(.delegate(.bannerTapped)):
                state.destination = .share(.init(.me(state.me.user), me: state.$me))
                return .none

            case .inviteFriendsBanner(.delegate(.dismissTapped)):
                state.$showInviteFriends.withLock { $0 = false }
                return .none

            case .addPhoneNumberTapped:
                state.destination = .addPhoneNumber(.init(me: state.me))
                state.$tappedAddNumberButtonInProfile.withLock { $0 = true }
                return .none

            case .creatorTapped(let user):
                navigationRouter.sendIfNavV2(route: .profile(user.handle, recommendationMetadata: nil, simpleProfile: user), else: {
                    state.destination = .profile(.init(me: state.$me, handle: user.handle, simpleProfile: user))
                })
                return .none

            case .blockUserToggleTapped:
                let wasBlocked = state.isBlocked
                if case .loaded(var profile) = state.loadState {
                    profile.isBlocked.toggle()
                    state.loadState = .loaded(profile)
                }

                return .run { [handle = state.handle] send in
                    await send(
                        .blockUserToggleTappedResponse(
                            Result(catching: { try await api.blockProfile(handle, wasBlocked) })
                        )
                    )
                }

            case .blockUserToggleTappedResponse(let result):
                switch result {
                case .success:
                    return .none
                case .failure(let error):
                    if case .loaded(var profile) = state.loadState {
                        profile.isBlocked.toggle()
                        state.loadState = .loaded(profile)
                    }
                    let toast = ToastReducer.State.ToastType.warning(L10n.FeatureProfile.actionFailed, position: .bottom)
                    showToast(toast)
                    log.telemetry.error(error)
                    return .none
                }

            case .hideUserToggleTapped:
                let wasHidden = state.hooksHiddenByUser
                if case .loaded(var profile) = state.loadState {
                    profile.hooksHiddenByUser.toggle()
                    state.loadState = .loaded(profile)
                }

                return .run { [handle = state.handle, metadata = state.recommendationMetadata] send in
                    await send(
                        .hideUserToggleTappedResponse(
                            Result(catching: { try await api.toggleHideCreator(HideCreatorContentType.hook, handle, metadata, wasHidden) })
                        )
                    )
                }

            case .hideUserToggleTappedResponse(let result):
                switch result {
                case .success:
                    return .none
                case .failure(let error):
                    if case .loaded(var profile) = state.loadState {
                        profile.hooksHiddenByUser.toggle()
                        state.loadState = .loaded(profile)
                    }
                    let toast = ToastReducer.State.ToastType.warning(L10n.FeatureProfile.actionFailed, position: .bottom)
                    showToast(toast)
                    log.telemetry.error(error)
                    return .none
                }

            case .scrollToTop:
                state.shouldScrollToTop = true
                return .none

            case .didScrollToTop:
                state.shouldScrollToTop = false
                return .none

            case .hookCardTapped(let hook):
                navigationRouter.send(
                    route: .hooksContextualFeed(
                        state.hooks,
                        state.hooks.firstIndex(where: { $0.id == hook.id }) ?? 0,
                        nil,
                        .profile(id: state.loadState.value?.id ?? state.handle)
                    )
                )
                return .none

            case .hookOptionsTapped(let hook):
                let source: HooksFeedSource = state.isMe ? .myHooks : .profile(id: state.loadState.value?.id ?? state.handle)
                sendHookEvent(.showHooksMoreMenu(hook, source: source))
                return .none

            case .delegate(.authorTapped(let handle)):
                navigationRouter.send(route: .profile(handle))
                return .none // Handled in parent in Nav V1

            case .meUpdated(let me):
                guard case .loaded(var profile) = state.loadState,
                      profile.id == me.user.id else { return .none }
                profile.avatarImageUrl = me.user.avatarImageUrl
                profile.displayName = me.user.displayName
                profile.handle = me.user.handle
                state.loadState = .loaded(profile)
                return .none

            case .internal(.billingInfoResult(.success(let billingInfo))):
                state.billingInfoLoadState = .loaded(billingInfo)
                state.creditsRemainingString = max(billingInfo.totalCreditsLeft, 0).formatted(.number.notation(.compactName))
                // TODO: This logic might deserve its own client https://github.com/suno-ai/app-ios/pull/1156/files
                // https://linear.app/sunomusic/issue/CRE8-77/%5Bios%5D-re-think-billing-songs-left-display-logic
                // If we have free mobile v4 gens, display them here
//                if let freeMobileV4GensRemaining = billingInfo.freeMobileV4GensRemaining, freeMobileV4GensRemaining > 0 {
//                    state.songsLeftString = L10n.FeatureProfile.freeV4SongsLeft(freeMobileV4GensRemaining)
//                } else {
                // Remove this once we resolve the following logic
                // Until then, make sure to set this to nil so we don't show it to new users
//                    state.songsLeftString = nil
//                }
                // TODO: We need a way to get a fresh count of songs left in order to make this label actually feel okay
                // https://linear.app/sunomusic/issue/IOS-839/enable-live-updates-of-current-credits-and-songs-left
                /**
                 // If subscribed, show how many songs are left this month
                 else if let _ = billingInfo.plan {
                     state.songsLeftString = L10n.FeatureProfile.songsLeftThisMonth(billingInfo.songsLeft)
                 // If free, show how many songs are left today
                 } else {
                     state.songsLeftString = L10n.FeatureProfile.songsLeftToday(billingInfo.songsLeft)
                 }
                  */
                return .none

            case .internal(.billingInfoResult(.failure(let error))):
                state.billingInfoLoadState = .failed(error.localizedDescription)
                state.creditsRemainingString = L10n.FeatureProfile.failedToLoadCredits
                state.songsLeftString = nil
                log.telemetry.error(error)
                return .none

            case .creditsToolbarTapped:
                navigationRouter.send(.subscriptions)
                return .none

            case .followersTapped(let handle):
                navigationRouter.send(route: .followers(handle: handle))
                return .none

            case .followingTapped(let handle):
                navigationRouter.send(route: .following(handle: handle))
                return .none

            case .clipList, .delegate, .destination, .internal, .clipEvents:
                // Catch-all
                return .none
            }
        }
        .ifLet(\.$destination, action: \.destination)

        Analytics()
    }
}

public struct PublicProfileScreenV1: View {
    @State private var headerVisible = true
    @Namespace private var scrollSpace
    @Bindable var store: StoreOf<PublicProfileV1>
    /*
        Custom tab view uses opacity for showing different tabs
        onAppear is actually based on being added to hierarchy
        so we cannot rely on onAppear for tab view profile

        However if we still need to know if it is on screen or not
        we can use isVisibleProxy
     */
    let isVisibleProxy: Bool

    public init(store: StoreOf<PublicProfileV1>, isVisibleProxy: Bool = true) {
        self.store = store
        self.isVisibleProxy = isVisibleProxy
    }

    private var isLoadingState: Bool {
        if case .loading = store.loadState { return true }
        return false
    }

    public var body: some View {
        ZStack(alignment: .top) {
            sharedBackgroundView

            PublicProfileLoadingScreen(
                currentUser: store.me,
                targetHandle: store.handle,
                simpleProfile: store.simpleProfile,
                targetDisplayName: store.displayName,
                targetAvatarImageUrl: store.avatarImageUrl,
                isVisibleProxy: isVisibleProxy,
                isCreatorsToFollowVisible: store.isCreatorsToFollowVisible
            )
            .task { store.send(.loadProfile) }
            .opacity(isLoadingState ? 1 : 0)

            if case .loaded(let profile) = store.loadState {
                loadedView(profile)
                    .transition(.opacity)
            }

            if case .failed(let message) = store.loadState {
                FailedView(
                    title: L10n.FeatureProfile.errorTitle,
                    message: message,
                    buttonTitle: L10n.FeatureProfile.retry,
                    action: { store.send(.loadProfile) }
                )
                .transition(.opacity)
            }
        }
        .animation(.easeInOut(duration: 0.3), value: store.loadState)
        .background(Color.SemanticV1.backgroundPrimary)
        .navigationBarBackButtonHidden()
        .navigationBarTitleDisplayMode(.inline)
        .toolbarBackground(.clear, for: .navigationBar)
        .publicProfileNavigationModifiers(store: $store)
        .task { store.send(.task) }
        .toolbar {
            // If we're showing the credits toolbar, don't show the inline displayName for the profile
            // FIXME: Uncomment after live-credits infra is built
            //            if !store.showSettingsAndCredits {
            ToolbarItem(placement: .principal) {
                ToolbarTitle(store.displayName ?? "", visibility: !headerVisible && store.songsLeftString == nil)
            }
            //            }
            // Nav V2
            if store.showSettingsAndCredits {
                // Show these toolbar items only if the profile isn't in a loading state
                ToolbarItemGroup(placement: .topBarLeading) {
                    creditsToolbar
                        .transaction { transaction in
                            transaction.animation = nil
                        }
                }

                if FeatureFlag.hooks.isFeedEnabled {
                    ToolbarItem(placement: .topBarTrailing) {
                        ToolbarButton(.notifications, background: Color.SemanticV1.backgroundQuaternary) {
                            store.send(.notificationsTapped)
                        }
                        .notificationToolbarBadge()
                    }
                }

                if #available(iOS 26.0, *) {
                    ToolbarSpacer(.fixed, placement: .topBarTrailing)
                }

                ToolbarItem(placement: .topBarTrailing) {
                    ToolbarButton(.settings, background: Color.SemanticV1.backgroundQuaternary) {
                        store.send(.settingsTapped)
                    }
                }
                // Nav V1
            } else if store.isMe && store.isNotificationsFeedEnabled {
                ToolbarItem(placement: .topBarTrailing) {
                    ToolbarButton(.notifications, background: Material.ultraThin) {
                        store.send(.notificationsTapped)
                    }
                    .notificationToolbarBadge()
                }
            }

            // Visibility Menu
            if !store.isMe {
                ToolbarItem(placement: .topBarTrailing) {
                    if case .loaded = store.loadState {
                        visibilityMenu
                    } else {
                        loadingVisibilityMenuPlaceholder
                    }
                }
            }
        }
    }

    private var sharedBackgroundView: some View {
        ZStack(alignment: .bottom) {
            if isVisibleProxy {
                AuraShaderView(
                    id: "user_profile_shared",
                    appPreset: .purpleOrange(brightness: 1.0),
                    morphSpeed: 0.05,
                    scale: 1.5,
                    seed: 77.0,
                    isPaused: isLoadingState ? false : !headerVisible
                )
                .ignoresSafeArea()
            }
            Image.Assets.gradient
                .resizable()
                .aspectRatio(contentMode: .fill)
                .foregroundStyle(Color.SemanticV1.backgroundPrimary)
                .frame(height: 171)
        }
        .frame(height: 750)
        .offset(y: -630)
        .allowsHitTesting(false)
    }

    private var loadingVisibilityMenuPlaceholder: some View {
        ToolbarButton(.moreHorizontal, background: Material.ultraThin) {}
            .opacity(0.3)
            .disabled(true)
    }

    // swiftlint:disable:next function_body_length
    private func loadedView(_ profile: Profile) -> some View {
        ScrollViewReader { proxy in
            List {
                Group {
                    profileHeader(profile: profile)
                        .onAppear { headerVisible = true }
                        .onDisappear { headerVisible = false }
                        .id(scrollSpace)

                    userCreatedContentView
                        .padding(.top, 16)
                }
                .listRowBackground(Color.clear)
                .listRowSeparator(.hidden)
                .listRowInsets(.init())

                if !store.isBlocked {
                    songsSection
                }
            }
            .onChange(of: store.shouldScrollToTop) { _, _ in
                withAnimation {
                    proxy.scrollTo(scrollSpace, anchor: .bottom)
                }
                store.send(.didScrollToTop)
            }
        }
        .animation(.easeInOut(duration: 0.3), value: store.isCreatorsToFollowVisible)
        .scrollContentBackground(.hidden)
        .listStyle(.plain)
        .stableRefreshable { await store.send(.loadProfile).finish() }
        .contentMargins(.bottom, 90, for: .scrollContent)
        .navigationTitle(headerVisible ? "" : profile.displayName ?? "@\(profile.handle)")
    }

    @ViewBuilder
    private var visibilityMenu: some View {
        Menu {
            Button {
                store.send(.blockUserToggleTapped)
            } label: {
                Label {
                    Text(store.isBlocked ? L10n.FeatureProfile.unblockUser : L10n.FeatureProfile.blockUser)
                } icon: {
                    Image.Icon.cancel
                }
            }

            Button {
                store.send(.hideUserToggleTapped)
            } label: {
                Label {
                    Text(
                        store.hooksHiddenByUser ? L10n.FeatureProfile.unhideCreatorFromFeed : L10n.FeatureProfile.hideCreatorFromFeed
                    )
                } icon: {
                    Image.Icon.eye
                }
            }
        } label: {
            ToolbarButton(.moreHorizontal, background: Material.ultraThin) {}
        }
    }

    @ViewBuilder
    private var userCreatedContentView: some View {
        // if user is blocked, show blocked title and description, otherwise, show main content
        if store.isBlocked {
            VStack {
                Text(L10n.FeatureProfile.userBlockedHeader)
                    .typographyV1(.blockHeaderFont)
                    .foregroundColor(.SemanticV2.foregroundPrimary)
                    .multilineTextAlignment(.center)
                    .padding(.top, 44)
                    .padding(.bottom, 15)
                Text(L10n.FeatureProfile.userBlockedDescription)
                    .typographyV1(.blockDescriptionFont)
                    .foregroundColor(.SemanticV2.foregroundTertiary)
                    .multilineTextAlignment(.center)
                    .padding(.bottom, 15)
                Button {
                    store.send(.blockUserToggleTapped)
                } label: {
                    Text(L10n.FeatureProfile.unblock)
                        .typographyV1(.blockButtonFont)
                        .foregroundColor(.SemanticV2.foregroundPrimary)
                }
                .frame(maxWidth: .infinity)
                .padding(.vertical, 16)
                .background(Color.SemanticV2.backgroundSecondary)
                .clipShape(Capsule())
                .padding(.horizontal, 44)
            }
            .padding(.horizontal, 40)
        } else {
            if store.isMe,
               !store.recommendedUsers.isEmpty,
               store.isCreatorsToFollowVisible
            {
                ProfileSection(header: sectionHeader(
                    title: L10n.FeatureProfile.creatorsSectionTitle,
                    action: { store.send(.moreTapped(.recommendedUsers)) }
                )) {
                    ScrollView(.horizontal, showsIndicators: false) {
                        HStack(spacing: 8) {
                            ForEach(store.recommendedUsers) { recommendedUser in
                                recommendedUserCard(recommendedUser)
                            }
                        }
                    }
                    .contentMargins(.horizontal, 12, for: .scrollContent)
                }
                .textCase(nil)

                Divider()
            }

            // Only show Hooks if the user is flagged in
            // and has Hooks to show
            if FeatureFlag.hooks.isFeedEnabled, !store.hooks.isEmpty {
                ProfileSection(header: sectionHeader(
                    title: L10n.FeatureCatalog.hooks,
                    action: { store.send(.moreTapped(.hooks)) }
                )) {
                    hooksList(store.hooks)
                }
                .textCase(nil)

                Divider()
            }
        }
    }

    @ViewBuilder
    private var songsSection: some View {
        ProfileSectionHeader(
            header: sectionHeader(title: L10n.FeatureProfile.songsSectionTitle)
        )
        .textCase(nil)

        if !store.clipList.clips.isEmpty {
            ClipListContent(store: store.scope(state: \.clipList, action: \.clipList)) { _, item in
                item
                    .listRowSpacing(16.0)
                    .listRowInsets(.init(top: 0, leading: 16, bottom: 0, trailing: 16))
            }
        } else {
            emptySongs
                .listRowBackground(Color.clear)
                .listRowInsets(.init())
        }
    }

    @ViewBuilder
    private func hooksList(_: [Hook]) -> some View {
        ScrollView(.horizontal, showsIndicators: false) {
            HStack(spacing: 8) {
                ForEach(store.hooks) { hook in
                    HookCard(
                        thumbnailUrlString: hook.thumbnailImageUrl,
                        createdAt: nil,
                        clipName: hook.clip?.title,
                        playCount: hook.viewCount,
                        cardTapped: {
                            store.send(.hookCardTapped(hook))
                        },
                        moreTapped: {
                            store.send(.hookOptionsTapped(hook))
                        }
                    )
                    .containerRelativeFrame(.horizontal) { width, _ in
                        width / 3.25
                    }
                }
            }
        }
        .contentMargins(.horizontal, 12, for: .scrollContent)
    }

    let profileHeaderHorizontalPadding: CGFloat = 12
    let profileHeaderBottomPadding: CGFloat = 24

    // Adding extra padding to the top to account for the
    // hidden top navigation bar space when swiping left from Hooks Feed
    private var profileHeaderTopPadding: CGFloat {
        return store.isTopNavBarVisible ? 16 : 44
    }

    // swiftlint:disable:next function_body_length
    @ViewBuilder private func profileHeader(profile: Profile) -> some View {
        VStack(alignment: .leading, spacing: 0) {
            HStack(spacing: 0) {
                avatar(profile)
                Spacer()
                followers(profile)
                Spacer()
                following(profile)
                Spacer()
            }
            .padding(.bottom, 24)

            VStack(alignment: .leading, spacing: 0) {
                if let name = profile.displayName {
                    displayName(name)
                }

                handle(profile.handle, isProminent: profile.displayName == nil)
            }
        }
        .padding(.horizontal, profileHeaderHorizontalPadding)
        .padding(.top, profileHeaderTopPadding)
        .padding(.bottom, profileHeaderBottomPadding)

        VStack(spacing: 0) {
            ScrollView(.horizontal, showsIndicators: false) {
                HStack(spacing: 8) {
                    TagView(icon: Image.Icon.note, value: L10n.FeatureProfile.songs(profile.stats.songs))
                    TagView(icon: Image.Icon.thumbsUpV1, value: L10n.FeatureProfile.likes(profile.stats.likes))
                    TagView(icon: Image.Icon.playFilled, value: L10n.FeatureProfile.plays(profile.stats.plays))
                }
                .scrollTargetLayout()
            }
            .contentMargins(.horizontal, 12, for: .scrollContent)
            .scrollTargetBehavior(.viewAligned)

            if store.showAddPhoneNumberButton {
                addPhoneNumberButton
            }
        }
        .padding(.bottom, 24)

        if store.isMe {
            HStack(spacing: 12) {
                PrimaryButtonV1(
                    title: L10n.FeatureProfile.edit,
                    colorCombination: .secondary,
                    preferredSize: .medium,
                    leadingView: { Image.Icon.edit },
                    action: { store.send(.editTapped) }
                )

                PrimaryButtonV1(
                    title: L10n.FeatureProfile.share,
                    colorCombination: .secondary,
                    preferredSize: .medium,
                    leadingView: { Image.Icon.share },
                    action: { store.send(.shareTapped) }
                )

                PrimaryButtonV1(
                    isIconOnly: true,
                    colorCombination: store.isCreatorsToFollowVisible ? .dark : .secondary,
                    preferredSize: .medium,
                    leadingView: { Image.Icon.follow },
                    action: { store.send(.creatorsToggleTapped) }
                )
                .aspectRatio(1, contentMode: .fit)
            }
            .padding(.horizontal, 12)
            .padding(.bottom, 32)

        } else {
            if !store.isBlocked {
                HStack(spacing: 12) {
                    PrimaryButtonV1(
                        title: store.isFollowing ? L10n.FeatureProfile.following : L10n.FeatureProfile.follow,
                        isLoading: store.isUpdatingFollow,
                        colorCombination: store.isFollowing ? .secondary : .dark,
                        preferredSize: .medium,
                        leadingView: { store.isFollowing ? Image.Icon.checkV1 : Image.Icon.follow },
                        action: { store.send(.followToggleTapped) }
                    )

                    PrimaryButtonV1(
                        isIconOnly: true,
                        colorCombination: .secondary,
                        preferredSize: .medium,
                        leadingView: { Image.Icon.share },
                        action: { store.send(.shareTapped) }
                    )
                    .aspectRatio(1, contentMode: .fit)

                    PrimaryButtonV1(
                        isIconOnly: true,
                        colorCombination: .dark,
                        preferredSize: .medium,
                        leadingView: { Image.Icon.playFilled },
                        action: { store.send(.playAllTapped) }
                    )
                    .aspectRatio(1, contentMode: .fit)
                }
                .padding(.horizontal, 12)
                .padding(.bottom, 32)
            }
        }
    }

    private func avatar(_ profile: Profile) -> some View {
        RemoteImage(url: profile.avatarImageUrl, fallbackId: profile.id)
            .frame(width: 140, height: 140)
            .clipShape(.circle)
    }

    private func followers(_ profile: Profile) -> some View {
        Button {
            store.send(.followersTapped(handle: profile.handle))
        } label: {
            VStack(spacing: 0) {
                Text(profile.stats.followersCount.formatted())
                    .typographyV1(.body1.inputSans())
                    .foregroundStyle(Color.SemanticV1.textPrimary)
                Text(L10n.FeatureProfile.followers)
                    .typographyV1(.body2)
                    .foregroundStyle(Color.SemanticV1.textPrimary)
            }
        }
        .buttonStyle(.plain)
    }

    private func following(_ profile: Profile) -> some View {
        Button {
            store.send(.followingTapped(handle: profile.handle))
        } label: {
            VStack(spacing: 0) {
                Text(profile.stats.followingCount.formatted())
                    .typographyV1(.body1.inputSans())
                    .foregroundStyle(Color.SemanticV1.textPrimary)
                Text(L10n.FeatureProfile.following)
                    .typographyV1(.body2)
                    .foregroundStyle(Color.SemanticV1.textPrimary)
            }
        }
        .buttonStyle(.plain)
    }

    private func displayName(_ displayName: String) -> some View {
        Text(displayName)
            .typographyV1(.headline2)
            .foregroundStyle(Color.SemanticV1.textPrimary)
    }

    private func handle(_ handle: String, isProminent: Bool) -> some View {
        Text("@\(handle)")
            .typographyV1(isProminent ? .headline2 : .body2)
            .foregroundStyle(Color.SemanticV1.textPrimary)
    }

    private func sectionHeader(title: String, action: (() -> Void)? = nil) -> some View {
        HStack(alignment: .bottom, spacing: 8) {
            Text(title)
                .typographyV1(.headline4Wide.kerning(0.36).size { _ in 18.0 })
                .foregroundStyle(Color.SemanticV1.textPrimary)

            Spacer()

            if let action {
                Button {
                    action()
                } label: {
                    HStack(spacing: 2) {
                        Text(L10n.FeatureProfile.more)
                            .typographyV1(.body1)
                            .foregroundStyle(Color.SemanticV1.textSecondary)
                        Image.Icon.arrowRight
                            .resizable()
                            .aspectRatio(contentMode: .fit)
                            .frame(width: 16, height: 16)
                            .foregroundStyle(Color.SemanticV1.textSecondary)
                    }
                }
                .padding(.bottom, 6)
                .contentShape(.rect)
            }
        }
        .padding(.bottom, 4)
        .padding(.horizontal, 12)
    }

    private func recommendedUserCard(_ recommendedUser: RecommendUser) -> some View {
        let hasDisplayName = !recommendedUser.user.displayName.isEmpty
        return VStack(spacing: 0) {
            Button {
                store.send(.creatorTapped(recommendedUser.user))
            } label: {
                VStack(spacing: 0) {
                    RemoteImage(url: recommendedUser.user.avatarImageUrl, fallbackId: recommendedUser.user.id)
                        .frame(width: 52, height: 52)
                        .mask(Circle())
                        .padding(.bottom, 8)

                    Text(hasDisplayName
                        ? recommendedUser.user.displayName
                        : recommendedUser.user.handle)
                        .typographyV1(.body2.neueMontrealMedium())
                        .foregroundStyle(Color.SemanticV1.textPrimary)
                        .lineLimit(1)

                    Text(recommendedUser.reason.title)
                        .typographyV1(.body2)
                        .foregroundStyle(Color.SemanticV1.textSecondary)
                        .lineLimit(1)
                        .padding(.bottom, 18)
                }
            }

            Button {
                store.send(.followTapped(recommendedUser))
            } label: {
                if recommendedUser.user.isFollowing {
                    HStack(spacing: 4) {
                        Image.Icon.checkCircle
                            .resizable()
                            .aspectRatio(contentMode: .fit)
                            .frame(width: 16, height: 16)
                            .foregroundStyle(Color.SemanticV1.iconLink)
                        Text(L10n.FeatureProfile.following)
                            .foregroundStyle(Color.SemanticV1.textLink)
                            .typographyV1(.body1)
                    }
                } else {
                    Text(L10n.FeatureProfile.plusFollow)
                        .foregroundStyle(Color.SemanticV1.textLink)
                        .typographyV1(.body1)
                }
            }
            .opacity(store.followRequestsInFlight.contains(recommendedUser) ? 0 : 1)
            .overlay {
                ProgressView()
                    .progressViewStyle(.circular)
                    .opacity(store.followRequestsInFlight.contains(recommendedUser) ? 1 : 0)
            }
        }
        .padding(.top, 34)
        .padding(.bottom, 16)
        .padding(.horizontal, 8)
        .frame(width: 162)
        .background(RoundedRectangle(cornerRadius: 8).fill(Color.SemanticV1.backgroundTertiary))
        .overlay(alignment: .topTrailing) {
            Button {
                store.send(.hideRecommendedUserTapped(recommendedUser), animation: .easeInOut)
            } label: {
                Image.Icon.close
                    .foregroundStyle(Color.SemanticV1.iconTertiary)
                    .padding(8)
            }
        }
    }

    private var addPhoneNumberButton: some View {
        Button {
            store.send(.addPhoneNumberTapped)
        } label: {
            HStack(spacing: 12) {
                Image.Icon.addPhone
                    .foregroundStyle(Color.SemanticV1.iconPrimary)
                    .frame(width: 24, height: 24)

                VStack(alignment: .leading, spacing: 1) {
                    Text(L10n.FeatureSocial.addYourPhoneNumber)
                        .typographyV1(.body1)
                        .foregroundStyle(Color.SemanticV1.textPrimary)
                        .lineLimit(1)

                    Text(L10n.FeatureSocial.letContactsFindYou)
                        .inlineTypographyV1(.body2thin)
                        .foregroundStyle(Color.SemanticV1.textSecondary)
                }

                Spacer()

                Text(L10n.FeatureSocial.add)
                    .typographyV1(.body1)
                    .foregroundStyle(Color.SemanticV1.textInvert)
                    .padding(.horizontal, 16)
                    .padding(.vertical, 8)
                    .background(RoundedRectangle(cornerRadius: 8).fill(Color.SemanticV1.backgroundInvert))
            }
            .padding(.horizontal, 16)
            .padding(.vertical, 14)
            .background(RoundedRectangle(cornerRadius: 8).fill(Color.SemanticV1.backgroundTertiary))
        }
        .frame(maxWidth: .infinity)
        .contentShape(.rect)
        .padding(12)
    }

    private var emptySongs: some View {
        NoResultsView(
            leadingView: { Image.Assets.songsEmptyState.offset(y: 10) },
            title: L10n.FeatureProfile.emptySongsTitle,
            message: store.isMe ? L10n.FeatureProfile.emptySongsMessageMe : L10n.FeatureProfile.emptySongsMessageOther,
            style: .v2,
            detailMesasge: detail
        )
        .listRowBackground(Color.clear)
    }

    private var detail: AttributedString? {
        guard store.isMe else { return nil }
        let libraryURLString = Bundle.main.makeInAppURL(for: .library)?.absoluteString ?? ""
        return try? AttributedString(markdown: L10n.FeatureProfile.emptySongsMessageMeLink(libraryURLString))
    }

    @ViewBuilder
    private var creditsToolbar: some View {
        if #available(iOS 26.0, *) {
            newCreditsToolbar
        } else {
            oldCreditsToolbar
        }
    }

    @ViewBuilder
    private var oldCreditsToolbar: some View {
        HStack {
            Button {
                UIImpactFeedbackGenerator(style: .light).impactOccurred()
                store.send(.creditsToolbarTapped)
            } label: {
                HStack(spacing: 4) {
                    if let billingInfoLoadState = store.billingInfoLoadState {
                        switch billingInfoLoadState {
                        case .loaded:
                            Image.Icon.genres
                                .resizable()
                                .frame(width: 20, height: 20)
                                .foregroundStyle(Color.SemanticV1.iconPrimary)

                        case .loading:
                            // FIXME: Temporary until have a better source of truth on song generations remaining
                            // https://linear.app/sunomusic/issue/IOS-839/enable-live-updates-of-current-credits-and-songs-left
                            // It doesn't make sense to show a spinner, as it signals to the user that we have live data
                            Image.Icon.genres
                                .resizable()
                                .frame(width: 20, height: 20)
                                .foregroundStyle(Color.SemanticV1.iconPrimary)

                        case .failed:
                            Image.Icon.alert
                                .resizable()
                                .frame(width: 20, height: 20)
                                .foregroundStyle(Color.SemanticV1.iconPrimary)
                        }
                    } else {
                        Image.Icon.genres
                            .resizable()
                            .frame(width: 20, height: 20)
                            .foregroundStyle(Color.SemanticV1.iconPrimary)
                    }

                    // This is detached from `billingInfoLoadState` so we can display it if we have it, even if we're fetching the BillingInfo again
                    Text(store.creditsRemainingString ?? "")
                        .frame(width: 40, alignment: .leading)
                        .typographyV1(.body2thin.neueMontrealMedium())
                        .foregroundStyle(Color.SemanticV1.textPrimary)
                        .opacity(store.creditsRemainingString != nil ? 1 : 0)
                }
                .frame(height: 40)
                .padding(.horizontal, 14)
                .clipShape(.rect)
                .background {
                    if #unavailable(iOS 26.0) {
                        RoundedRectangle(cornerRadius: .infinity).fill(Material.ultraThin)
                    }
                }
            }
            .fixedSize()
            .buttonStyle(.plain)
            .disabled(store.billingInfoLoadState == .loading)
            .transaction { transaction in
                transaction.animation = nil
            }

            if let songsLeft = store.songsLeftString {
                Text(songsLeft)
                    .typographyV1(.body2thin.neueMontrealMedium())
                    .foregroundStyle(Color.SemanticV1.textPrimary)
            }
        }
    }

    @ViewBuilder
    private var newCreditsToolbar: some View {
        Button {
            UIImpactFeedbackGenerator(style: .light).impactOccurred()
            store.send(.creditsToolbarTapped)
        } label: {
            if let billingInfoLoadState = store.billingInfoLoadState {
                switch billingInfoLoadState {
                case .loaded:
                    Image.Icon.genres
                        .resizable()
                        .frame(width: 20, height: 20)
                        .foregroundStyle(Color.SemanticV1.iconPrimary)

                case .loading:
                    // FIXME: Temporary until have a better source of truth on song generations remaining
                    // https://linear.app/sunomusic/issue/IOS-839/enable-live-updates-of-current-credits-and-songs-left
                    // It doesn't make sense to show a spinner, as it signals to the user that we have live data
                    Image.Icon.genres
                        .resizable()
                        .frame(width: 20, height: 20)
                        .foregroundStyle(Color.SemanticV1.iconPrimary)

                case .failed:
                    Image.Icon.alert
                        .resizable()
                        .frame(width: 20, height: 20)
                        .foregroundStyle(Color.SemanticV1.iconPrimary)
                }

                // This is detached from `billingInfoLoadState` so we can display it if we have it, even if we're fetching the BillingInfo again
                if let creditsRemaining = store.creditsRemainingString {
                    Text(creditsRemaining)
                        .typographyV1(.body2thin.neueMontrealMedium())
                        .foregroundStyle(Color.SemanticV1.textPrimary)
                        .transition(.blurReplace)
                }
            } else {
                Image.Icon.genres
                    .resizable()
                    .frame(width: 20, height: 20)
                    .foregroundStyle(Color.SemanticV1.iconPrimary)
            }
        }
        .animation(.default, value: store.creditsRemainingString)
        .fixedSize()
        .disabled(store.billingInfoLoadState == nil)
        .transaction { transaction in
            transaction.animation = nil
        }

        if let songsLeft = store.songsLeftString {
            Text(songsLeft)
                .typographyV1(.body2thin.neueMontrealMedium())
                .foregroundStyle(Color.SemanticV1.textPrimary)
        }
    }
}

/// Custom section implementation doesn't have sticky header behavior
private struct ProfileSection<Header: View, Content: View>: View {
    let header: Header
    let content: Content

    init(
        @ViewBuilder header: () -> Header,
        @ViewBuilder content: () -> Content
    ) {
        self.header = header()
        self.content = content()
    }

    init(
        header: Header,
        @ViewBuilder content: () -> Content
    ) {
        self.header = header
        self.content = content()
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            header
            content
        }
        .listRowBackground(Color.clear)
        .listRowSeparator(.hidden)
        .listRowInsets(.init())
        .padding(.top, 24)
    }
}

/// Section header without content wrapper - for sections that need to render List rows directly
private struct ProfileSectionHeader<Header: View>: View {
    let header: Header

    init(header: Header) {
        self.header = header
    }

    var body: some View {
        header
            .listRowBackground(Color.clear)
            .listRowSeparator(.hidden)
            .listRowInsets(.init())
            .padding(.top, 24)
    }
}

// MARK: - Navigation Modifiers

private extension View {
    func publicProfileNavigationModifiers(store: Bindable<StoreOf<PublicProfileV1>>) -> some View {
        self
            // Sheets
                .shareSheet(store: store)
                .addPhoneNumberFullScreenCover(store: store)
                // Navigation Destinations
                .creatorsToFollowDestination(store: store)
                .notificationsDestination(store: store)
                .settingsDestination(store: store)
                .profileDestination(store: store)
    }

    // MARK: - Sheets

    func shareSheet(store: Bindable<StoreOf<PublicProfileV1>>) -> some View {
        self.sheet(item: store.scope(state: \.destination?.share, action: \.destination.share)) { store in
            ShareScreen(store: store)
        }
    }

    func addPhoneNumberFullScreenCover(store: Bindable<StoreOf<PublicProfileV1>>) -> some View {
        self.fullScreenCover(item: store.scope(state: \.destination?.addPhoneNumber, action: \.destination.addPhoneNumber)) { store in
            PhoneAddNumberInPlatformView(store: store)
        }
    }

    // MARK: - Navigation Destinations

    func creatorsToFollowDestination(store: Bindable<StoreOf<PublicProfileV1>>) -> some View {
        self.navigationDestination(item: store.scope(state: \.destination?.creatorsToFollow, action: \.destination.creatorsToFollow)) { store in
            CreatorsToFollowScreen(store: store)
        }
    }

    func notificationsDestination(store: Bindable<StoreOf<PublicProfileV1>>) -> some View {
        self.navigationDestination(item: store.scope(state: \.destination?.notifications, action: \.destination.notifications)) { store in
            NotificationsScreen(store: store)
                .customBackButton(background: Color.SemanticV1.backgroundQuaternary, action: { store.send(.back) })
        }
    }

    func settingsDestination(store: Bindable<StoreOf<PublicProfileV1>>) -> some View {
        self.navigationDestination(item: store.scope(state: \.destination?.settings, action: \.destination.settings)) { store in
            SettingsV2Screen(store: store)
        }
    }

    func profileDestination(store: Bindable<StoreOf<PublicProfileV1>>) -> some View {
        self.navigationDestination(item: store.scope(state: \.destination?.profile, action: \.destination.profile)) { store in
            PublicProfileScreenV1(store: store)
                .customBackButton(background: Material.ultraThin) {
                    store.send(.dismiss)
                }
        }
    }
}

private extension TypographyV1 {
    static let blockHeaderFont: TypographyV1 = .init(
        name: "Block Header Font",
        size: 20,
        style: .body,
        weight: .ppNeueMontrealMedium,
        lineHeight: 24
    )

    static let blockDescriptionFont: TypographyV1 = .init(
        name: "Block Description Font",
        size: 14,
        style: .body,
        weight: .ppNeueMontrealRegular,
        lineHeight: 20
    )

    static let blockButtonFont: TypographyV1 = .init(
        name: "Block Button Font",
        size: 17,
        style: .body,
        weight: .ppNeueMontrealMedium,
        lineHeight: 24
    )
}

public struct PublicProfileLoadingScreen: View {
    let currentUser: Me
    let targetHandle: String
    let simpleProfile: SimpleProfile?
    let isVisibleProxy: Bool
    let isCreatorsToFollowVisible: Bool

    private let _targetDisplayName: String?
    private let _targetAvatarImageUrl: String?

    var isOwnProfile: Bool {
        currentUser.user.handle == targetHandle
    }

    var targetDisplayName: String? {
        simpleProfile?.displayName ?? _targetDisplayName
    }

    var targetAvatarImageUrl: String? {
        simpleProfile?.avatarImageUrl ?? _targetAvatarImageUrl
    }

    public init(
        currentUser: Me,
        targetHandle: String,
        simpleProfile: SimpleProfile? = nil,
        targetDisplayName: String? = nil,
        targetAvatarImageUrl: String? = nil,
        isVisibleProxy: Bool = true,
        isCreatorsToFollowVisible: Bool = true
    ) {
        self.currentUser = currentUser
        self.targetHandle = targetHandle
        self.simpleProfile = simpleProfile
        self._targetDisplayName = targetDisplayName
        self._targetAvatarImageUrl = targetAvatarImageUrl
        self.isVisibleProxy = isVisibleProxy
        self.isCreatorsToFollowVisible = isCreatorsToFollowVisible
    }

    public var body: some View {
        ScrollView(.vertical) {
            profileHeaderPlaceholder
                .padding(.top, 16)
                .padding(.bottom, 24)
            if isOwnProfile, isCreatorsToFollowVisible {
                creatorsToFollow
                    .padding(.top, 9)
            }

            hooksSection
                .padding(.top, 9)

            clipList
                .padding(.top, 20)
        }
        .navigationBarBackButtonHidden()
        .navigationBarTitleDisplayMode(.inline)
        .toolbarBackground(.clear, for: .navigationBar)
    }

    @ViewBuilder
    public func placeholderText(
        width: CGFloat,
        height: CGFloat = 16,
        cornerRadius: CGFloat = 4
    ) -> some View {
        RoundedRectangle(cornerRadius: cornerRadius)
            .fill(Color.gray.opacity(0.3))
            .frame(width: width, height: height)
            .placeholderShimmering(isVisible: true, cornerRadius: cornerRadius)
    }

    @ViewBuilder
    public func placeholderButton(
        width: CGFloat? = nil,
        height: CGFloat = 56,
        cornerRadius: CGFloat = 8
    ) -> some View {
        RoundedRectangle(cornerRadius: cornerRadius)
            .fill(Color.gray.opacity(0.3))
            .frame(width: width, height: height)
            .placeholderShimmering(isVisible: true, cornerRadius: cornerRadius)
    }

    @ViewBuilder
    private var profileHeaderPlaceholder: some View {
        VStack(alignment: .leading, spacing: 0) {
            // Avatar and stats row
            HStack(spacing: 0) {
                // Real avatar from currentUser
                if let targetAvatarImageUrl = targetAvatarImageUrl, !targetAvatarImageUrl.isEmpty {
                    RemoteImage(url: targetAvatarImageUrl, fallbackId: simpleProfile?.id)
                        .frame(width: 140, height: 140)
                        .clipShape(.circle)
                } else if let avatar = simpleProfile?.avatarImageUrl {
                    RemoteImage(url: avatar, fallbackId: simpleProfile?.id)
                        .frame(width: 140, height: 140)
                        .clipShape(.circle)
                } else {
                    Circle()
                        .fill(Color.gray.opacity(0.3))
                        .frame(width: 140, height: 140)
                        .placeholderShimmering(isVisible: true, cornerRadius: 70)
                }

                Spacer()

                // Followers
                VStack(spacing: 0) {
                    placeholderText(width: 40, height: 20, cornerRadius: 4)

                    placeholderText(width: 60, height: 16, cornerRadius: 4)
                        .padding(.top, 4)
                }

                Spacer()

                // Following placeholder
                VStack(spacing: 0) {
                    placeholderText(width: 40, height: 20, cornerRadius: 4)

                    placeholderText(width: 60, height: 16, cornerRadius: 4)
                        .padding(.top, 4)
                }

                Spacer()
            }
            .padding(.bottom, 26)

            VStack(alignment: .leading, spacing: 0) {
                if let displayName = simpleProfile?.displayName, !displayName.isEmpty {
                    Text(displayName)
                        .typographyV1(.headline2)
                        .foregroundStyle(Color.SemanticV1.textPrimary)
                } else if let displayName = targetDisplayName, !displayName.isEmpty {
                    Text(displayName)
                        .typographyV1(.headline2)
                        .foregroundStyle(Color.SemanticV1.textPrimary)
                }

                if let handle = simpleProfile?.handle, !handle.isEmpty {
                    Text("@\(handle)")
                        .typographyV1(.body2)
                        .foregroundStyle(Color.SemanticV1.textPrimary)
                } else {
                    Text("@\(targetHandle)")
                        .typographyV1(.body2)
                        .foregroundStyle(Color.SemanticV1.textPrimary)
                }
            }

            // Tags section placeholder
            VStack(spacing: 0) {
                ScrollView(.horizontal, showsIndicators: false) {
                    HStack(spacing: 8) {
                        // Clips count tag placeholder
                        placeholderButton(width: 105, height: 26, cornerRadius: 16)

                        // Likes tag placeholder
                        placeholderButton(width: 105, height: 26, cornerRadius: 16)

                        // Plays tag placeholder
                        placeholderButton(width: 105, height: 26, cornerRadius: 16)
                    }
                    .scrollTargetLayout()
                }
                .scrollTargetBehavior(.viewAligned)
            }
            .padding(.bottom, 24)
            .padding(.top, 22)

            // Action buttons placeholder
            HStack(spacing: 12) {
                if isOwnProfile {
                    // Edit button placeholder
                    placeholderButton(height: 56, cornerRadius: 8)

                    // Share button placeholder
                    placeholderButton(height: 56, cornerRadius: 8)

                    // Follow toggle button placeholder (icon only)
                    placeholderButton(width: 56, height: 56, cornerRadius: 8)
                } else {
                    // Follow button placeholder
                    placeholderButton(height: 56, cornerRadius: 8)

                    // Share button placeholder (icon only)
                    placeholderButton(width: 56, height: 56, cornerRadius: 8)

                    // Play all button placeholder (icon only)
                    placeholderButton(width: 56, height: 56, cornerRadius: 8)
                }
            }
            .padding(.bottom, 32)
        }
        .padding(.horizontal, 12) // profileHeaderHorizontalPadding
    }

    @ViewBuilder
    private var creatorsToFollow: some View {
        VStack(alignment: .leading, spacing: 9) {
            HStack {
                placeholderText(width: 130, height: 20, cornerRadius: 4)
                Spacer()
                placeholderText(width: 56, height: 17, cornerRadius: 4)
                    .padding(.bottom, 13)
            }
            .padding(.horizontal, 12)
            ScrollView(.horizontal, showsIndicators: false) {
                HStack(spacing: 8) {
                    ForEach(0 ..< 3, id: \.self) { _ in
                        RoundedRectangle(cornerRadius: 8)
                            .fill(Color.gray.opacity(0.3))
                            .frame(width: 160, height: 192)
                            .placeholderShimmering(isVisible: true, cornerRadius: 8)
                    }
                }
            }
            .contentMargins(.horizontal, 12, for: .scrollContent)
            .scrollDisabled(true)
        }
    }

    @ViewBuilder
    private var hooksSection: some View {
        VStack(alignment: .leading, spacing: 16) {
            HStack {
                placeholderText(width: 60, height: 17, cornerRadius: 4)
                Spacer()
                placeholderText(width: 56, height: 17, cornerRadius: 4)
                    .padding(.bottom, 4)
            }
            .padding(.horizontal, 12)
            ScrollView(.horizontal, showsIndicators: false) {
                HStack(spacing: 8) {
                    ForEach(0 ..< 4, id: \.self) { _ in
                        RoundedRectangle(cornerRadius: 8)
                            .fill(Color.gray.opacity(0.3))
                            .frame(width: 116, height: 194)
                            .placeholderShimmering(isVisible: true, cornerRadius: 8)
                    }
                }
            }
            .contentMargins(.horizontal, 12, for: .scrollContent)
            .scrollDisabled(true)
        }
        .padding(.top, 4)
    }

    @ViewBuilder
    private var clipList: some View {
        VStack(alignment: .leading, spacing: 16) {
            placeholderButton(width: 60, height: 20, cornerRadius: 4)

            VStack(spacing: 8) {
                ForEach(0 ..< 5, id: \.self) { _ in
                    ClipListItemPlaceholderView()
                }
            }
        }
        .padding(.horizontal, 12)
    }

    @ViewBuilder
    func ClipListItemPlaceholderView() -> some View {
        VStack(alignment: .leading, spacing: 8) {
            HStack(spacing: 12) {
                placeholderButton(width: 50, height: 68, cornerRadius: 8)

                VStack(alignment: .leading, spacing: 6) {
                    // Title Placeholder
                    placeholderText(width: 120, height: 16, cornerRadius: 4)

                    // Subtitle Placeholder
                    placeholderText(width: 180, height: 14, cornerRadius: 4)

                    // Secondary action bar placeholder
                    HStack(spacing: 7) {
                        Circle()
                            .fill(Color.gray.opacity(0.3))
                            .frame(width: 16, height: 16)
                        ForEach(0 ..< 3, id: \.self) { _ in
                            placeholderButton(width: 24, height: 16, cornerRadius: 6)
                        }
                    }
                    .padding(.top, 4)
                }
                Spacer()
            }
            Divider().opacity(0.1)
        }
        .padding(.vertical, 4)
        .transition(.opacity)
    }
}
