import APIClient
import ComponentLibrary
import ComposableArchitecture
import ContactsClient
import DeeplinkIntents
import Errors
import EventBusClient
import FeatureClipList
import FeatureDiscoverSectionDetail
import FeaturePlaylistDetail
import FeatureProfile
import FeatureRemix
import FeatureShare
import FeatureSocial
import Localization
import NavigationRouterClient
import RequestState
import StatsigClient
import SunoModelClient
import SwiftUI
import UserConfigClient
import Utilities

// swiftlint:disable file_length

@Reducer
public struct Discover {
    @Reducer(state: .equatable)
    public enum Destination {
        case trending(Trending)
        case playlists(Playlists) // List of playlists
        case playlistDetail(PlaylistDetail)
        case invite(Share)
        case fromYourContacts(FromYourContacts)
        case profile(PublicProfileV1)
        case likedSongs(LikedSongs)
        case likedPlaylists(LikedPlaylists)
    }

    @ObservableState
    public struct State: Equatable {
        public enum Constants {
            static let trendingIds: [String] = [
                "1190bf92-10dc-4ce5-968a-7a377f37f984", // prod
                "845539aa-2a39-4cf5-b4ae-16d3fe159a77", // prod v2
                "08a079b2-a63b-4f9c-9f29-de3c1864ddef", // prod v3
                "discover_playlist", // new discover
                "07653cdf-8f72-430e-847f-9ab8ac05af40", // staging
            ]

            enum RedirectTo {
                static let playlists = "playlist"
                static let likedSongs = "liked_songs"
                static let likedPlaylists = "liked_playlists"
                static let trendingSongs = "trending_songs"
                static let genre = "genre"
                static let stylePlaylist = "style" // Used when playlists use a url with /style/{styleName}
            }

            // IDs for production environment
            static let featureArtistTimbalandHandle = "timbaland"
            static let featureArtistTimbalandPlaylistId = "2479ec84-fc53-4611-b014-0ffc90c030dd"

            static let maxDisplayedClips = 12

            static let refreshInterval: TimeInterval = 60 * 5 // 5 minutes

            static let horizontalContentMargin: CGFloat = 12
        }

        public enum LoadingState {
            case firstPage
            case nextPage
            case idle
            case failed
        }

        public var isLoading: Bool {
            loadingState != .idle
        }

        public var showPlaceholder: Bool {
            loadingState == .firstPage || (feed == .skeleton)
        }

        public var isShimmering: Bool {
            loadingState == .firstPage || (feed == .skeleton && loadingState != .failed)
        }

        public var canLoadMorePages: Bool {
            // If we're at the last section, and we have more available sections to load,
            // we can load more pages if we're not already loading a page.
            guard numberOfSections < feed.totalSections,
                  !isLoading
            else { return false }

            return true
        }

        public var hasMoreSections: Bool {
            feed.sections.count < feed.totalSections
        }

        // Used to determine if we're at the last section
        public var lastSectionIndex: Int? {
            guard numberOfSections > 0 else { return nil }
            return numberOfSections - 1
        }

        @Presents public var destination: Destination.State?

        @Shared var me: Me
        var feed: DiscoverFeed = .skeleton
        var loadingState: LoadingState = .idle
        var featuredArtistPlaylist: Playlist?

        @Shared(.inMemory(.isCompactPlayerVisible)) fileprivate var isCompactPlayerVisible = false

        var isContactSyncEnabled: Bool {
            FeatureFlag.legacy.contactSync
        }

        var isFeaturedArtistEnabled: Bool {
            FeatureFlag.legacy.featuredArtist
        }

        @Shared(.appStorage(.dismissedContactSyncDiscover)) var dismissedContactSyncDiscover: Bool = false
        @Shared(.appStorage(.dismissedInviteFriendsDiscover)) var dismissedInviteFriendsDiscover: Bool = false
        @Shared(.appStorage(.dismissedFeaturedArtistDiscover)) var dismissedFeaturedArtistDiscover: Bool = false
        @Shared(.appStorage(.lastDiscoverUpdateAt)) var lastDiscoverUpdateAt: Date?

        // Don't show any banner if we've dismissed one this session, tracked in-memory
        var didDismissBannerThisSession = false

        var hasSetRemixPerm: RequestState<Bool?> = .none
        var dismissedRemixPermDiscover = false

        public enum Banner {
            case contactSync
            case inviteFriends
            case featuredArtist
            case remixability
        }

        var banner: Banner? {
            guard !didDismissBannerThisSession else { return nil }

            let hasNotSetPermissionBefore = hasSetRemixPerm.didSucceed && hasSetRemixPerm.value != true
            if FeatureFlag.create.remixPerm, hasNotSetPermissionBefore, !dismissedRemixPermDiscover {
                return .remixability
            }

            @Dependency(\.contactsClient.getAuthorizationStatus) var getContactsAuthorizationStatus
            if !dismissedContactSyncDiscover, !(getContactsAuthorizationStatus() == .authorized) {
                return .contactSync
            }

            if !dismissedInviteFriendsDiscover {
                return .inviteFriends
            }

            return nil
        }

        @ObservationStateIgnored @ObservedBox var contactSyncBanner = ContactSyncBanner.State()
        @ObservationStateIgnored @ObservedBox var inviteFriendsBanner = InviteFriendsBanner.State()
        @ObservationStateIgnored @ObservedBox var featuredArtistBanner = FeaturedArtistBanner.State()
        @ObservationStateIgnored @ObservedBox var remixBanner = RemixBanner.State()

        // This is how many sections we fetch at a time.
        // This is the default value, but we update it with
        // every page we receive back from backend.
        public var pageSize: Int = 3

        // Always fetch the first page first
        public var startIndex: Int = 0

        public var numberOfSections: Int {
            feed.sections.count
        }

        public var currentPage: Int = 0

