//
//  InstanceBrowseView.swift
//  Yattee
//
//  Displays content (Popular/Trending) for a specific backend instance.
//

import SwiftUI

struct InstanceBrowseView: View {
    @Environment(\.appEnvironment) private var appEnvironment

    let instance: Instance
    let initialTab: BrowseTab?

    @Namespace private var sheetTransition
    @State private var selectedTab: BrowseTab = .popular
    @State private var popularVideos: [Video] = []
    @State private var trendingVideos: [Video] = []
    @State private var isLoading = true
    @State private var errorMessage: String?

    // Search state (managed by SearchViewModel)
    @State private var searchText = ""
    @State private var searchViewModel: SearchViewModel?
    @State private var showFilterSheet = false

    // Feed state (for Invidious/Piped login)
    @State private var feedVideos: [Video] = []
    @State private var feedSubscriptions: [Channel] = []
    
    // Playlists state (for Invidious login)
    @State private var userPlaylists: [Playlist] = []
    @State private var selectedFeedChannelID: String?
    @State private var isLoggedIn = false
    @State private var feedPage = 1
    @State private var hasMoreFeedResults = true
    @State private var isLoadingMoreFeed = false
    @State private var contentLoadTask: Task<Void, Never>?
    @State private var feedLoadedVideoCount = 0  // Track count when last load was triggered
    
    // View options (persisted per instance)
    @AppStorage var layout: VideoListLayout
    @AppStorage var rowStyle: VideoRowStyle
    @AppStorage var gridColumnCount: Int
    @AppStorage var hideWatched: Bool
    
    // View options UI state
    @State private var showViewOptions = false
    @State private var viewWidth: CGFloat = 0
    @State private var watchEntriesMap: [String: WatchEntry] = [:]
    @State private var hasInitializedTab = false
    
    init(instance: Instance, initialTab: BrowseTab? = nil) {
        self.instance = instance
        self.initialTab = initialTab

        // Initialize AppStorage with instance-scoped keys
        _layout = AppStorage(wrappedValue: .list, "instanceBrowse.\(instance.id).layout")
        _rowStyle = AppStorage(wrappedValue: .regular, "instanceBrowse.\(instance.id).rowStyle")
        _gridColumnCount = AppStorage(wrappedValue: 2, "instanceBrowse.\(instance.id).gridColumns")
        _hideWatched = AppStorage(wrappedValue: false, "instanceBrowse.\(instance.id).hideWatched")
    }

    /// List style from centralized settings.
    private var listStyle: VideoListStyle {
        appEnvironment?.settingsManager.listStyle ?? .inset
    }

    /// The first enabled Yattee Server instance (for avatar URLs).
    private var yatteeServer: Instance? {
        appEnvironment?.instancesManager.enabledYatteeServerInstances.first
    }
    private var yatteeServerURL: URL? { yatteeServer?.url }

    /// Auth header for Yattee Server instances (when browsing a Yattee Server directly)
    private var yatteeServerAuthHeader: String? {
        guard instance.type == .yatteeServer else { return nil }
        return appEnvironment?.basicAuthCredentialsManager.basicAuthHeader(for: instance)
    }

    /// Auth header for avatar loading (uses Yattee Server for YouTube channel avatars)
    private var avatarAuthHeader: String? {
        guard let server = yatteeServer else { return nil }
        return appEnvironment?.basicAuthCredentialsManager.basicAuthHeader(for: server)
    }

    enum BrowseTab: String, CaseIterable, Identifiable {
        case popular
        case trending
        case feed
        case playlists

        var id: String { rawValue }

        var title: String {
            switch self {
            case .popular: return String(localized: "popular.title")
            case .trending: return String(localized: "trending.title")
            case .feed: return String(localized: "feed.title")
            case .playlists: return String(localized: "playlists.title")
            }
        }

        var systemImage: String {
            switch self {
            case .popular: return "flame"
            case .trending: return "chart.line.uptrend.xyaxis"
            case .feed: return "person.crop.rectangle.stack"
            case .playlists: return "play.square.stack"
            }
        }
    }

    private var viewOptionsSheetContent: some View {
        ViewOptionsSheet(
            layout: $layout,
            rowStyle: $rowStyle,
            gridColumns: $gridColumnCount,
            hideWatched: $hideWatched,
            maxGridColumns: gridConfig.maxColumns
        )
    }

    /// View options button lives on the leading edge on macOS, trailing elsewhere.
    private var viewOptionsPlacement: ToolbarItemPlacement {
        #if os(macOS)
        .navigation
        #else
        .primaryAction
        #endif
    }