        public var shouldScrollToTop: Bool = false

        public var currentlyPreviewingClip: Clip?
        public var horizontalIndices: [Int: Int] = [:] // Track horizontal scroll positions
        public var refreshId = UUID()

        public init(me: Shared<Me>) {
            self._me = me
        }
    }

    public enum Action {
        case destination(PresentationAction<Destination.Action>)

        case task
        case selectClip(Clip, queue: [Clip], context: SessionContext)
        case updateClip(Clip)
        case deleteClip(Clip)
        case selectPlaylist(Playlist)
        case selectStyle(StyleItem)
        case playlistMoreTapped(PlaylistSection)
        case playlistListMoreTapped(PlaylistListSection)
        case authorTapped(String, String?, String?)
        case `internal`(Internal)
        case getDiscoverFeed(pullToRefresh: Bool)
        case contactSyncBanner(ContactSyncBanner.Action)
        case inviteFriendsBanner(InviteFriendsBanner.Action)
        case featuredArtistBanner(FeaturedArtistBanner.Action)
        case remixBanner(RemixBanner.Action)
        case getFeaturedArtistPlaylist(String)
        case showFeaturedArtistPlaylist
        case showRemixAnnouncement
        case searchTapped
        case notificationsTapped
        case scrollToTop
        case didScrollToTop
        case clipEvents(EventBusClient.ClipEvent)
        case setCurrentlyPreviewingClip(Clip)
        case updateVisibleItem(sectionIndex: Int, itemIndex: Int)
        case playCurrentlyPreviewingClip(Clip)
        case promoPrimaryCtaTapped(promoId: String)
        case promoSecondaryCtaTapped(promoId: String)
        case delegate(Delegate)

        public enum Delegate {
            case handleDeeplink(DeeplinkIntent) // Generic/fallback for backwards compatability
            case handlePromoSectionDeeplink(DeeplinkIntent, promoType: PromoItem.PromoType, promoId: String)
        }

        public enum Internal {
            case feedResponse(Result<DiscoverFeed, Error>)
            case getNextPage
            case getNextPageResponse(Result<DiscoverFeed, Error>)
            case playlistResponse(Result<Playlist, Error>)
            case refreshRemixPermState
            case updateRemixPermission
        }
    }

    @Dependency(APIClientV2.self) var apiClientV2
    @Dependency(\.telemetryClient) var telemetry
    @Dependency(SunoModelClient.self) var sunoModelClient
    @Dependency(NavigationRouterClient.self) var navigationRouter
    @Dependency(\.eventBus.getOmniplayerChannel) private var getOmniplayerChannel
    @Dependency(\.eventBus.getClipPublisher) private var getClipPublisher
    @Dependency(VideoCoverClient.self) var videoCoverClient
    @Dependency(\.userConfigClient) var userConfigClient

    public init() {}