    var body: some View {
        let backgroundStyle: ListBackgroundStyle = listStyle == .inset ? .grouped : .plain
        GeometryReader { geometry in
            #if os(tvOS)
            VStack(spacing: 0) {
                // tvOS: Search field, type filter, search filters, view options
                HStack(spacing: 24) {
                    TextField("instance.browse.search.placeholder", text: $searchText)
                        .textFieldStyle(.plain)
                        .onSubmit {
                            searchViewModel?.cancelSuggestions()
                            Task { await searchViewModel?.search(query: searchText) }
                        }

                    if isInSearchMode {
                        // Type filter
                        filterMenu(
                            title: (searchViewModel?.filters.type ?? .video).title,
                            selection: Binding(
                                get: { searchViewModel?.filters.type ?? .video },
                                set: { searchViewModel?.filters.type = $0 }
                            ),
                            options: SearchContentType.allCases,
                            labelForOption: { $0.title }
                        )

                        // Combined search filters menu
                        tvOSFiltersMenu
                    }

                    Button {
                        showViewOptions = true
                    } label: {
                        Label(String(localized: "viewOptions.title"), systemImage: "slider.horizontal.3")
                    }
                }
                .focusSection()
                .padding(.horizontal, 48)
                .padding(.top, 20)
                .padding(.bottom, 20)
                // Content
                ScrollView {
                    VStack(spacing: 0) {
                        Spacer()
                            .frame(height: 20)

                        // Tab picker (hidden during search)
                        if !isInSearchMode {
                            tabPicker
                                .padding()
                        }

                        // Content
                        Group {
                            if isInSearchMode, let vm = searchViewModel {
                                if !vm.hasSearched {
                                    if vm.suggestions.isEmpty {
                                        searchHintView
                                    } else {
                                        suggestionsView
                                    }
                                } else if vm.isSearching && !vm.hasResults {
                                    ProgressView()
                                        .frame(maxWidth: .infinity, minHeight: 200)
                                } else if let error = vm.errorMessage, !vm.hasResults {
                                    searchErrorView(error)
                                } else if vm.hasResults {
                                    searchResultsContent
                                } else {
                                    searchEmptyView
                                }
                            } else if selectedTab == .playlists {
                                if isLoading && userPlaylists.isEmpty {
                                    ProgressView()
                                        .frame(maxWidth: .infinity, minHeight: 200)
                                } else if let error = errorMessage, userPlaylists.isEmpty {
                                    errorView(error)
                                } else if !userPlaylists.isEmpty {
                                    switch layout {
                                    case .list:
                                        playlistsListContent
                                    case .grid:
                                        playlistsGridContent
                                    }
                                } else {
                                    playlistsEmptyView
                                }
                            } else if isLoading && currentVideos.isEmpty {
                                ProgressView()
                                    .frame(maxWidth: .infinity, minHeight: 200)
                            } else if let error = errorMessage, currentVideos.isEmpty {
                                errorView(error)
                            } else if !currentVideos.isEmpty {
                                switch layout {
                                case .list:
                                    listContent
                                case .grid:
                                    gridContent
                                }
                            } else {
                                emptyView
                            }
                        }
                    }
                }
                .scrollClipDisabled()
                .refreshable {
                    await startContentLoad(forceRefresh: true)
                }
                .focusSection()
            }
            .onChange(of: geometry.size.width, initial: true) { _, newWidth in
                viewWidth = newWidth
            }
            #else
            backgroundStyle.color
                .ignoresSafeArea()
                .overlay(
                    ScrollView {
                        VStack(spacing: 0) {
                            // Tab picker (hidden during search; lives in the toolbar on macOS)
                            #if !os(macOS)
                            if !isInSearchMode {
                                tabPicker
                                    .padding()
                            }
                            #endif

                            // Feed channel filter strip (hidden during search)
                            if selectedTab == .feed && !feedSubscriptions.isEmpty && !isInSearchMode {
                                feedChannelFilterStrip
                            }

                            // Search filter strip (shown persistently after search submitted; lives in the toolbar on macOS)
                            #if !os(macOS)
                            if isInSearchMode && (searchViewModel?.hasSearched ?? false) && instance.supportsSearchFilters {
                                searchFiltersStrip
                            }
                            #endif

                            // Content
                            Group {
                                if isInSearchMode, let vm = searchViewModel {
                                    // Search mode content
                                    if !vm.hasSearched {
                                        // Typing but not yet submitted - show suggestions or hint
                                        if vm.suggestions.isEmpty {
                                            searchHintView
                                        } else {
                                            suggestionsView
                                        }
                                    } else if vm.isSearching && !vm.hasResults {
                                        ProgressView()
                                            .frame(maxWidth: .infinity, minHeight: 200)
                                    } else if let error = vm.errorMessage, !vm.hasResults {
                                        searchErrorView(error)
                                    } else if vm.hasResults {
                                        searchResultsContent
                                    } else {
                                        searchEmptyView
                                    }
                                } else if selectedTab == .playlists {
                                    // Playlists tab content
                                    if isLoading && userPlaylists.isEmpty {
                                        ProgressView()
                                            .frame(maxWidth: .infinity, minHeight: 200)
                                    } else if let error = errorMessage, userPlaylists.isEmpty {
                                        errorView(error)
                                    } else if !userPlaylists.isEmpty {
                                        switch layout {
                                        case .list:
                                            playlistsListContent
                                        case .grid:
                                            playlistsGridContent
                                        }
                                    } else {
                                        playlistsEmptyView
                                    }
                                } else if isLoading && currentVideos.isEmpty {
                                    ProgressView()
                                        .frame(maxWidth: .infinity, minHeight: 200)
                                } else if let error = errorMessage, currentVideos.isEmpty {
                                    errorView(error)
                                } else if !currentVideos.isEmpty {
                                    // Conditional layout based on user preference
                                    switch layout {
                                    case .list:
                                        listContent
                                    case .grid:
                                        gridContent
                                    }
                                } else {
                                    emptyView
                                }
                            }
                        }
                    }
                    .refreshable {
                        await startContentLoad(forceRefresh: true)
                    }
                )
            .onChange(of: geometry.size.width, initial: true) { _, newWidth in
                viewWidth = newWidth
            }
            #endif
        }
        #if !os(tvOS)
        .navigationTitle(instance.displayName)
        .toolbarTitleDisplayMode(.inlineLarge)
        .toolbar {
            // View options first so it is the leftmost navigation item on macOS.
            ToolbarItem(placement: viewOptionsPlacement) {
                Button {
                    showViewOptions = true
                } label: {
                    Label(String(localized: "viewOptions.title"), systemImage: "slider.horizontal.3")
                }
                .liquidGlassTransitionSource(id: "instanceBrowseViewOptions", in: sheetTransition)
                #if os(macOS)
                .popover(isPresented: $showViewOptions, arrowEdge: .bottom) {
                    viewOptionsSheetContent
                }
                #endif
            }
            #if os(macOS)
            if #available(macOS 26, *) {
                // Render the items bare (conditional `if`, no opacity/disabled
                // wrappers): wrapping a `.segmented` Picker inserts a hosting layer
                // that makes the toolbar fall back to a Liquid-Glass pill instead of
                // the native segmented control. The search filters button shows only
                // while displaying search results; the principal picker swaps between
                // the browse tab picker and the search content type picker.
                if showsSearchFiltersInToolbar {
                    ToolbarItem(placement: .navigation) {
                        searchFiltersToolbarButton
                    }
                }
                if isToolbarPickerVisible {
                    ToolbarItem(placement: .principal) {
                        if showsSearchFiltersInToolbar {
                            searchContentTypePicker
                        } else {
                            tabPicker
                        }
                    }
                }
            } else if showsSearchFiltersInToolbar {
                ToolbarItem(placement: .navigation) {
                    searchFiltersToolbarButton
                }
                ToolbarItem(placement: .principal) {
                    searchContentTypePicker
                        .fixedSize()
                }
            } else if !isInSearchMode {
                ToolbarItem(placement: .principal) {
                    tabPicker
                        .fixedSize()
                }
            }
            // Pin the trailing group (search field) to the right edge,
            // matching the global Search view.
            if #available(macOS 26, *) {
                ToolbarSpacer(.flexible, placement: .primaryAction)
            }
            #endif
        }
        #endif
        #if !os(macOS)
        .sheet(isPresented: $showViewOptions) {
            viewOptionsSheetContent
                .liquidGlassSheetContent(sourceID: "instanceBrowseViewOptions", in: sheetTransition)
        }
        #endif
        .task {
            // Initialize search view model
            if let appEnvironment {
                searchViewModel = SearchViewModel(
                    instance: instance,
                    contentService: appEnvironment.contentService,
                    deArrowProvider: appEnvironment.deArrowBrandingProvider,
                    dataManager: appEnvironment.dataManager,
                    settingsManager: appEnvironment.settingsManager
                )
            }
            
            // Check login status for instances that support authentication
            if instance.supportsAuthentication {
                isLoggedIn = appEnvironment?.credentialsManager(for: instance)?.isLoggedIn(for: instance) ?? false
            }
            
            // Set initial tab only once (not on navigation back)
            if !hasInitializedTab {
                hasInitializedTab = true
                if let initialTab {
                    selectedTab = initialTab
                } else if instance.supportsAuthentication && isLoggedIn {
                    // Default to Feed tab when logged in
                    selectedTab = .feed
                }
            }
            
            // Load watch entries for hide watched feature
            loadWatchEntries()
            
            await startContentLoad()
        }
        .onReceive(NotificationCenter.default.publisher(for: .watchHistoryDidChange)) { _ in
            loadWatchEntries()
        }
        .onChange(of: selectedTab) { _, _ in
            isLoading = true
            errorMessage = nil
            Task { await startContentLoad() }
        }
        #if os(iOS)
        .searchable(
            text: $searchText,
            placement: .navigationBarDrawer(displayMode: .automatic),
            prompt: Text(String(localized: "instance.browse.search.placeholder"))
        )
        .onSubmit(of: .search) {
            searchViewModel?.cancelSuggestions()
            Task {
                await searchViewModel?.search(query: searchText)
            }
        }
        #elseif !os(tvOS)
        .searchable(
            text: $searchText,
            prompt: Text(String(localized: "instance.browse.search.placeholder"))
        )
        .onSubmit(of: .search) {
            searchViewModel?.cancelSuggestions()
            Task {
                await searchViewModel?.search(query: searchText)
            }
        }
        #endif
        .onChange(of: searchText) { _, newValue in
            if newValue.isEmpty {
                searchViewModel?.clearResults()
            } else {
                searchViewModel?.fetchSuggestions(for: newValue)
            }
        }
        #if !os(macOS)
        .sheet(isPresented: $showFilterSheet) {
            searchFiltersSheetContent
            #if !os(tvOS)
                .presentationDetents([.medium, .large])
            #endif
        }
        #endif
    }

    // MARK: - Computed Properties

    private var searchFiltersSheetContent: some View {
        SearchFiltersSheet(onApply: {
            Task {
                await searchViewModel?.search(query: searchText)
            }
        }, filters: Binding(
            get: { searchViewModel?.filters ?? .defaults },
            set: { searchViewModel?.filters = $0 }
        ))
    }

    #if os(macOS)
    private var isShowingSearchResults: Bool {
        isInSearchMode && (searchViewModel?.hasSearched ?? false)
    }

    private var showsSearchFiltersInToolbar: Bool {
        isShowingSearchResults && instance.supportsSearchFilters
    }

    private var isToolbarPickerVisible: Bool {
        // Tab picker stays visible while typing; it swaps to the search content
        // type picker only once a search has been submitted
        !isShowingSearchResults || showsSearchFiltersInToolbar
    }

    private var searchContentTypePicker: some View {
        Picker("", selection: Binding(
            get: { searchViewModel?.filters.type ?? .video },
            set: { searchViewModel?.filters.type = $0 }
        )) {
            ForEach(SearchContentType.allCases) { type in
                Text(type.title).tag(type)
            }
        }
        .pickerStyle(.segmented)
        .onChange(of: searchViewModel?.filters.type) { _, _ in
            Task {
                await searchViewModel?.search(query: searchText)
            }
        }
    }

    private var searchFiltersToolbarButton: some View {
        Button {
            showFilterSheet = true
        } label: {
            Label(String(localized: "search.filters"), systemImage: (searchViewModel?.filters.isDefault ?? true)
                ? "line.3.horizontal.decrease.circle"
                : "line.3.horizontal.decrease.circle.fill")
        }
        .popover(isPresented: $showFilterSheet, arrowEdge: .bottom) {
            searchFiltersSheetContent
        }
    }
    #endif

    private var tabPicker: some View {
        Picker("", selection: $selectedTab) {
            ForEach(availableTabs) { tab in
                #if os(macOS)
                // Labels render icon-only in a macOS toolbar segmented control
                Text(tab.title)
                    .tag(tab)
                #else
                Label(tab.title, systemImage: tab.systemImage)
                    .tag(tab)
                #endif
            }
        }
        .pickerStyle(.segmented)
    }

    private var availableTabs: [BrowseTab] {
        if instance.supportsFeed && isLoggedIn {
            // Playlists tab only available for Invidious (Piped playlists to be added in future)
            if instance.type == .invidious {
                return [.feed, .popular, .trending, .playlists]
            }
            return [.feed, .popular, .trending]
        }
        return [.popular, .trending]
    }

    private var currentVideos: [Video] {
        var videos: [Video]
        switch selectedTab {
        case .popular: videos = popularVideos
        case .trending: videos = trendingVideos
        case .feed: videos = filteredFeedVideos
        case .playlists: videos = []  // Playlists tab doesn't show videos directly
        }
        
        // Filter out watched videos if enabled
        if hideWatched {
            videos = videos.filter { video in
                guard let entry = watchEntriesMap[video.id.videoID] else { return true }
                return !entry.isFinished
            }
        }
        
        return videos
    }

    private var filteredFeedVideos: [Video] {
        if let channelID = selectedFeedChannelID {
            return feedVideos.filter { $0.author.id == channelID }
        }
        return feedVideos
    }

    private var isInSearchMode: Bool {
        !searchText.trimmingCharacters(in: .whitespaces).isEmpty
    }
    
    // Grid layout configuration
    private var gridConfig: GridLayoutConfiguration {
        GridLayoutConfiguration(viewWidth: viewWidth, gridColumns: gridColumnCount)
    }
    
    /// Gets the watch progress (0.0-1.0) for a video, or nil if not watched/finished.
    private func watchProgress(for video: Video) -> Double? {
        guard let entry = watchEntriesMap[video.id.videoID] else { return nil }
        let progress = entry.progress
        // Only show progress bar for partially watched videos
        return progress > 0 && progress < 1 ? progress : nil
    }

    /// Subscriptions sorted by most recent video upload date
    private var sortedFeedSubscriptions: [Channel] {
        var latestVideoDate: [String: Date] = [:]
        for video in feedVideos {
            let channelID = video.author.id
            let videoDate = video.publishedAt ?? .distantPast
            if let existing = latestVideoDate[channelID] {
                if videoDate > existing {
                    latestVideoDate[channelID] = videoDate
                }
            } else {
                latestVideoDate[channelID] = videoDate
            }
        }

        return feedSubscriptions.sorted { (sub1: Channel, sub2: Channel) -> Bool in
            let channelID1 = sub1.id.channelID
            let channelID2 = sub2.id.channelID
            let date1 = latestVideoDate[channelID1] ?? .distantPast
            let date2 = latestVideoDate[channelID2] ?? .distantPast
            return date1 > date2
        }
    }

    // MARK: - Search Filters Strip

    #if os(tvOS)
    private func filterMenu<T: Hashable & Identifiable & CaseIterable>(
        title: String,
        selection: Binding<T>,
        options: [T],
        labelForOption: @escaping (T) -> String
    ) -> some View {
        Menu {
            ForEach(options) { option in
                Button {
                    selection.wrappedValue = option
                    Task { await searchViewModel?.search(query: searchText) }
                } label: {
                    if option == selection.wrappedValue {
                        Label(labelForOption(option), systemImage: "checkmark")
                    } else {
                        Text(labelForOption(option))
                    }
                }
            }
        } label: {
            Text(title)
        }
    }

    private var tvOSFiltersMenu: some View {
        Menu {
            Menu(String(localized: "search.sort")) {
                ForEach(SearchSortOption.allCases) { option in
                    Button {
                        searchViewModel?.filters.sort = option
                        Task { await searchViewModel?.search(query: searchText) }
                    } label: {
                        if searchViewModel?.filters.sort == option {
                            Label(option.title, systemImage: "checkmark")
                        } else {
                            Text(option.title)
                        }
                    }
                }
            }

            Menu(String(localized: "search.uploadDate")) {
                ForEach(SearchDateFilter.allCases) { option in
                    Button {
                        searchViewModel?.filters.date = option
                        Task { await searchViewModel?.search(query: searchText) }
                    } label: {
                        if searchViewModel?.filters.date == option {
                            Label(option.title, systemImage: "checkmark")
                        } else {
                            Text(option.title)
                        }
                    }
                }
            }

            Menu(String(localized: "search.duration")) {
                ForEach(SearchDurationFilter.allCases) { option in
                    Button {
                        searchViewModel?.filters.duration = option
                        Task { await searchViewModel?.search(query: searchText) }
                    } label: {
                        if searchViewModel?.filters.duration == option {
                            Label(option.title, systemImage: "checkmark")
                        } else {
                            Text(option.title)
                        }
                    }
                }
            }

            Divider()

            Button(role: .destructive) {
                let currentType = searchViewModel?.filters.type ?? .video
                searchViewModel?.filters = .defaults
                searchViewModel?.filters.type = currentType
                Task { await searchViewModel?.search(query: searchText) }
            } label: {
                Label(String(localized: "search.filters.reset"), systemImage: "arrow.counterclockwise")
            }
            .disabled(searchViewModel?.filters.isDefault ?? true)
        } label: {
            Label(String(localized: "search.filters"), systemImage: "line.3.horizontal.decrease")
        }
    }
    #else
    private var searchFiltersStrip: some View {
        HStack(spacing: 12) {
            Button {
                showFilterSheet = true
            } label: {
                Image(systemName: (searchViewModel?.filters.isDefault ?? true)
                    ? "line.3.horizontal.decrease.circle"
                    : "line.3.horizontal.decrease.circle.fill")
                    .font(.title2)
            }

            Picker("", selection: Binding(
                get: { searchViewModel?.filters.type ?? .video },
                set: { searchViewModel?.filters.type = $0 }
            )) {
                ForEach(SearchContentType.allCases) { type in
                    Text(type.title).tag(type)
                }
            }
            .pickerStyle(.segmented)
        }
        .padding(.horizontal)
        .padding(.vertical, 8)
        .onChange(of: searchViewModel?.filters.type) { _, _ in
            Task {
                await searchViewModel?.search(query: searchText)
            }
        }
    }
    #endif

    // MARK: - Feed Channel Filter Strip

    private var feedChannelFilterStrip: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            LazyHStack(spacing: 10) {
                ForEach(sortedFeedSubscriptions) { subscription in
                    ChannelFilterChip(
                        channelID: subscription.id.channelID,
                        name: subscription.name,
                        avatarURL: subscription.thumbnailURL,
                        serverURL: yatteeServerURL,
                        isSelected: selectedFeedChannelID == subscription.id.channelID,
                        avatarSize: 44,
                        onTap: {
                            if selectedFeedChannelID == subscription.id.channelID {
                                selectedFeedChannelID = nil
                            } else {
                                selectedFeedChannelID = subscription.id.channelID
                            }
                        },
                        onGoToChannel: {
                            appEnvironment?.navigationCoordinator.navigate(
                                to: .channel(subscription.id.channelID, .global(provider: ContentSource.youtubeProvider))
                            )
                        },
                        onUnsubscribe: nil,
                        authHeader: avatarAuthHeader
                    )
                }
            }
            .padding(.horizontal)
            .padding(.vertical, 12)
        }
        #if os(tvOS)
        .background(Color.black.opacity(0.3))
        #else
        .background(.bar)
        .clipShape(RoundedRectangle(cornerRadius: 20))
        .padding(.horizontal, 16)
        #endif
    }

    // MARK: - List Layout

    @ViewBuilder
    private var listContent: some View {
        VideoListContent(listStyle: listStyle) {
            ForEach(Array(currentVideos.enumerated()), id: \.element.id) { index, video in
                VideoListRow(
                    isLast: index == currentVideos.count - 1,
                    rowStyle: rowStyle,
                    listStyle: listStyle
                ) {
                    VideoRowView(
                        video: video,
                        style: rowStyle,
                        watchProgress: watchProgress(for: video)
                    )
                    .tappableVideo(
                        video,
                        queueSource: selectedTab == .feed ? .subscriptions(continuation: nil) : .manual,
                        sourceLabel: selectedTab.title,
                        videoList: currentVideos,
                        videoIndex: index
                    )
                }
                #if !os(tvOS)
                .videoSwipeActions(video: video)
                #endif
            }

            // Feed tab load more
            if selectedTab == .feed {
                LoadMoreTrigger(
                    isLoading: isLoadingMoreFeed,
                    hasMore: hasMoreFeedResults && feedVideos.count > feedLoadedVideoCount
                ) {
                    loadMoreFeedResults()
                }
            }
        }
    }
    
    // MARK: - Grid Layout

    @ViewBuilder
    private var gridContent: some View {
        VideoGridContent(columns: gridConfig.effectiveColumns) {
            ForEach(Array(currentVideos.enumerated()), id: \.element.id) { index, video in
                VideoCardView(
                    video: video,
                    watchProgress: watchProgress(for: video),
                    isCompact: gridConfig.isCompactCards
                )
                .tappableVideo(
                    video,
                    queueSource: selectedTab == .feed ? .subscriptions(continuation: nil) : .manual,
                    sourceLabel: selectedTab.title,
                    videoList: currentVideos,
                    videoIndex: index
                )
                .onAppear {
                    // Infinite scroll for feed tab - trigger when near end AND we have new content since last trigger
                    if selectedTab == .feed
                        && index >= currentVideos.count - 3
                        && hasMoreFeedResults
                        && !isLoadingMoreFeed
                        && feedVideos.count > feedLoadedVideoCount {
                        loadMoreFeedResults()
                    }
                }
            }
        }

        if selectedTab == .feed && isLoadingMoreFeed {
            ProgressView()
                .frame(maxWidth: .infinity)
                .padding()
        }
    }

    // MARK: - Playlists Layout

    @ViewBuilder
    private var playlistsListContent: some View {
        VideoListContent(listStyle: listStyle) {
            ForEach(Array(userPlaylists.enumerated()), id: \.element.id) { index, playlist in
                VideoListRow(
                    isLast: index == userPlaylists.count - 1,
                    rowStyle: rowStyle,
                    listStyle: listStyle
                ) {
                    NavigationLink(value: NavigationDestination.playlist(.remote(playlist.id, instance: instance, title: playlist.title))) {
                        SearchPlaylistRowView(playlist: playlist, style: rowStyle)
                            .contentShape(Rectangle())
                    }
                    .zoomTransitionSource(id: playlist.id)
                    .buttonStyle(.plain)
                }
            }
        }
    }

    @ViewBuilder
    private var playlistsGridContent: some View {
        VideoGridContent(columns: gridConfig.effectiveColumns) {
            ForEach(userPlaylists) { playlist in
                NavigationLink(value: NavigationDestination.playlist(.remote(playlist.id, instance: instance, title: playlist.title))) {
                    PlaylistCardView(playlist: playlist, isCompact: gridConfig.isCompactCards)
                        .contentShape(Rectangle())
                }
                .zoomTransitionSource(id: playlist.id)
                .buttonStyle(.plain)
            }
        }
    }

    private var playlistsEmptyView: some View {
        ContentUnavailableView {
            Label(String(localized: "playlists.empty.title"), systemImage: "play.square.stack")
        } description: {
            Text(String(localized: "playlists.empty.description"))
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }

    // MARK: - Empty View

    private var emptyView: some View {
        ContentUnavailableView {
            Label(String(localized: "common.noContent"), systemImage: "tray")
        } description: {
            Text(String(localized: "instance.browse.noVideos"))
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }

    // MARK: - Error View

    private func errorView(_ error: String) -> some View {
        ContentUnavailableView {
            Label(String(localized: "common.error"), systemImage: "exclamationmark.triangle")
        } description: {
            Text(error)
        } actions: {
            Button(String(localized: "common.retry")) {
                Task { await startContentLoad(forceRefresh: true) }
            }
            .buttonStyle(.bordered)
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }

    // MARK: - Search Results

    @ViewBuilder
    private var searchResultsContent: some View {
        if let vm = searchViewModel {
            switch layout {
            case .list:
                searchResultsListContent(vm: vm)
            case .grid:
                searchResultsGridContent(vm: vm)
            }
        }
    }
    
    @ViewBuilder
    private func searchResultsListContent(vm: SearchViewModel) -> some View {
        VideoListContent(listStyle: listStyle) {
            ForEach(Array(vm.resultItems.enumerated()), id: \.element.id) { resultIndex, item in
                VideoListRow(
                    isLast: resultIndex == vm.resultItems.count - 1,
                    rowStyle: rowStyle,
                    listStyle: listStyle,
                    contentWidth: item.isChannel ? rowStyle.thumbnailHeight : nil
                ) {
                    switch item {
                    case .video(let video, let videoIndex):
                        VideoRowView(
                            video: video,
                            style: rowStyle,
                            watchProgress: watchProgress(for: video)
                        )
                        .tappableVideo(
                            video,
                            queueSource: .search(query: searchText, continuation: nil),
                            sourceLabel: String(localized: "queue.source.search \(searchText)"),
                            videoList: vm.videos,
                            videoIndex: videoIndex
                        )
                        #if !os(tvOS)
                        .videoSwipeActions(video: video)
                        #endif
                        .onAppear {
                            if resultIndex >= vm.resultItems.count - 3 {
                                Task { await vm.loadMore() }
                            }
                        }

                    case .playlist(let playlist):
                        NavigationLink(value: NavigationDestination.playlist(.remote(playlist.id, instance: instance, title: playlist.title))) {
                            SearchPlaylistRowView(playlist: playlist, style: rowStyle)
                                .contentShape(Rectangle())
                        }
                        .zoomTransitionSource(id: playlist.id)
                        .buttonStyle(.plain)
                        .onAppear {
                            if resultIndex >= vm.resultItems.count - 3 {
                                Task { await vm.loadMore() }
                            }
                        }

                    case .channel(let channel):
                        NavigationLink(
                            value: NavigationDestination.channel(
                                channel.id.channelID,
                                channel.id.source
                            )
                        ) {
                            ChannelRowView(channel: channel, style: rowStyle)
                                .contentShape(Rectangle())
                        }
                        .zoomTransitionSource(id: channel.id.channelID)
                        .buttonStyle(.plain)
                        .onAppear {
                            if resultIndex >= vm.resultItems.count - 3 {
                                Task { await vm.loadMore() }
                            }
                        }
                    }
                }
            }

            LoadMoreTrigger(
                isLoading: vm.isLoadingMore,
                hasMore: vm.hasMoreResults
            ) {
                Task { await vm.loadMore() }
            }
        }
    }
    
    @ViewBuilder
    private func searchResultsGridContent(vm: SearchViewModel) -> some View {
        VideoGridContent(columns: gridConfig.effectiveColumns) {
            ForEach(Array(vm.resultItems.enumerated()), id: \.element.id) { resultIndex, item in
                switch item {
                case .video(let video, let videoIndex):
                    VideoCardView(
                        video: video,
                        watchProgress: watchProgress(for: video),
                        isCompact: gridConfig.isCompactCards
                    )
                    .tappableVideo(
                        video,
                        queueSource: .search(query: searchText, continuation: nil),
                        sourceLabel: String(localized: "queue.source.search \(searchText)"),
                        videoList: vm.videos,
                        videoIndex: videoIndex
                    )
                    .onAppear {
                        if resultIndex >= vm.resultItems.count - 3 {
                            Task { await vm.loadMore() }
                        }
                    }

                case .playlist(let playlist):
                    NavigationLink(value: NavigationDestination.playlist(.remote(playlist.id, instance: instance, title: playlist.title))) {
                        PlaylistCardView(playlist: playlist, isCompact: gridConfig.isCompactCards)
                            .contentShape(Rectangle())
                    }
                    .zoomTransitionSource(id: playlist.id)
                    .buttonStyle(.plain)
                    .onAppear {
                        if resultIndex >= vm.resultItems.count - 3 {
                            Task { await vm.loadMore() }
                        }
                    }

                case .channel(let channel):
                    NavigationLink(
                        value: NavigationDestination.channel(
                            channel.id.channelID,
                            channel.id.source
                        )
                    ) {
                        ChannelCardGridView(channel: channel, isCompact: gridConfig.isCompactCards)
                            .contentShape(Rectangle())
                    }
                    .zoomTransitionSource(id: channel.id.channelID)
                    .buttonStyle(.plain)
                    .onAppear {
                        if resultIndex >= vm.resultItems.count - 3 {
                            Task { await vm.loadMore() }
                        }
                    }
                }
            }
        }

        if vm.isLoadingMore {
            ProgressView()
                .frame(maxWidth: .infinity)
                .padding()
        }
    }

    private var searchHintView: some View {
        ContentUnavailableView {
            Label(String(localized: "search.hint.title"), systemImage: "magnifyingglass")
        } description: {
            Text(String(localized: "search.hint.description"))
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }

    @ViewBuilder
    private var suggestionsView: some View {
        if let vm = searchViewModel {
            LazyVStack(alignment: .leading, spacing: 0) {
                ForEach(vm.suggestions, id: \.self) { suggestion in
                    Button {
                        dismissKeyboard()
                        searchText = suggestion
                        Task { await vm.search(query: suggestion) }
                    } label: {
                        HStack {
                            Image(systemName: "magnifyingglass")
                                .foregroundStyle(.secondary)
                            Text(suggestion)
                            Spacer()
                        }
                        .padding(.horizontal)
                        .padding(.vertical, 12)
                        .contentShape(Rectangle())
                    }
                    .buttonStyle(.plain)

                    Divider()
                        .padding(.leading)
                }
            }
        }
    }

    private var searchEmptyView: some View {
        ContentUnavailableView {
            Label(String(localized: "search.noResults.title"), systemImage: "magnifyingglass")
        } description: {
            Text(String(localized: "search.noResults.description"))
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }

    private func searchErrorView(_ error: String) -> some View {
        ContentUnavailableView {
            Label(String(localized: "common.error"), systemImage: "exclamationmark.triangle")
        } description: {
            Text(error)
        } actions: {
            Button(String(localized: "common.retry")) {
                Task { await searchViewModel?.search(query: searchText) }
            }
            .buttonStyle(.bordered)
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }

    // MARK: - Data Loading
    
    private func loadWatchEntries() {
        watchEntriesMap = appEnvironment?.dataManager.watchEntriesMap() ?? [:]
    }

    private func startContentLoad(forceRefresh: Bool = false) async {
        // Cancel any in-flight load before starting a new one
        contentLoadTask?.cancel()
        let task = Task {
            await performLoadContent(forceRefresh: forceRefresh)
        }
        contentLoadTask = task
        await task.value
    }

    private func performLoadContent(forceRefresh: Bool = false) async {
        guard let appEnvironment else {
            errorMessage = "App not initialized"
            isLoading = false
            return
        }

        // Skip loading if we already have data and not forcing refresh
        let hasData: Bool
        switch selectedTab {
        case .popular: hasData = !popularVideos.isEmpty
        case .trending: hasData = !trendingVideos.isEmpty
        case .feed: hasData = !feedVideos.isEmpty
        case .playlists: hasData = !userPlaylists.isEmpty
        }

        if hasData && !forceRefresh {
            isLoading = false
            return
        }

        isLoading = !hasData  // Only show loading spinner when no existing data
        errorMessage = nil

        do {
            switch selectedTab {
            case .popular:
                let videos = try await appEnvironment.contentService.popular(for: instance)
                popularVideos = videos
                prefetchBranding(for: videos)
            case .trending:
                let videos = try await appEnvironment.contentService.trending(for: instance)
                trendingVideos = videos
                prefetchBranding(for: videos)
            case .feed:
                guard let credential = appEnvironment.credentialsManager(for: instance)?.credential(for: instance) else {
                    errorMessage = String(localized: "feed.error.notLoggedIn")
                    isLoading = false
                    return
                }

                if forceRefresh {
                    feedPage = 1
                    hasMoreFeedResults = true
                    // Don't clear feedVideos here — keep old data visible
                    // until the API call succeeds and replaces it.
                }

                // Load subscriptions and feed based on instance type
                let subscriptionChannels: [Channel]
                let videos: [Video]

                switch instance.type {
                case .invidious:
                    let api = InvidiousAPI(httpClient: appEnvironment.httpClient)

                    // Load subscriptions for channel filter
                    async let subscriptionsTask = api.subscriptions(instance: instance, sid: credential)

                    // Load feed
                    async let feedTask = api.feed(instance: instance, sid: credential, page: feedPage)

                    let (subscriptions, feedResponse) = try await (subscriptionsTask, feedTask)

                    // Fetch channel thumbnails in parallel (subscriptions API doesn't include them)
                    let enrichedSubscriptions = await fetchChannelThumbnails(
                        for: subscriptions,
                        instance: instance,
                        api: api
                    )

                    subscriptionChannels = enrichedSubscriptions.map { $0.toChannel(baseURL: instance.url) }
                    videos = feedResponse.videos
                    hasMoreFeedResults = feedResponse.hasMore

                case .piped:
                    let api = PipedAPI(httpClient: appEnvironment.httpClient)

                    // Load subscriptions and feed in parallel
                    async let subscriptionsTask = api.subscriptions(instance: instance, authToken: credential)
                    async let feedTask = api.feed(instance: instance, authToken: credential)

                    let (pipedSubscriptions, feedVideos) = try await (subscriptionsTask, feedTask)

                    subscriptionChannels = pipedSubscriptions.map { $0.toChannel() }
                    videos = feedVideos
                    // Piped doesn't support pagination for feed
                    hasMoreFeedResults = false

                default:
                    errorMessage = String(localized: "feed.error.notSupported")
                    isLoading = false
                    return
                }

                feedSubscriptions = subscriptionChannels.map { $0.enrichedThumbnail(using: appEnvironment.dataManager) }
                feedVideos = videos
                prefetchBranding(for: videos)
            case .playlists:
                // Playlists are currently only supported for Invidious
                guard instance.type == .invidious else {
                    errorMessage = String(localized: "feed.error.notSupported")
                    isLoading = false
                    return
                }

                guard let credential = appEnvironment.credentialsManager(for: instance)?.credential(for: instance) else {
                    errorMessage = String(localized: "feed.error.notLoggedIn")
                    isLoading = false
                    return
                }

                let api = InvidiousAPI(httpClient: appEnvironment.httpClient)
                let playlists = try await api.userPlaylists(instance: instance, sid: credential)
                userPlaylists = playlists
            }
        } catch is CancellationError {
            // Task was cancelled — another load is taking over, don't touch state
            return
        } catch let error as APIError where error == .cancelled {
            // HTTP request was cancelled — another load is taking over, don't touch state
            return
        } catch {
            errorMessage = error.localizedDescription
        }

        isLoading = false
    }

    private func prefetchBranding(for videos: [Video]) {
        guard let appEnvironment else { return }
        let youtubeIDs = videos.compactMap { video -> String? in
            if case .global = video.id.source { return video.id.videoID }
            return nil
        }
        appEnvironment.deArrowBrandingProvider.prefetch(videoIDs: youtubeIDs)
    }

    /// Fetches channel thumbnails for subscriptions, using cache when available.
    /// Only fetches from network for channels not already cached.
    private func fetchChannelThumbnails(
        for subscriptions: [InvidiousSubscription],
        instance: Instance,
        api: InvidiousAPI
    ) async -> [InvidiousSubscription] {
        guard let credentialsManager = appEnvironment?.invidiousCredentialsManager else {
            return subscriptions
        }

        // Build initial thumbnail map from cache
        var thumbnailMap: [String: URL] = [:]
        for subscription in subscriptions {
            if let cachedURL = credentialsManager.thumbnailURL(forChannelID: subscription.authorId) {
                thumbnailMap[subscription.authorId] = cachedURL
            }
        }

        // Find channels that need fetching
        let allChannelIDs = subscriptions.map(\.authorId)
        let uncachedIDs = Set(credentialsManager.uncachedChannelIDs(from: allChannelIDs))

        // Fetch only uncached channels in parallel
        if !uncachedIDs.isEmpty {
            let fetchedThumbnails = await withTaskGroup(of: (String, URL?).self) { group in
                for subscription in subscriptions where uncachedIDs.contains(subscription.authorId) {
                    group.addTask {
                        do {
                            let channel = try await api.channel(id: subscription.authorId, instance: instance)
                            return (subscription.authorId, channel.thumbnailURL)
                        } catch {
                            return (subscription.authorId, nil)
                        }
                    }
                }

                var results: [String: URL] = [:]
                for await (authorId, thumbnailURL) in group {
                    if let url = thumbnailURL {
                        results[authorId] = url
                    }
                }
                return results
            }

            // Merge fetched thumbnails and cache them
            for (channelID, url) in fetchedThumbnails {
                thumbnailMap[channelID] = url
            }

            // Save to cache on main actor
            await MainActor.run {
                credentialsManager.setThumbnailURLs(fetchedThumbnails)
            }
        }

        // Create enriched subscriptions with thumbnails
        return subscriptions.map { subscription in
            var enriched = subscription
            enriched.thumbnailURL = thumbnailMap[subscription.authorId]
            return enriched
        }
    }

    private func loadMoreFeedResults() {
        guard hasMoreFeedResults, !isLoadingMoreFeed, !isLoading else { return }
        guard let appEnvironment,
              let sid = appEnvironment.invidiousCredentialsManager.sid(for: instance) else { return }

        isLoadingMoreFeed = true
        feedLoadedVideoCount = feedVideos.count  // Mark current count to prevent re-triggering
        feedPage += 1

        Task {
            do {
                let api = InvidiousAPI(httpClient: appEnvironment.httpClient)
                let feedResponse = try await api.feed(instance: instance, sid: sid, page: feedPage)

                await MainActor.run {
                    feedVideos.append(contentsOf: feedResponse.videos)
                    hasMoreFeedResults = feedResponse.hasMore
                    isLoadingMoreFeed = false
                    prefetchBranding(for: feedResponse.videos)
                }
            } catch {
                await MainActor.run {
                    isLoadingMoreFeed = false
                }
            }
        }
    }

    private func dismissKeyboard() {
        #if os(iOS)
        UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
        #endif
    }

}

// MARK: - Preview

#Preview {
    NavigationStack {
        InstanceBrowseView(
            instance: Instance(
                type: .invidious,
                url: URL(string: "https://invidious.example.com")!,
                name: "Example Instance"
            )
        )
    }
    .appEnvironment(.preview)
}