    public var body: some ReducerOf<Self> {
        Scope(state: \.contactSyncBanner, action: \.contactSyncBanner) {
            ContactSyncBanner()
        }
        Scope(state: \.inviteFriendsBanner, action: \.inviteFriendsBanner) {
            InviteFriendsBanner()
        }
        Scope(state: \.featuredArtistBanner, action: \.featuredArtistBanner) {
            FeaturedArtistBanner()
        }
        Scope(state: \.remixBanner, action: \.remixBanner) {
            RemixBanner()
        }
        Reduce<State, Action> { state, action in
            switch action {
            case .task:
                if state.banner == .featuredArtist {
                    return .concatenate(
                        .send(.getDiscoverFeed(pullToRefresh: false)),
                        .send(.getFeaturedArtistPlaylist(State.Constants.featureArtistTimbalandPlaylistId))
                    )
                    .merge(with: .send(.internal(.refreshRemixPermState)))
                    .merge(with: .subscribe(getClipPublisher(), send: Action.clipEvents))
                } else {
                    return .send(.getDiscoverFeed(pullToRefresh: false))
                        .merge(with: .send(.internal(.refreshRemixPermState)))
                        .merge(with: .subscribe(getClipPublisher(), send: Action.clipEvents))
                }

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

            case .inviteFriendsBanner(.delegate(.dismissTapped)):
                state.$dismissedInviteFriendsDiscover.withLock { $0 = true }
                state.didDismissBannerThisSession = true
                return .none

            case .contactSyncBanner(.delegate(.bannerTappedAndAuthorized)):
                state.destination = .fromYourContacts(.init(me: state.me))
                return .none

            case .contactSyncBanner(.delegate(.dismissTapped)):
                state.$dismissedContactSyncDiscover.withLock { $0 = true }
                state.didDismissBannerThisSession = true
                return .none

            case .featuredArtistBanner(.delegate(.bannerTapped)):
                return .send(.showFeaturedArtistPlaylist)

            case .featuredArtistBanner(.delegate(.dismissTapped)):
                state.$dismissedFeaturedArtistDiscover.withLock { $0 = true }
                state.didDismissBannerThisSession = true
                return .none

            case .remixBanner(.delegate(.dismissTapped)):
                state.dismissedRemixPermDiscover = true
                state.didDismissBannerThisSession = true
                return .none

            case .remixBanner(.delegate(.bannerTapped)):
                return .send(.showRemixAnnouncement, animation: .default)

            case .internal(.refreshRemixPermState):
                guard FeatureFlag.create.remixPerm else { return .none }
                guard !state.hasSetRemixPerm.isLoading else { return .none }

                state.hasSetRemixPerm = .loading
                return .run { send in
                    await userConfigClient.fetch()
                    await send(.internal(.updateRemixPermission))
                }

            case .internal(.updateRemixPermission):
                state.hasSetRemixPerm = .done(.success(userConfigClient.userConfig?.hasSetRemixPerm))
                return .none

            case .showRemixAnnouncement:
                /* Basically a delegate method to root */
                return .none

            case .showFeaturedArtistPlaylist:
                guard let playlist = state.featuredArtistPlaylist else {
                    // In case we fail to load playlist we show profile as fallback
                    return .send(.authorTapped(State.Constants.featureArtistTimbalandHandle, nil, nil))
                }
                navigationRouter.sendIfNavV2(route: .playlist(playlist), else: {
                    state.destination = .playlistDetail(.init(me: state.$me, source: .playlist(playlist)))
                })
                return .none

            case .getDiscoverFeed(let pullToRefresh):
                // If this is a manual pull-to-refresh, skip the time check
                if !pullToRefresh,
                   let lastDiscoverUpdateAt = state.lastDiscoverUpdateAt,
                   state.feed != .skeleton
                {
                    let secondsFromLastUpdate = Date.now.timeIntervalSince1970 - lastDiscoverUpdateAt.timeIntervalSince1970
                    // Only refresh if it's been more than 5 minutes
                    if secondsFromLastUpdate < State.Constants.refreshInterval { return .none }
                }

                // Reset the page for pull to refresh
                if pullToRefresh {
                    state.currentPage = 0
                    state.startIndex = 0
                    state.feed = .skeleton
                    state.currentlyPreviewingClip = nil
                    state.horizontalIndices.removeAll()
                    state.refreshId = UUID() // Generate new UUID to force scroll reset
                    videoCoverClient.removeCurrentPreviewItem()
                }

                state.loadingState = .firstPage

                return .run { [pageSize = state.pageSize] send in
                    await send(.internal(.feedResponse(.init(catching: { try await apiClientV2.getDiscoverFeed(0, pageSize) }))))
                }

            case .internal(.feedResponse(.success(let feed))):
                state.loadingState = .idle
                state.feed = feed
                // Filter out promo sections if the flag is off
                if !FeatureFlag.promo.q22025RemixContests {
                    state.feed.sections = state.feed.sections.filter { section in
                        if case .promo = section { return false }
                        return true
                    }
                }
                state.$lastDiscoverUpdateAt.withLock { $0 = Date() }
                // Only override pageSize, if needed, on the first page
                // We expect this to stay constant at 3, however this is in case
                // that changes in the future
                state.pageSize = feed.pageSize
                // Set initial preview clip from the first playlist section
                for section in feed.sections {
                    if case .playlist(let playlistSection) = section,
                       let firstClip = playlistSection.items.first
                    {
                        return .send(.setCurrentlyPreviewingClip(firstClip))
                    }
                }

                return .none

            case .internal(.feedResponse(.failure(let error))):
                state.loadingState = .failed
                log.telemetry.error(error)
                // TODO: Handle error better
                return .none

            case .internal(.getNextPage):
                // Make sure we're not already loading the next page
                // and we have more pages to load
                guard state.canLoadMorePages else {
                    return .none
                }
                state.loadingState = .nextPage

                return .run { [pageSize = state.pageSize, startIndex = state.startIndex] send in
                    let newStartIndex = startIndex + pageSize
                    try await Task.sleep(for: .seconds(0.1))
                    await send(.internal(.getNextPageResponse(
                        .init(catching: {
                            try await apiClientV2.getDiscoverFeed(newStartIndex, pageSize)
                        })
                    )))
                }

            case .internal(.getNextPageResponse(.success(let feed))):
                // Only set the current page after a successful response
                state.loadingState = .idle
                state.currentPage += 1
                state.startIndex = feed.sectionIndex
                state.feed.sections.append(contentsOf: feed.sections)
                return .none

            case .internal(.getNextPageResponse(.failure(let error))):
                state.loadingState = .idle
                log.telemetry.error(error)
                return .none

            case .getFeaturedArtistPlaylist(let playlistId):
                return .run { send in
                    await send(.internal(.playlistResponse(.init(catching: { try await apiClientV2.getPlaylistById(0, playlistId, nil, nil) }))))
                }

            case .internal(.playlistResponse(.success(let playlist))):
                state.featuredArtistPlaylist = playlist
                return .none

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

            case .playlistMoreTapped(let section):
                if State.Constants.trendingIds.contains(section.id) {
                    navigationRouter.sendIfNavV2(route: .trendingPlaylistSection(section), else: {
                        state.destination = .trending(.init(section: section, me: state.$me))
                    })
                } else if section.id == PlaylistSection.Constants.continueListeningSectionId {
                    navigationRouter.sendIfNavV2(route: .listenHistory, else: {
                        state.destination = .playlistDetail(.init(me: state.$me, source: .section(section)))
                    })
                } else if let link = section.link,
                          link.contains(State.Constants.RedirectTo.stylePlaylist)
                {
                    // If we're seeing a playlist that has a URL with `/style/{styleName}`,
                    // map it to a StyleSection with a Genre object since it's not actually a playlist.
                    let parts = link.components(separatedBy: "/")
                    guard !parts.isEmpty, let name = parts.last else { return .none }
                    let genre = Genre(
                        id: section.id,
                        name: name.capitalized,
                        image: section.items.first?.largeImageUrl ?? ""
                    )
                    let styleItem = StyleItem(
                        id: section.id,
                        name: name.capitalized,
                        imageUrl: section.items.first?.largeImageUrl ?? "",
                        redirectUrl: link
                    )
                    navigationRouter.sendIfNavV2(route: .genreDetailSection(styleItem), else: {
                        state.destination = .playlistDetail(.init(me: state.$me, source: .genre(genre)))
                    })
                } else if let playlistId = section.playlistId {
                    // Real playlist → navigate to PlaylistDetailScreen
                    navigationRouter.sendIfNavV2(route: .playlistWithId(playlistId), else: {
                        state.destination = .playlistDetail(.init(me: state.$me, source: .playlistId(playlistId)))
                    })
                } else {
                    state.destination = .playlistDetail(.init(me: state.$me, source: .section(section)))
                }
                return .none

            case .playlistListMoreTapped(let section):
                navigationRouter.sendIfNavV2(route: .playlistListSection(section), else: {
                    state.destination = .playlists(.init(section: section, me: state.$me))
                })
                return .none

            case .selectStyle(let section):
                // Most of the sections returned from new discover API with `platform = mobile` are of type `style_list` which contains `redirectUrl` which can be used to send user to specific destinations
                if let redirectUrl = section.redirectUrl?.dropFirst().replacingOccurrences(of: "sections/", with: "") {
                    let parts = redirectUrl.components(separatedBy: "/")
                    if let redirectTo = parts.first {
                        switch redirectTo {
                        case State.Constants.RedirectTo.playlists:
                            guard let playlistId = parts.last else { return .none }
                            navigationRouter.sendIfNavV2(route: .playlistWithId(playlistId), else: {
                                state.destination = .playlistDetail(.init(me: state.$me, source: .playlistId(playlistId)))
                            })

                        case PlaylistSection.Constants.followingFeedSectionId:
                            let followingFeed = PlaylistSection(
                                id: section.id,
                                title: section.name,
                                items: [],
                                previewItemsCount: 0
                            )
                            state.destination = .playlistDetail(.init(me: state.$me, source: .section(followingFeed)))

                        case PlaylistSection.Constants.continueListeningSectionId:
                            let listenHistory = PlaylistSection(
                                id: section.id,
                                title: section.name,
                                items: [],
                                previewItemsCount: 0
                            )
                            navigationRouter.sendIfNavV2(route: .listenHistory, else: {
                                state.destination = .playlistDetail(.init(me: state.$me, source: .section(listenHistory)))
                            })

                        case State.Constants.RedirectTo.likedSongs:
                            navigationRouter.sendIfNavV2(route: .likedSongs, else: {
                                state.destination = .likedSongs(.init(me: state.$me))
                            })

                        case State.Constants.RedirectTo.likedPlaylists:
                            navigationRouter.sendIfNavV2(route: .likedPlaylists, else: {
                                state.destination = .likedPlaylists(.init(me: state.$me))
                            })

                        case State.Constants.RedirectTo.trendingSongs:
                            let trendingSection = PlaylistSection(
                                id: section.id,
                                title: section.name,
                                items: [],
                                previewItemsCount: 0
                            )
                            navigationRouter.sendIfNavV2(route: .trendingPlaylistSection(trendingSection), else: {
                                state.destination = .trending(
                                    .init(
                                        section: trendingSection,
                                        me: state.$me
                                    )
                                )
                            })

                        case State.Constants.RedirectTo.genre:
                            guard let name = parts.last else { return .none }
                            let genre = Genre(
                                id: section.id,
                                name: name,
                                image: section.imageUrl
                            )
                            navigationRouter.sendIfNavV2(route: .genreDetailSection(section), else: {
                                state.destination = .playlistDetail(.init(me: state.$me, source: .genre(genre)))
                            })

                        default:
                            break
                        }
                    }
                } else {
                    // If no resource URL was provided assume genre tap
                    let genre = Genre(id: section.id, name: section.name, image: section.imageUrl)
                    navigationRouter.sendIfNavV2(route: .genreDetailSection(section), else: {
                        state.destination = .playlistDetail(.init(me: state.$me, source: .genre(genre)))
                    })
                }
                return .none

            case .updateClip(let clip):
                var feed = state.feed
                for (index, section) in feed.sections.enumerated() {
                    if case var .playlist(playlistSection) = section {
                        playlistSection.updateClip(clip)
                        feed.sections[index] = .playlist(playlistSection)
                    }
                }
                state.feed = feed

                guard let destination = state.destination else { return .none }

                switch destination {
                case .playlistDetail:
                    return .send(.destination(.presented(.playlistDetail(.updateClip(clip)))))
                case .trending:
                    return .send(.destination(.presented(.trending(.updateClip(clip)))))
                case .likedSongs:
                    return .send(.destination(.presented(.likedSongs(.updateClip(clip)))))
                case .likedPlaylists:
                    return .send(.destination(.presented(.likedPlaylists(.destination(.presented(.playlistDetail(.updateClip(clip))))))))
                case .profile:
                    return .send(.destination(.presented(.profile(.clipList(.updateClip(clip))))))
                case .playlists, .invite, .fromYourContacts:
                    return .none
                }

            case .deleteClip(let clip):
                var feed = state.feed
                for (index, section) in feed.sections.enumerated() {
                    if case var .playlist(playlistSection) = section {
                        playlistSection.deleteClip(clip)
                        feed.sections[index] = .playlist(playlistSection)
                    }
                }
                state.feed = feed

                guard let destination = state.destination else { return .none }

                switch destination {
                case .playlistDetail:
                    return .send(.destination(.presented(.playlistDetail(.deleteClip(clip)))))
                case .trending:
                    return .send(.destination(.presented(.trending(.deleteClip(clip)))))
                case .likedSongs:
                    return .send(.destination(.presented(.likedSongs(.updateClip(clip)))))
                case .likedPlaylists:
                    return .send(.destination(.presented(.likedPlaylists(.destination(.presented(.playlistDetail(.updateClip(clip))))))))
                case .profile:
                    return .send(.destination(.presented(.profile(.clipList(.deleteClip(clip))))))
                case .playlists, .invite, .fromYourContacts:
                    return .none
                }

            case .authorTapped(let handle, let displayName, let avatarImageUrl):
                navigationRouter.send(.profile(handle, displayName: displayName, avatarImageUrl: avatarImageUrl, recommendationMetadata: nil))
                return .none

            case .selectClip(let clip, let queue, let context):
                getOmniplayerChannel().queue(.playClip(clip, queue: queue, context: context))
                return .none

            case .selectPlaylist(let playlist):
                navigationRouter.sendIfNavV2(route: .playlist(playlist), else: {
                    state.destination = .playlistDetail(.init(me: state.$me, source: .playlist(playlist)))
                })
                return .none

            case .searchTapped:
                navigationRouter.send(route: .search(.publicSong))
                return .none

            case .notificationsTapped:
                navigationRouter.send(route: .notifications)
                return .none

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

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

            // These are all the events at the moment, but are also explicitly the ones we care about so let's list them individually to be clear for the future
            case .clipEvents(.updateClip(let clip)),
                 .clipEvents(.removeClip(let clip)),
                 .clipEvents(.removeClipFromPlaylist(let clip, _)),
                 .clipEvents(.toggledLike(let clip)):
                return self.reduce(into: &state, action: .updateClip(clip))

            case .destination, .contactSyncBanner, .clipEvents:
                /* Catch All */
                return .none

            case .setCurrentlyPreviewingClip(let clip):
                guard FeatureFlag.clips.videoPreviewsOnDiscoverScreen else { return .none }
                videoCoverClient.pausePreview()
                if let previewUrl = clip.videoCoverPreviewUrl,
                   let url = URL(string: previewUrl)
                {
                    return .run { send in
                        _ = await videoCoverClient.replaceCurrentPreviewItem(url)
                        videoCoverClient.playPreview()
                        await send(.playCurrentlyPreviewingClip(clip))
                    }
                } else {
                    state.currentlyPreviewingClip = nil
                    videoCoverClient.removeCurrentPreviewItem()
                    return .none
                }

            case .playCurrentlyPreviewingClip(let clip):
                state.currentlyPreviewingClip = clip
                videoCoverClient.playPreview()
                // Cache the larger video
                guard let coverUrl = clip.videoCoverUrl,
                      let url = URL(string: coverUrl) else { return .none }
                videoCoverClient.cacheVideoCover(url)
                return .none

            case .updateVisibleItem(let sectionIndex, let itemIndex):
                state.horizontalIndices[sectionIndex] = itemIndex
                return .none

            case .promoPrimaryCtaTapped(let promoId):
                guard let section = state.feed.sections.first(where: { section in
                    if case .promo = section { return true }
                    return false
                }),
                    case .promo(let promoSection) = section,
                    let promoItem = promoSection.items.first(where: { $0.id == promoId }),
                    let primaryCta = promoItem.primaryCta,
                    let primaryCtaUrl = primaryCta.url,
                    let url = URL(string: primaryCtaUrl)
                else { return .none }

                // Check if this is a suno.com URL that can be handled internally
                if let deeplinkIntent = DeeplinkIntent.createDeeplinkIntent(from: url) {
                    // Handle as an explicit source in the RootCoordinator if we have a promo type
                    if let promoType = promoItem.type {
                        return .send(.delegate(.handlePromoSectionDeeplink(deeplinkIntent, promoType: promoType, promoId: promoId)))
                    } else {
                        return .send(.delegate(.handleDeeplink(deeplinkIntent)))
                    }
                } else {
                    // Fallback to external URL
                    return .run { _ in
                        await MainActor.run {
                            UIApplication.shared.open(url)
                        }
                    }
                }

            case .promoSecondaryCtaTapped(let promoId):
                guard let section = state.feed.sections.first(where: { section in
                    if case .promo = section { return true }
                    return false
                }),
                    case .promo(let promoSection) = section,
                    let promoItem = promoSection.items.first(where: { $0.id == promoId }),
                    let secondaryCta = promoItem.secondaryCta,
                    let secondaryCtaUrl = secondaryCta.url,
                    let url = URL(string: secondaryCtaUrl)
                else { return .none }

                // Check if this is a suno.com URL that can be handled internally
                if let deeplinkIntent = DeeplinkIntent.createDeeplinkIntent(from: url) {
                    if let promoType = promoItem.type {
                        return .send(.delegate(.handlePromoSectionDeeplink(deeplinkIntent, promoType: promoType, promoId: promoId)))
                    } else {
                        return .send(.delegate(.handleDeeplink(deeplinkIntent)))
                    }
                } else {
                    // Fallback to external URL
                    return .run { _ in
                        await MainActor.run {
                            UIApplication.shared.open(url)
                        }
                    }
                }

            case .delegate:
                return .none
            }
        }
        .ifLet(\.$destination, action: \.destination)

        Analytics()
    }
}

public struct DiscoverScreen: View {
    @Bindable private var store: StoreOf<Discover>
    @Namespace private var scrollSpace

    @Environment(\.colorScheme) private var colorScheme

    private let horizontalContentMargin: CGFloat = Discover.State.Constants.horizontalContentMargin

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

    public var body: some View {
        root
            .navigationDestination(item: $store.scope(state: \.destination?.trending, action: \.destination.trending)) { store in
                TrendingScreen(store: store)
            }
            .navigationDestination(item: $store.scope(state: \.destination?.playlistDetail, action: \.destination.playlistDetail)) { store in
                PlaylistDetailScreen(store: store)
                    .toolbar {
                        ToolbarItem(placement: .topBarLeading) {
                            ToolbarButton(.back, background: Material.ultraThin, colorScheme: store.averageColors.colorScheme) {
                                store.send(.dismiss)
                            }
                        }
                    }
            }
            .navigationDestination(item: $store.scope(state: \.destination?.playlists, action: \.destination.playlists)) { store in
                PlaylistsScreen(store: store)
            }
            .overlay(alignment: .top) {
                if #available(iOS 26.0, *) {
                    LinearGradient(colors: [Color.SemanticV1.backgroundPrimary,
                                            Color.SemanticV1.backgroundPrimary.opacity(0.75),
                                            Color.SemanticV1.backgroundPrimary.opacity(0)], startPoint: .top, endPoint: .bottom)
                        .frame(height: 140)
                        .ignoresSafeArea()
                        .allowsHitTesting(false)
                }
            }
            .toolbar {
                ToolbarItem(placement: .primaryAction) {
                    // Only show the notifications here if we don't have it in the Tab Bar,
                    // like in the Hooks tab bar
                    if FeatureFlag.hooks.isFeedEnabled {
                        ToolbarButton(.notifications, background: Color.SemanticV1.backgroundQuaternary) {
                            store.send(.notificationsTapped)
                        }
                        .notificationToolbarBadge()
                    }
                }

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

                ToolbarItem(placement: .primaryAction) {
                    ToolbarButton(.search, background: Color.SemanticV1.backgroundQuaternary) {
                        store.send(.searchTapped)
                    }
                }
            }
            .toolbarColorScheme(colorScheme)
    }

    private var root: some View {
        ZStack(alignment: .top) {
            if store.feed.sections.isEmpty, store.feed != .skeleton {
                emptyRoot
            } else {
                content
            }
        }
        .background(Color.SemanticV1.backgroundPrimary)
        .sheet(item: $store.scope(state: \.destination?.invite, action: \.destination.invite)) { store in
            ShareScreen(store: store)
        }
        .fullScreenCover(item: $store.scope(state: \.destination?.fromYourContacts, action: \.destination.fromYourContacts)) { store in
            NavigationStack {
                FromYourContactsScreen(store: store)
                    .toolbar {
                        ToolbarItem(placement: .topBarTrailing) {
                            ToolbarButton(.close, background: Color.SemanticV1.backgroundQuaternary) {
                                store.send(.dismiss)
                            }
                        }
                    }
            }
        }
        .sheet(item: $store.scope(state: \.destination?.invite, action: \.destination.invite)) { store in
            ShareScreen(store: store)
        }
    }

    private var emptyRoot: some View {
        VStack {
            Spacer()
            Text(L10n.FeatureDiscover.noResults)
                .typographyV1(.headline4)
                .foregroundColor(.SemanticV1.textSecondary)
            Spacer()
        }
        .frame(maxWidth: .infinity)
    }

    private var content: some View {
        ScrollViewReader { proxy in
            ScrollView {
                LazyVStack(spacing: 16) {
                    Group {
                        if store.banner == .remixability {
                            RemixBannerView(store: store.scope(state: \.remixBanner, action: \.remixBanner))
                        }
                        if store.banner == .contactSync {
                            ContactSyncBannerView(store: store.scope(state: \.contactSyncBanner, action: \.contactSyncBanner))
                        }
                        if store.banner == .inviteFriends {
                            InviteFriendsBannerView(store: store.scope(state: \.inviteFriendsBanner, action: \.inviteFriendsBanner))
                        }
                    }
                    .padding(.bottom, 24)
                    .padding(.horizontal, horizontalContentMargin)
                    .id(scrollSpace)

                    if store.feed == .skeleton {
                        skeleton
                    } else {
                        loaded
                    }
                }
                .padding(.vertical, 12)
            }
            .onChange(of: store.shouldScrollToTop) { _, _ in
                withAnimation {
                    proxy.scrollTo(scrollSpace, anchor: .top)
                }
                store.send(.didScrollToTop)
            }
            .stableRefreshable { didPullToRefresh in
                guard let didPullToRefresh = didPullToRefresh else { return }
                await store.send(.getDiscoverFeed(pullToRefresh: didPullToRefresh)).finish()
            }
            .scrollIndicators(.hidden, axes: .vertical)
            .scrollBounceBehavior(.basedOnSize)
        }
        .navigationDestination(item: $store.scope(state: \.destination?.likedSongs, action: \.destination.likedSongs)) { store in
            LikedSongsScreen(store: store)
        }
        .navigationDestination(item: $store.scope(state: \.destination?.profile, action: \.destination.profile)) { store in
            PublicProfileScreenV1(store: store)
        }
        .navigationDestination(item: $store.scope(state: \.destination?.likedPlaylists, action: \.destination.likedPlaylists)) { store in
            LikedPlaylistsScreen(store: store)
        }
    }

    private var progressIndicator: some View {
        ProgressView()
            .progressViewStyle(CircularProgressViewStyle())
            .frame(maxWidth: .infinity)
            .padding(.vertical, 24)
    }

    private func userListView(_: UserListSection) -> some View {
        EmptyView()
    }

    @ViewBuilder
    private func playlistView(_ section: PlaylistSection, showMoreAction: @escaping () -> Void) -> some View {
        if section.isTrending {
            playlistViewSmall(section, showMoreAction: showMoreAction)
        } else {
            playlistViewLarge(section, showMoreAction: showMoreAction)
        }
    }

    private func playlistViewLarge(_ section: PlaylistSection, showMoreAction: @escaping () -> Void) -> some View {
        let header = header(
            title: section.title,
            action: section.items.count > Discover.State.Constants.maxDisplayedClips || section.isTrending ? showMoreAction : nil
        )

        return Section(header: header) {
            if section.items.isEmpty {
                Text(L10n.FeatureDiscover.noSongs)
                    .typographyV1(.body1)
                    .foregroundStyle(Color.SemanticV1.textTertiary)
            } else {
                let items = Array(section.items.prefix(Discover.State.Constants.maxDisplayedClips))
                let context = SessionContext(source: .discoverCarousel(id: section.id))
                ScrollView(.horizontal) {
                    LazyHStack(spacing: 8) {
                        ForEach(items) { clip in
                            clipCell(clip, queue: section.items, context: context)
                                .containerRelativeFrame(.horizontal) { width, _ in
                                    width / 2.15
                                }
                        }

                        Spacer(minLength: 80)
                    }
                    .scrollTargetLayout()
                }
                .scrollClipDisabled()
                .scrollTargetBehavior(.viewAligned)
                .scrollIndicators(.hidden, axes: .horizontal)
                .contentMargins(.horizontal, horizontalContentMargin, for: .scrollContent)
            }
        }
    }

    private func playlistViewSmall(_ section: PlaylistSection, showMoreAction: @escaping () -> Void) -> some View {
        let header = header(
            title: section.title,
            action: section.items.count > Discover.State.Constants.maxDisplayedClips || section.isTrending ? showMoreAction : nil
        )

        return Section(header: header) {
            if section.items.isEmpty {
                Text(L10n.FeatureDiscover.noSongs)
                    .typographyV1(.body1)
                    .foregroundStyle(Color.SemanticV1.textTertiary)
            } else {
                let items = Array(section.items.prefix(Discover.State.Constants.maxDisplayedClips))
                let enumeratedItems = Array(items.enumerated())
                let contextId: String = {
                    if section.isTrending {
                        let language = section.selectedOption ?? "undefined"
                        let rankBy = section.secondarySelectedOption ?? "undefined"
                        return "\(section.id)-\(language)-\(rankBy)"
                    } else {
                        return section.id
                    }
                }()
                let context = SessionContext(source: .featuredFeed(id: contextId))

                ScrollView(.horizontal) {
                    LazyHGrid(rows: [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())], spacing: 16) {
                        ForEach(enumeratedItems, id: \.offset) {
                            offset,
                                clip in
                            VStack(spacing: 0) {
                                Button(
                                    action: { store.send(.selectClip(clip, queue: items, context: context)) },
                                    label: {
                                        ClipListItemContent(
                                            me: store.$me,
                                            clip: clip,
                                            showPin: false,
                                            authorTapped: {
                                                _,
                                                    _,
                                                    _ in store
                                                        .send(
                                                            .authorTapped(
                                                                clip.handle,
                                                                clip.displayName,
                                                                clip.avatarImageUrl
                                                            )
                                                        )
                                            }
                                        )
                                    }
                                )
                                .contentShape(.rect)
                                .buttonStyle(ScaleButtonStyle(scaleAmount: 0.98))

                                if offset % 3 != 2 {
                                    Divider()
                                        .overlay(Color.SemanticV1.borderPrimary)
                                }
                            }
                            .containerRelativeFrame(.horizontal) { width, _ in
                                width - 20
                            }
                        }
                    }
                    .scrollTargetLayout()
                }
                .scrollClipDisabled()
                .scrollTargetBehavior(.viewAligned)
                .scrollIndicators(.hidden, axes: .horizontal)
                .contentMargins(.horizontal, horizontalContentMargin, for: .scrollContent)
            }
        }
    }

    private func playlistListView(_ section: PlaylistListSection, showMoreAction: @escaping () -> Void) -> some View {
        let header = header(title: section.title, action: showMoreAction)

        return Section(header: header) {
            if section.items.isEmpty {
                Text(L10n.FeatureDiscover.noPlaylists)
                    .typographyV1(.body1)
                    .foregroundStyle(Color.SemanticV1.textTertiary)
            } else {
                let columns = [GridItem(.adaptive(minimum: 160))]
                let items = Array(section.items.prefix(6))

                LazyVGrid(columns: columns, spacing: 24) {
                    ForEach(items) { playlist in
                        playlistCell(playlist)
                            .placeholder(isVisible: store.showPlaceholder)
                            .placeholderShimmering(isVisible: store.isShimmering)
                    }
                }
                .padding(.horizontal, horizontalContentMargin)
            }
        }
    }

    private func styleView(_ section: StyleListSection) -> some View {
        Section(header: header(title: section.title)) {
            ScrollView(.horizontal) {
                LazyHStack(spacing: 8) {
                    ForEach(section.items, id: \.id) { style in
                        Button {
                            store.send(.selectStyle(style))
                        } label: {
                            StyleCard(style: style)
                                .colorScheme(.dark)
                                .containerRelativeFrame(.horizontal) { width, _ in width * 0.47 }
                        }
                        .buttonStyle(ScaleButtonStyle(scaleAmount: 0.98))
                        .placeholder(isVisible: store.showPlaceholder)
                        .placeholderShimmering(isVisible: store.isShimmering)
                    }
                }
                .scrollTargetLayout()
            }
            .scrollClipDisabled()
            .scrollTargetBehavior(.viewAligned)
            .scrollIndicators(.hidden, axes: .horizontal)
            .contentMargins(.horizontal, horizontalContentMargin, for: .scrollContent)
        }
    }

    @ViewBuilder
    private func promoView(_ section: PromoSection) -> some View {
        // Only show one promo card
        if let item = section.items.first {
            PromoCard(item: item, onPrimaryCtaTap: {
                store.send(.promoPrimaryCtaTapped(promoId: item.id))
            }, onSecondaryCtaTap: {
                store.send(.promoSecondaryCtaTapped(promoId: item.id))
            })
            .padding(.horizontal, horizontalContentMargin)
        }
    }

    @ViewBuilder
    private func clipCell(_ clip: Clip, queue: [Clip], context: SessionContext) -> some View {
        if FeatureFlag.legacy.videoSongCover, FeatureFlag.clips.videoPreviewsOnDiscoverScreen {
            PreviewableClipCard(
                title: clip.title,
                tags: clip.tags,
                imageUrl: clip.imageUrl,
                videoCoverUrl: clip.videoCoverUrl,
                videoCoverPreviewUrl: clip.videoCoverPreviewUrl,
                playCount: clip.playCount,
                upvoteCount: clip.localUpvoteCount,
                isLiked: clip.isLiked,
                commentCount: clip.commentCount,
                avatarImageUrl: clip.avatarImageUrl,
                displayName: clip.displayName,
                userId: clip.userId,
                clipId: clip.id.remoteId,
                isLoading: store.showPlaceholder,
                didFailToLoad: store.loadingState == .failed,
                isVideoCoverPlaying: store.currentlyPreviewingClip?.id == clip.id
            ) {
                store.send(.selectClip(clip, queue: queue, context: context))
            } authorTappedAction: {
                store.send(.authorTapped(clip.handle, clip.displayName, clip.avatarImageUrl))
            }

        } else {
            ClipCard(
                title: clip.title,
                tags: clip.tags,
                imageUrl: clip.imageUrl,
                playCount: clip.playCount,
                upvoteCount: clip.localUpvoteCount,
                isLiked: clip.isLiked,
                commentCount: clip.commentCount,
                avatarImageUrl: clip.avatarImageUrl,
                displayName: clip.displayName,
                userId: clip.userId,
                clipId: clip.id.remoteId,
                isLoading: store.showPlaceholder,
                didFailToLoad: store.loadingState == .failed
            ) {
                store.send(.selectClip(clip, queue: queue, context: context))
            } authorTappedAction: {
                store.send(.authorTapped(clip.handle, clip.displayName, clip.avatarImageUrl))
            }
        }
    }

    private func playlistCell(_ playlist: Playlist) -> some View {
        Button {
            store.send(.selectPlaylist(playlist))
        } label: {
            PlayListCardV1(
                image: .remote(url: playlist.imageUrl, fallbackId: playlist.id),
                title: playlist.name,
                tags: playlist.description,
                avatarImageUrl: playlist.userAvatarImageUrl,
                displayName: playlist.userDisplayName ?? playlist.userHandle ?? "",
                plays: playlist.playCount ?? 0,
                likes: playlist.upvoteCount ?? 0
            )
        }
        .buttonStyle(ScaleButtonStyle(scaleAmount: 0.98))
    }

    private func header(title: String, action: (() -> Void)? = nil) -> some View {
        HStack(spacing: 0) {
            Text(title)
                .typographyV1(.headline4Wide)
                .foregroundStyle(Color.SemanticV1.textPrimary)
                .placeholder(isVisible: store.showPlaceholder)
                .placeholderShimmering(isVisible: store.isShimmering)

            Spacer()

            if let action {
                Button(action: action) {
                    HStack(spacing: 4) {
                        Text(L10n.FeatureDiscover.showMore)
                            .typographyV1(.body1)
                        Image.Icon.arrowRight
                    }
                    .foregroundStyle(Color.SemanticV1.textBrand)
                    .placeholder(isVisible: store.showPlaceholder)
                    .placeholderShimmering(isVisible: store.isShimmering)
                }
            }
        }
        .padding(.horizontal, horizontalContentMargin)
    }

    private var divider: some View {
        Divider()
            .overlay(Color.SemanticV1.borderPrimary)
            .padding(.vertical, 8)
    }

    @ViewBuilder
    private var loaded: some View {
        ForEach(store.feed.sections) { section in
            switch section {
            case .playlist(let section):
                playlistView(section) {
                    store.send(.playlistMoreTapped(section))
                }

            case .playlistList(let section):
                playlistListView(section) {
                    store.send(.playlistListMoreTapped(section))
                }

            case .style(let section):
                styleView(section)

            case .userList(let section):
                userListView(section)

            case .promo(let section):
                promoView(section)
            }

            let showDivider: Bool = {
                if case .userList = section { return false }
                if case .promo = section { return false }
                return section != store.feed.sections.last && !section.isEmpty
            }()

            if showDivider {
                divider
            }
        }

        if store.hasMoreSections {
            progressIndicator
                .opacity(store.loadingState == .nextPage ? 1 : 0)
                .onAppear {
                    store.send(.internal(.getNextPage))
                }
        }
    }

    @ViewBuilder
    private var skeleton: some View {
        ForEach(store.feed.sections) { section in
            switch section {
            case .playlist(let section):
                playlistView(section) {}

            case .playlistList(let section):
                playlistListView(section) {}

            case .style(let section):
                styleView(section)

            case .userList(let section):
                // TODO: No designs. Needs implementation.
                userListView(section)

            case .promo(let section):
                promoView(section)
            }

            let showDivider: Bool = {
                if case .userList = section { return false }
                return section != store.feed.sections.last && !section.isEmpty
            }()

            if showDivider {
                divider
            }
        }
    }
}

public extension PlaylistSection {
    mutating func updateClip(_ clip: Clip) {
        if let index = items.firstIndex(where: { $0.id == clip.id }) {
            items[index] = clip
        }
    }

    mutating func deleteClip(_ clip: Clip) {
        if let index = items.firstIndex(where: { $0.id == clip.id }) {
            items.remove(at: index)
        }
    }
}

extension PlaylistSection {
    var isTrending: Bool { Discover.State.Constants.trendingIds.contains(id) }
}

extension PlaylistListSection {
    var isTrending: Bool { Discover.State.Constants.trendingIds.contains(id) }
}

extension StyleListSection {
    var isTrending: Bool { Discover.State.Constants.trendingIds.contains(id) }
}

#if DEBUG
    #Preview {
        // swiftformat:disable:next redundantLet
        let _ = prepareDependencies {
            $0[APIClientV2.self].getDiscoverFeed = { @Sendable _, _ in
                DiscoverFeed(
                    sectionIndex: 0,
                    sections: [
                        .playlist(PlaylistSection(id: "1", title: "previews are great", items: [Clip.mock()], previewItemsCount: 3)),
                    ],
                    totalSections: 1,
                    pageSize: 1
                )
            }
        }

        DiscoverScreen(store: .init(
            initialState: Discover.State(
                me: Shared(value: Me(models: [], roles: [:], flags: [:], user: User.mock()))
            ),
            reducer: { Discover() }
        ))
    }
#endif
