//
//  SearchView.swift
//  Yattee
//
//  Search tab with source filtering and results.
//

import SwiftUI

struct SearchView: View {
    @Environment(\.appEnvironment) private var appEnvironment
    @Namespace private var sheetTransition

    /// Initial query for deep linking. When set, auto-executes search on appear.
    var initialQuery: String? = nil

    /// External search text binding (from CompactTabView iOS 18+ integration)
    private var externalSearchText: Binding<String>?

    /// Internal search text state (for standalone usage)
    @State private var internalSearchText = ""

    /// Computed binding to use external or internal search text
    private var searchTextBinding: Binding<String> {
        externalSearchText ?? $internalSearchText
    }

    @State private var showFilterSheet = false
    @State private var selectedSearchInstance: Instance?
    @State private var searchViewModel: SearchViewModel?
    @State private var searchHistory: [SearchHistory] = []
    @State private var recentChannels: [RecentChannel] = []
    @State private var recentPlaylists: [RecentPlaylist] = []
    @State private var showingClearAllRecentsConfirmation = false
    @State private var showViewOptions = false
    @State private var isSearchHistoryExpanded = false

    /// Suppresses the next onChange suggestion-fetch when the search text is set
    /// programmatically (recent search / suggestion tap / deep link), so it goes
    /// straight to results instead of re-showing suggestions.
    @State private var suppressNextTextChangeSearch = false

    @AppStorage("searchFilters") private var savedFiltersData: Data?

    // Persisted search instance selection
    @AppStorage("searchInstanceID") private var savedSearchInstanceID: String?

    // View options (persisted, separate from subscriptions)
    @AppStorage("searchLayout") private var layout: VideoListLayout = .list
    @AppStorage("searchRowStyle") private var rowStyle: VideoRowStyle = .regular
    @AppStorage("searchGridColumns") private var gridColumns = 2
    @AppStorage("searchHideWatched") private var hideWatched = false

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

    // Privacy toggle tracking (establishes @Observable observation)
    private var saveRecentSearches: Bool {
        appEnvironment?.settingsManager.saveRecentSearches ?? true
    }
    private var saveRecentChannels: Bool {
        appEnvironment?.settingsManager.saveRecentChannels ?? true
    }
    private var saveRecentPlaylists: Bool {
        appEnvironment?.settingsManager.saveRecentPlaylists ?? true
    }

    // Grid layout configuration
    @State private var viewWidth: CGFloat = 0
    @State private var watchEntriesMap: [String: WatchEntry] = [:]
    private var gridConfig: GridLayoutConfiguration {
        GridLayoutConfiguration(viewWidth: viewWidth, gridColumns: gridColumns)
    }

    /// The instance to use for searching.
    /// Defaults to the active instance if none is explicitly selected.
    private var searchInstance: Instance? {
        selectedSearchInstance ?? appEnvironment?.instancesManager.activeInstance
    }

    /// All enabled instances available for searching.
    private var availableInstances: [Instance] {
        appEnvironment?.instancesManager.enabledInstances ?? []
    }

    private var hasResults: Bool {
        searchViewModel?.hasResults ?? false
    }

    private var hasSearched: Bool {
        searchViewModel?.hasSearched ?? false
    }

    /// Initialize SearchView with optional external search text binding
    init(searchText: Binding<String>? = nil, initialQuery: String? = nil) {
        self.externalSearchText = searchText
        self.initialQuery = initialQuery
    }

    /// 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 {
        tvOSOrDefaultContent
        #if !os(macOS)
        .sheet(isPresented: $showFilterSheet) {
            searchFiltersSheetContent
            #if !os(tvOS)
            .presentationDetents([.medium, .large])
            #endif
        }
        .sheet(isPresented: $showViewOptions) {
            viewOptionsSheetContent
            #if !os(tvOS)
            .liquidGlassSheetContent(sourceID: "searchViewOptions", in: sheetTransition)
            #endif
        }
        #endif
        .onChange(of: searchTextBinding.wrappedValue) { _, newValue in
            if suppressNextTextChangeSearch {
                suppressNextTextChangeSearch = false
                return
            }
            if newValue.isEmpty {
                searchViewModel?.clearResults()  // Clear everything when empty
                searchViewModel?.filters = .defaults
                saveFilters(.defaults)
            } else {
                // Clear results but keep suggestions visible until new ones load
                searchViewModel?.clearSearchResults()
                searchViewModel?.fetchSuggestions(for: newValue)
            }
        }
        .task {
            initializeViewModel()
        }
        .task(id: initialQuery) {
            // Auto-execute search when opened with an initial query
            if let query = initialQuery, !query.isEmpty, searchTextBinding.wrappedValue.isEmpty {
                suppressNextTextChangeSearch = true
                searchTextBinding.wrappedValue = query
                searchViewModel?.cancelSuggestions()
                searchViewModel?.filters.type = .video
                if let filters = searchViewModel?.filters {
                    saveFilters(filters)
                }
                Task { await searchViewModel?.search(query: query) }
            }
        }
        .onReceive(NotificationCenter.default.publisher(for: .searchHistoryDidChange)) { _ in
            loadSearchHistory()
        }
        .onReceive(NotificationCenter.default.publisher(for: .watchHistoryDidChange)) { _ in
            loadWatchEntries()
        }
        .task {
            loadWatchEntries()
        }
        .onReceive(NotificationCenter.default.publisher(for: .recentChannelsDidChange)) { _ in
            loadRecentChannels()
        }
        .onReceive(NotificationCenter.default.publisher(for: .recentPlaylistsDidChange)) { _ in
            loadRecentPlaylists()
        }
        .onReceive(NotificationCenter.default.publisher(for: .instancesDidChange)) { _ in
            handleInstancesChanged()
        }
        .onChange(of: hideWatched) { _, newValue in
            searchViewModel?.hideWatchedVideos = newValue
        }
        .onChange(of: saveRecentSearches) { _, _ in
            loadSearchHistory()
        }
        .onChange(of: saveRecentChannels) { _, _ in
            loadRecentChannels()
        }
        .onChange(of: saveRecentPlaylists) { _, _ in
            loadRecentPlaylists()
        }
    }

    private var searchFiltersSheetContent: some View {
        SearchFiltersSheet(onApply: {
            if hasResults {
                Task { await searchViewModel?.search(query: searchTextBinding.wrappedValue) }
            }
        }, filters: Binding(
            get: { searchViewModel?.filters ?? .defaults },
            set: { newFilters in
                searchViewModel?.filters = newFilters
                saveFilters(newFilters)
            }
        ))
    }

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

    #if os(macOS)
    private var searchFiltersToolbarButton: some View {
        Button {
            showFilterSheet = true
        } label: {
            Label(String(localized: "search.filters"), systemImage: filtersIconName)
        }
        .popover(isPresented: $showFilterSheet, arrowEdge: .bottom) {
            searchFiltersSheetContent
        }
    }
    #endif

    @ViewBuilder
    private var tvOSOrDefaultContent: some View {
        #if os(tvOS)
        VStack(spacing: 0) {
            // tvOS: Search field, type filter, search filters, view options
            HStack(spacing: 24) {
                TextField("search.placeholder", text: searchTextBinding)
                    .textFieldStyle(.plain)
                    .onSubmit {
                        searchViewModel?.cancelSuggestions()
                        searchViewModel?.filters.type = .video
                        if let filters = searchViewModel?.filters {
                            saveFilters(filters)
                        }
                        Task { await searchViewModel?.search(query: searchTextBinding.wrappedValue) }
                    }

                if !searchTextBinding.wrappedValue.isEmpty {
                    // 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)
            searchContent
                .padding(.top, 20)
                .focusSection()
        }
        #else
        searchContent
        .navigationTitle(String(localized: "tabs.search"))
        .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: "searchViewOptions", in: sheetTransition)
                #if os(macOS)
                .popover(isPresented: $showViewOptions, arrowEdge: .bottom) {
                    viewOptionsSheetContent
                }
                #endif
            }
            #if os(macOS)
            if searchInstance?.supportsSearchFilters == true {
                // Filters button + content type picker are always visible on macOS.
                // The picker is rendered bare (no opacity/disabled/fixedSize wrappers):
                // wrapping a `.segmented` Picker in those modifiers inserts a hosting
                // layer that makes the toolbar fall back to a Liquid-Glass pill instead
                // of the native segmented control.
                ToolbarItem(placement: .navigation) {
                    searchFiltersToolbarButton
                }
                ToolbarItem(placement: .principal) {
                    if #available(macOS 26, *) {
                        contentTypePicker
                    } else {
                        contentTypePicker
                            .fixedSize()
                    }
                }
            }
            #endif
            #if os(macOS)
            // Pin the trailing group (search field) to the right edge
            // so it doesn't shift when the filter items appear/disappear.
            if #available(macOS 26, *) {
                ToolbarSpacer(.flexible, placement: .primaryAction)
            }
            #endif
        }
        .searchable(text: searchTextBinding, prompt: Text(String(localized: "search.placeholder")))
        .onSubmit(of: .search) {
            searchViewModel?.cancelSuggestions()
            #if !os(macOS)
            // On macOS the content type picker is always visible, so preserve the
            // user's chosen type instead of resetting it to .video on each submit.
            searchViewModel?.filters.type = .video
            #endif
            if let filters = searchViewModel?.filters {
                saveFilters(filters)
            }
            Task { await searchViewModel?.search(query: searchTextBinding.wrappedValue) }
        }
        .onChange(of: searchViewModel?.filters.type) { _, _ in
            if let filters = searchViewModel?.filters {
                saveFilters(filters)
            }
            Task { await searchViewModel?.search(query: searchTextBinding.wrappedValue) }
        }
        #endif
    }

    @ViewBuilder
    private var searchContent: some View {
        Group {
            if searchTextBinding.wrappedValue.isEmpty {
                emptySearchView
            } else if let vm = searchViewModel {
                if !vm.hasSearched {
                    if !vm.suggestions.isEmpty && !hasResults {
                        suggestionsView
                    } else if vm.isFetchingSuggestions && !hasResults {
                        ProgressView()
                            .frame(maxWidth: .infinity, maxHeight: .infinity)
                    } else {
                        Spacer()
                            .frame(maxWidth: .infinity, maxHeight: .infinity)
                    }
                } else if vm.isSearching && !hasResults {
                    resultsViewWithLoading
                } else if let error = vm.errorMessage, !hasResults {
                    errorView(error)
                } else if hasResults {
                    resultsView
                } else {
                    noResultsView
                }
            } else {
                Spacer()
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
            }
        }
    }

    private func initializeViewModel() {
        guard let appEnvironment, searchViewModel == nil else { return }

        // Restore persisted instance selection if available
        if selectedSearchInstance == nil, let savedID = savedSearchInstanceID,
           let savedUUID = UUID(uuidString: savedID),
           let restoredInstance = availableInstances.first(where: { $0.id == savedUUID }) {
            selectedSearchInstance = restoredInstance
        } else if selectedSearchInstance == nil, savedSearchInstanceID != nil {
            // Saved instance no longer exists, clear the saved ID
            savedSearchInstanceID = nil
        }

        guard let instance = searchInstance else { return }
        searchViewModel = SearchViewModel(
            instance: instance,
            contentService: appEnvironment.contentService,
            deArrowProvider: appEnvironment.deArrowBrandingProvider,
            dataManager: appEnvironment.dataManager,
            settingsManager: appEnvironment.settingsManager
        )
        searchViewModel?.hideWatchedVideos = hideWatched
        loadFilters()
        loadSearchHistory()
        loadRecentChannels()
        loadRecentPlaylists()
    }

    private func switchToInstance(_ instance: Instance) {
        guard let appEnvironment else { return }
        selectedSearchInstance = instance
        savedSearchInstanceID = instance.id.uuidString

        // Recreate ViewModel for new instance
        let hadResults = hasResults
        searchViewModel = SearchViewModel(
            instance: instance,
            contentService: appEnvironment.contentService,
            deArrowProvider: appEnvironment.deArrowBrandingProvider,
            dataManager: appEnvironment.dataManager,
            settingsManager: appEnvironment.settingsManager
        )
        searchViewModel?.hideWatchedVideos = hideWatched
        loadFilters()

        // Re-run search if we had results
        if hadResults {
            Task { await searchViewModel?.search(query: searchTextBinding.wrappedValue) }
        }
    }

    /// Handles instance list changes by invalidating stale references.
    private func handleInstancesChanged() {
        // Check if selected instance was removed
        if let selected = selectedSearchInstance,
           !availableInstances.contains(where: { $0.id == selected.id }) {
            selectedSearchInstance = nil
            savedSearchInstanceID = nil
        }

        // Check if the current VM's instance was removed
        if let vm = searchViewModel,
           !availableInstances.contains(where: { $0.id == vm.instance.id }) {
            searchViewModel = nil
            // Re-initialize with a valid instance
            initializeViewModel()
        }
    }

    private func loadFilters() {
        guard let data = savedFiltersData,
              let decoded = try? JSONDecoder().decode(SearchFilters.self, from: data) else {
            return
        }
        searchViewModel?.filters = decoded
    }

    private func saveFilters(_ filters: SearchFilters) {
        savedFiltersData = try? JSONEncoder().encode(filters)
    }

    // MARK: - Views

    private var recentItemsSpacing: CGFloat {
        #if os(tvOS)
        16
        #else
        0
        #endif
    }

    private var horizontalCardSpacing: CGFloat {
        #if os(tvOS)
        48
        #else
        16
        #endif
    }

    /// Instance picker for selecting search source when multiple instances are available
    @ViewBuilder
    private var instancePickerView: some View {
        if availableInstances.count > 1 {
            HStack {
                Text(String(localized: "search.source.title"))
                    .font(.headline)
                    .padding(.leading, listStyle == .inset ? 8 : 0)

                Spacer()

                Menu {
                    ForEach(availableInstances, id: \.id) { instance in
                        Button {
                            switchToInstance(instance)
                        } label: {
                            if instance.id == searchInstance?.id {
                                Label(instance.displayName, systemImage: "checkmark")
                            } else {
                                Text(instance.displayName)
                            }
                        }
                    }
                } label: {
                    Text(searchInstance?.displayName ?? "")
                        .lineLimit(1)
                        .truncationMode(.tail)
                }
            }
            .padding(listStyle == .inset ? 4 : 0)
            .background(listStyle == .inset ? ListBackgroundStyle.card.color : .clear)
            #if !os(tvOS)
            .clipShape(.rect(cornerRadius: 10))
            #endif
            .padding(.horizontal)
        }
    }

    #if !os(tvOS)
    private var filtersIconName: String {
        (searchViewModel?.filters.isDefault ?? true)
            ? "line.3.horizontal.decrease.circle"
            : "line.3.horizontal.decrease.circle.fill"
    }

    // Content type segmented picker
    private var contentTypePicker: 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)
    }
    #endif

    #if os(iOS)
    private var searchFiltersStrip: some View {
        HStack(spacing: 12) {
            // Filter button
            Button {
                showFilterSheet = true
            } label: {
                Image(systemName: filtersIconName)
                    .font(.title2)
            }

            contentTypePicker
        }
        .padding(.horizontal)
        .padding(.vertical, 8)
    }
    #endif

    #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
                    if let filters = searchViewModel?.filters {
                        saveFilters(filters)
                    }
                    Task { await searchViewModel?.search(query: searchTextBinding.wrappedValue) }
                } label: {
                    if option == selection.wrappedValue {
                        Label(labelForOption(option), systemImage: "checkmark")
                    } else {
                        Text(labelForOption(option))
                    }
                }
            }
        } label: {
            Text(title)
        }
    }

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

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

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

            Divider()

            // Reset
            Button(role: .destructive) {
                let currentType = searchViewModel?.filters.type ?? .video
                searchViewModel?.filters = .defaults
                searchViewModel?.filters.type = currentType
                if let filters = searchViewModel?.filters { saveFilters(filters) }
                Task { await searchViewModel?.search(query: searchTextBinding.wrappedValue) }
            } 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")
        }
    }
    #endif

    // MARK: - Search History Helpers

    private var displayedSearchHistory: [SearchHistory] {
        if isSearchHistoryExpanded || searchHistory.count <= 5 {
            return searchHistory
        } else {
            return Array(searchHistory.prefix(5))
        }
    }

    private var hasMoreSearchHistory: Bool {
        searchHistory.count > 5
    }

    private var emptySearchView: some View {
        Group {
            if searchHistory.isEmpty && recentChannels.isEmpty && recentPlaylists.isEmpty {
                // Empty state with icon
                VStack(spacing: 24) {
                    Spacer(minLength: 60)

                    Image(systemName: "magnifyingglass")
                        .font(.system(size: 48))
                        .foregroundStyle(.tertiary)

                    Text(String(localized: "search.empty.description"))
                        .font(.body)
                        .foregroundStyle(.secondary)
                        .multilineTextAlignment(.center)

                    Spacer()
                }
                .padding()
                .accessibilityLabel("search.empty")
            } else {
                // Recent searches, channels, and playlists in scrollable view
                ScrollView {
                    LazyVStack(alignment: .leading, spacing: 24, pinnedViews: []) {
                        // Instance picker at top when multiple instances available
                        instancePickerView

                        // Recent search queries section
                        if !searchHistory.isEmpty {
                            VStack(alignment: .leading, spacing: 0) {
                                // Header - tappable to expand/collapse when more than 5 items
                                #if os(tvOS)
                                HStack {
                                    Text(String(localized: "search.recentSearches.title"))
                                        .font(.headline)
                                        .foregroundStyle(.primary)
                                    Spacer()
                                }
                                .padding(.horizontal)
                                .padding(.bottom, 8)
                                #else
                                Button {
                                    if hasMoreSearchHistory {
                                        withAnimation(.easeInOut(duration: 0.2)) {
                                            isSearchHistoryExpanded.toggle()
                                        }
                                    }
                                } label: {
                                    HStack {
                                        Text(String(localized: "search.recentSearches.title"))
                                            .font(.headline)
                                            .foregroundStyle(.primary)
                                        Spacer()
                                        if hasMoreSearchHistory {
                                            Image(systemName: isSearchHistoryExpanded ? "chevron.up" : "chevron.down")
                                                .font(.caption)
                                                .foregroundStyle(.secondary)
                                        }
                                    }
                                    .contentShape(Rectangle())
                                }
                                .buttonStyle(.plain)
                                .disabled(!hasMoreSearchHistory)
                                .padding(.horizontal)
                                .padding(.bottom, 8)
                                #endif

                                // Items
                                VStack(spacing: recentItemsSpacing) {
                                    ForEach(displayedSearchHistory) { history in
                                        Button {
                                            executeSearch(history.query)
                                        } label: {
                                            HStack(spacing: 12) {
                                                Image(systemName: "clock.arrow.circlepath")
                                                    .foregroundStyle(.secondary)
                                                Text(history.query)
                                                    .foregroundStyle(.primary)
                                                Spacer()
                                            }
                                            .padding(.horizontal, listStyle == .inset ? 16 : 0)
                                            .padding(.vertical, 12)
                                            .contentShape(Rectangle())
                                        }
                                        .buttonStyle(.plain)
                                        .swipeActions {
                                            SwipeAction(
                                                symbolImage: "trash",
                                                tint: .white,
                                                background: .red,
                                                font: .body,
                                                size: CGSize(width: 38, height: 38)
                                            ) { reset in
                                                deleteHistory(history)
                                                reset()
                                            }
                                        }
                                        .contextMenu {
                                            Button(role: .destructive) {
                                                deleteHistory(history)
                                            } label: {
                                                Label(String(localized: "common.delete"), systemImage: "trash")
                                            }
                                        }

                                        #if !os(tvOS)
                                        if history.id != displayedSearchHistory.last?.id {
                                            Divider()
                                                .padding(.leading, 52)
                                        }
                                        #endif
                                    }
                                }
                                #if os(tvOS)
                                .background(.clear)
                                #else
                                .background(listStyle == .inset ? ListBackgroundStyle.card.color : Color.clear)
                                .clipShape(.rect(cornerRadius: listStyle == .inset ? 10 : 0))
                                #endif
                                .padding(.horizontal)
                            }
                        }

                        // Recent channels section
                        if !recentChannels.isEmpty {
                            VStack(alignment: .leading, spacing: 12) {
                                // Header
                                HStack {
                                    Text(String(localized: "search.recentChannels.title"))
                                        .font(.headline)
                                    Spacer()
                                }
                                .padding(.horizontal)

                                // Horizontal scroll
                                ScrollView(.horizontal, showsIndicators: false) {
                                    LazyHStack(spacing: horizontalCardSpacing) {
                                        ForEach(recentChannels) { recentChannel in
                                            NavigationLink(
                                                value: NavigationDestination.channel(
                                                    recentChannel.channelID,
                                                    sourceFromRawValue(
                                                        recentChannel.sourceRawValue,
                                                        instanceURL: recentChannel.instanceURLString
                                                    )
                                                )
                                            ) {
                                                ChannelCardGridView(
                                                    channel: channelFromRecent(recentChannel),
                                                    isCompact: false
                                                )
                                                #if os(tvOS)
                                                .frame(width: 240)
                                                #else
                                                .frame(width: 160)
                                                #endif
                                            }
                                            .zoomTransitionSource(id: recentChannel.channelID)
                                            .buttonStyle(.plain)
                                            .contextMenu {
                                                Button(role: .destructive) {
                                                    deleteRecentChannel(recentChannel)
                                                } label: {
                                                    Label(String(localized: "common.delete"), systemImage: "trash")
                                                }
                                            }
                                        }
                                    }
                                    .padding(.horizontal)
                                }
                                #if os(tvOS)
                                .scrollClipDisabled()
                                #endif
                            }
                        }

                        // Recent playlists section
                        if !recentPlaylists.isEmpty {
                            VStack(alignment: .leading, spacing: 12) {
                                // Header
                                HStack {
                                    Text(String(localized: "search.recentPlaylists.title"))
                                        .font(.headline)
                                    Spacer()
                                }
                                .padding(.horizontal)

                                // Horizontal scroll
                                ScrollView(.horizontal, showsIndicators: false) {
                                    LazyHStack(spacing: horizontalCardSpacing) {
                                        ForEach(recentPlaylists) { recentPlaylist in
                                            NavigationLink(value: NavigationDestination.playlist(.remote(playlistIDFromRecent(recentPlaylist), instance: nil, title: recentPlaylist.title))) {
                                                PlaylistCardView(
                                                    playlist: playlistFromRecent(recentPlaylist),
                                                    isCompact: false
                                                )
                                                #if os(tvOS)
                                                .frame(width: 320)
                                                #else
                                                .frame(width: 200)
                                                #endif
                                            }
                                            .zoomTransitionSource(id: playlistIDFromRecent(recentPlaylist))
                                            .buttonStyle(.plain)
                                            .contextMenu {
                                                Button(role: .destructive) {
                                                    deleteRecentPlaylist(recentPlaylist)
                                                } label: {
                                                    Label(String(localized: "common.delete"), systemImage: "trash")
                                                }
                                            }
                                        }
                                    }
                                    .padding(.horizontal)
                                }
                                #if os(tvOS)
                                .scrollClipDisabled()
                                #endif
                            }
                        }

                        // Clear All Recents button at bottom
                        Button {
                            showingClearAllRecentsConfirmation = true
                        } label: {
                            HStack(spacing: 6) {
                                Image(systemName: "trash")
                                Text(String(localized: "search.clearAllRecents"))
                                    .fontWeight(.semibold)
                            }
                        }
                        .foregroundStyle(.secondary)
                        .frame(maxWidth: .infinity)
                        .padding(.top, 16)
                        .padding(.bottom, 32)
                    }
                    .padding(.vertical)
                }
                #if os(tvOS)
                .scrollClipDisabled()
                #endif
                .background(listStyle == .inset ? ListBackgroundStyle.grouped.color : ListBackgroundStyle.plain.color)
                .accessibilityLabel("search.recents")
                .confirmationDialog(
                    String(localized: "search.clearAllRecents.confirm"),
                    isPresented: $showingClearAllRecentsConfirmation,
                    titleVisibility: .visible
                ) {
                    Button(String(localized: "search.clearAllRecents"), role: .destructive) {
                        clearAllRecents()
                    }
                    Button(String(localized: "common.cancel"), role: .cancel) {}
                }
                .presentationCompactAdaptation(.sheet)
            }
        }
    }

    @ViewBuilder
    private var suggestionsView: some View {
        if let vm = searchViewModel {
            List {
                ForEach(vm.suggestions, id: \.self) { suggestion in
                    Button {
                        dismissKeyboard()
                        suppressNextTextChangeSearch = (suggestion != searchTextBinding.wrappedValue)
                        searchTextBinding.wrappedValue = suggestion
                        vm.cancelSuggestions()
                        vm.filters.type = .video
                        saveFilters(vm.filters)
                        Task { await vm.search(query: suggestion) }
                    } label: {
                        HStack(spacing: 12) {
                            Image(systemName: "magnifyingglass")
                                .foregroundStyle(.secondary)
                            Text(suggestion)
                                .foregroundStyle(.primary)
                            Spacer()
                            Image(systemName: "arrow.up.left")
                                .font(.caption)
                                .foregroundStyle(.tertiary)
                        }
                        .contentShape(Rectangle())
                    }
                    .buttonStyle(.plain)
                }
            }
            .listStyle(.plain)
        }
    }

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

    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 searchViewModel?.search(query: searchTextBinding.wrappedValue) }
            }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }

    /// Queue source for search results
    private var searchQueueSource: QueueSource {
        let page = searchViewModel?.page ?? 1
        let continuationString = page > 1 ? String(page) : nil
        return .search(query: searchTextBinding.wrappedValue, continuation: continuationString)
    }

    /// Unified results combining all content types for display.
    /// Uses the API's original ordering when type is .all, otherwise prioritizes the selected type.
    private var unifiedResults: [SearchResultItem] {
        guard let vm = searchViewModel else { return [] }

        // For .all type, use resultItems which preserves API order
        if vm.filters.type == .all {
            return vm.resultItems
        }

        // For specific types, prioritize that type first
        var results: [SearchResultItem] = []

        switch vm.filters.type {
        case .video:
            // Show videos first, but also include any playlists/channels server returned
            for (index, video) in vm.videos.enumerated() {
                results.append(.video(video, index: index))
            }
            for playlist in vm.playlists {
                results.append(.playlist(playlist))
            }
            for channel in vm.channels {
                results.append(.channel(channel))
            }
        case .playlist:
            // Show playlists first, then any videos/channels server returned
            for playlist in vm.playlists {
                results.append(.playlist(playlist))
            }
            for (index, video) in vm.videos.enumerated() {
                results.append(.video(video, index: index))
            }
            for channel in vm.channels {
                results.append(.channel(channel))
            }
        case .channel:
            // Show channels first, then any videos/playlists server returned
            for channel in vm.channels {
                results.append(.channel(channel))
            }
            for (index, video) in vm.videos.enumerated() {
                results.append(.video(video, index: index))
            }
            for playlist in vm.playlists {
                results.append(.playlist(playlist))
            }
        case .all:
            // Already handled above
            break
        }

        return results
    }

    /// Background style based on layout and list style.
    private var resultsBackgroundStyle: ListBackgroundStyle {
        if layout == .list && listStyle == .inset {
            return .grouped
        } else {
            return .plain
        }
    }

    @ViewBuilder
    private var resultsViewWithLoading: some View {
        if searchViewModel != nil {
            #if os(tvOS)
            ProgressView()
                .accessibilityIdentifier("search.loading")
                .frame(maxWidth: .infinity)
                .padding(.top, 40)
            #else
            resultsBackgroundStyle.color
                .ignoresSafeArea()
                .overlay(
                    ScrollView {
                        VStack(spacing: 16) {
                            #if os(iOS)
                            if searchInstance?.supportsSearchFilters == true {
                                searchFiltersStrip
                            }
                            #endif

                            ProgressView()
                                .accessibilityIdentifier("search.loading")
                                .frame(maxWidth: .infinity)
                                .padding(.top, 40)
                        }
                    }
                )
            #endif
        }
    }

    @ViewBuilder
    private var resultsView: some View {
        if searchViewModel != nil {
            #if os(tvOS)
            ScrollView {
                if layout == .list {
                    listResultsContent
                } else {
                    gridResultsContent
                }
            }
            .scrollClipDisabled()
            .accessibilityLabel("search.results")
            .background(
                GeometryReader { geometry in
                    Color.clear
                        .onAppear {
                            viewWidth = geometry.size.width
                        }
                        .onChange(of: geometry.size.width) { _, newWidth in
                            viewWidth = newWidth
                        }
                }
            )
            #else
            resultsBackgroundStyle.color
                .ignoresSafeArea()
                .overlay(
                    ScrollView {
                        if layout == .list {
                            listResultsContent
                        } else {
                            gridResultsContent
                        }
                    }
                    .accessibilityLabel("search.results")
                    .background(
                        GeometryReader { geometry in
                            Color.clear
                                .onAppear {
                                    viewWidth = geometry.size.width
                                }
                                .onChange(of: geometry.size.width) { _, newWidth in
                                    viewWidth = newWidth
                                }
                        }
                    )
                )
            #endif
        }
    }

    @ViewBuilder
    private var listResultsContent: some View {
        if searchViewModel != nil {
            if listStyle == .inset {
                insetListContent
            } else {
                plainListContent
            }
        }
    }

    @ViewBuilder
    private var insetListContent: some View {
        if let vm = searchViewModel {
            VStack(spacing: 0) {
                // Filter strip at top (only for instances that support search filters)
                #if os(iOS)
                if searchInstance?.supportsSearchFilters == true {
                    searchFiltersStrip
                }
                #endif

                // Card container
                VideoListContent(listStyle: .inset) {
                    ForEach(Array(unifiedResults.enumerated()), id: \.element.id) { resultIndex, item in
                        searchResultRow(item: item, resultIndex: resultIndex, vm: vm)
                    }

                    LoadMoreTrigger(
                        isLoading: vm.isLoadingMore || (vm.isSearching && vm.page == 1),
                        hasMore: vm.hasMoreResults
                    ) {
                        Task { await vm.loadMore() }
                    }
                }
            }
        }
    }

    @ViewBuilder
    private var plainListContent: some View {
        if let vm = searchViewModel {
            VStack(spacing: 0) {
                // Filter strip at top (only for instances that support search filters)
                #if os(iOS)
                if searchInstance?.supportsSearchFilters == true {
                    searchFiltersStrip
                }
                #endif

                VideoListContent(listStyle: .plain) {
                    ForEach(Array(unifiedResults.enumerated()), id: \.element.id) { resultIndex, item in
                        searchResultRow(item: item, resultIndex: resultIndex, vm: vm)
                    }

                    LoadMoreTrigger(
                        isLoading: vm.isLoadingMore || (vm.isSearching && vm.page == 1),
                        hasMore: vm.hasMoreResults
                    ) {
                        Task { await vm.loadMore() }
                    }
                }
            }
        }
    }

    /// Single row for a search result item (video, playlist, or channel).
    @ViewBuilder
    private func searchResultRow(item: SearchResultItem, resultIndex: Int, vm: SearchViewModel) -> some View {
        VideoListRow(
            isLast: resultIndex == unifiedResults.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: searchQueueSource,
                        sourceLabel: String(localized: "queue.source.search \(searchTextBinding.wrappedValue)"),
                        videoList: vm.videos,
                        videoIndex: videoIndex,
                        loadMoreVideos: loadMoreSearchResultsCallback
                    )
                    #if !os(tvOS)
                    .videoSwipeActions(video: video)
                    #endif
                    .onAppear {
                        if resultIndex == unifiedResults.count - 3 {
                            Task { await vm.loadMore() }
                        }
                    }

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

    @ViewBuilder
    private var gridResultsContent: some View {
        if let vm = searchViewModel {
            LazyVStack(spacing: 16) {
                // Filter strip at top (only for instances that support search filters)
                #if os(iOS)
                if searchInstance?.supportsSearchFilters == true {
                    searchFiltersStrip
                        .padding(.bottom, 8)
                }
                #endif

                // Single grid with mixed content preserving order
                VideoGridContent(columns: gridConfig.effectiveColumns) {
                    ForEach(Array(unifiedResults.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
                            )
                            .frame(maxHeight: .infinity, alignment: .top)
                            .tappableVideo(
                                video,
                                queueSource: searchQueueSource,
                                sourceLabel: String(localized: "queue.source.search \(searchTextBinding.wrappedValue)"),
                                videoList: vm.videos,
                                videoIndex: videoIndex,
                                loadMoreVideos: loadMoreSearchResultsCallback
                            )
                            .onAppear {
                                if resultIndex == unifiedResults.count - 3 {
                                    Task { await vm.loadMore() }
                                }
                            }

                        case .playlist(let playlist):
                            NavigationLink(value: NavigationDestination.playlist(.remote(playlist.id, instance: nil, title: playlist.title))) {
                                PlaylistCardView(playlist: playlist, isCompact: gridConfig.isCompactCards)
                                    .frame(maxHeight: .infinity, alignment: .top)
                                    .contentShape(Rectangle())
                            }
                            .zoomTransitionSource(id: playlist.id)
                            .buttonStyle(.plain)
                            .onAppear {
                                if resultIndex == unifiedResults.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)
                                    .frame(maxHeight: .infinity, alignment: .top)
                                    .contentShape(Rectangle())
                            }
                            .zoomTransitionSource(id: channel.id.channelID)
                            .buttonStyle(.plain)
                            .onAppear {
                                if resultIndex == unifiedResults.count - 3 {
                                    Task { await vm.loadMore() }
                                }
                            }
                        }
                    }
                }

                // Loading indicator at bottom
                if vm.isLoadingMore || (vm.isSearching && vm.page == 1) {
                    ProgressView()
                        .frame(maxWidth: .infinity)
                        .padding()
                }
            }
        }
    }

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

    // MARK: - Search History Helpers

    private func loadWatchEntries() {
        watchEntriesMap = appEnvironment?.dataManager.watchEntriesMap() ?? [:]
    }

    private func watchProgress(for video: Video) -> Double? {
        guard let entry = watchEntriesMap[video.id.videoID] else { return nil }
        let progress = entry.progress
        return progress > 0 && progress < 1 ? progress : nil
    }

    private func loadSearchHistory() {
        guard appEnvironment?.settingsManager.saveRecentSearches != false else {
            searchHistory = []
            return
        }
        guard let limit = appEnvironment?.settingsManager.searchHistoryLimit else { return }
        searchHistory = appEnvironment?.dataManager.fetchSearchHistory(limit: limit) ?? []
    }

    private func deleteHistory(_ history: SearchHistory) {
        appEnvironment?.dataManager.deleteSearchQuery(history)
    }

    private func clearAllHistory() {
        appEnvironment?.dataManager.clearSearchHistory()
        isSearchHistoryExpanded = false
    }

    private func executeSearch(_ query: String) {
        dismissKeyboard()
        suppressNextTextChangeSearch = (query != searchTextBinding.wrappedValue)
        searchTextBinding.wrappedValue = query
        searchViewModel?.cancelSuggestions()
        searchViewModel?.filters.type = .video
        if let filters = searchViewModel?.filters {
            saveFilters(filters)
        }
        Task { await searchViewModel?.search(query: query) }
    }

    // MARK: - Recent Channels/Playlists Helpers

    private func loadRecentChannels() {
        guard appEnvironment?.settingsManager.saveRecentChannels != false else {
            recentChannels = []
            return
        }
        guard let limit = appEnvironment?.settingsManager.searchHistoryLimit else { return }
        recentChannels = appEnvironment?.dataManager.fetchRecentChannels(limit: limit) ?? []
    }

    private func loadRecentPlaylists() {
        guard appEnvironment?.settingsManager.saveRecentPlaylists != false else {
            recentPlaylists = []
            return
        }
        guard let limit = appEnvironment?.settingsManager.searchHistoryLimit else { return }
        recentPlaylists = appEnvironment?.dataManager.fetchRecentPlaylists(limit: limit) ?? []
    }

    private func deleteRecentChannel(_ channel: RecentChannel) {
        appEnvironment?.dataManager.deleteRecentChannel(channel)
    }

    private func deleteRecentPlaylist(_ playlist: RecentPlaylist) {
        appEnvironment?.dataManager.deleteRecentPlaylist(playlist)
    }

    private func clearAllRecentChannels() {
        appEnvironment?.dataManager.clearRecentChannels()
    }

    private func clearAllRecentPlaylists() {
        appEnvironment?.dataManager.clearRecentPlaylists()
    }

    private func clearAllRecents() {
        appEnvironment?.dataManager.clearSearchHistory()
        appEnvironment?.dataManager.clearRecentChannels()
        appEnvironment?.dataManager.clearRecentPlaylists()
        isSearchHistoryExpanded = false
    }

    /// Converts RecentChannel to Channel for display
    private func channelFromRecent(_ recent: RecentChannel) -> Channel {
        Channel(
            id: ChannelID(
                source: sourceFromRawValue(recent.sourceRawValue, instanceURL: recent.instanceURLString),
                channelID: recent.channelID
            ),
            name: recent.name,
            subscriberCount: recent.subscriberCount,
            thumbnailURL: recent.thumbnailURLString.flatMap { URL(string: $0) },
            isVerified: recent.isVerified
        )
    }

    /// Converts RecentPlaylist to Playlist for display
    private func playlistFromRecent(_ recent: RecentPlaylist) -> Playlist {
        Playlist(
            id: playlistIDFromRecent(recent),
            title: recent.title,
            author: Author(id: "", name: recent.authorName),
            videoCount: recent.videoCount,
            thumbnailURL: recent.thumbnailURLString.flatMap { URL(string: $0) }
        )
    }

    /// Reconstructs PlaylistID from RecentPlaylist
    private func playlistIDFromRecent(_ recent: RecentPlaylist) -> PlaylistID {
        PlaylistID(
            source: sourceFromRawValue(recent.sourceRawValue, instanceURL: recent.instanceURLString),
            playlistID: recent.playlistID
        )
    }

    /// Reconstructs ContentSource from raw values
    private func sourceFromRawValue(_ rawValue: String, instanceURL: String?) -> ContentSource {
        switch rawValue {
        case "global":
            return .global(provider: ContentSource.youtubeProvider)
        case "federated":
            if let urlString = instanceURL, let url = URL(string: urlString) {
                return .federated(provider: ContentSource.peertubeProvider, instance: url)
            }
            return .global(provider: ContentSource.youtubeProvider)
        case "extracted":
            if let urlString = instanceURL, let url = URL(string: urlString) {
                return .extracted(extractor: "generic", originalURL: url)
            }
            return .global(provider: ContentSource.youtubeProvider)
        default:
            return .global(provider: ContentSource.youtubeProvider)
        }
    }

    // MARK: - Video Queue Continuation

    @Sendable
    private func loadMoreSearchResultsCallback() async throws -> ([Video], String?) {
        guard let searchViewModel else {
            throw NSError(
                domain: "SearchView",
                code: -1,
                userInfo: [NSLocalizedDescriptionKey: "Search view model not initialized"]
            )
        }

        // Check if there are more results to load
        guard searchViewModel.hasMoreResults else {
            return ([], nil)
        }

        // Load more results using SearchViewModel's pagination
        await searchViewModel.loadMore()

        // Return newly loaded videos and next page as continuation
        let videos = searchViewModel.videos
        let nextPage = searchViewModel.page
        let hasMore = searchViewModel.hasMoreResults

        // Convert next page to continuation string (only if there are more results)
        let continuation = hasMore ? String(nextPage) : nil

        return (videos, continuation)
    }
}

// MARK: - Search Filters Sheet

struct SearchFiltersSheet: View {
    @Environment(\.dismiss) private var dismiss

    let onApply: () -> Void

    @Binding var filters: SearchFilters

    var body: some View {
        #if os(macOS)
        // Popover content: filters apply live, click-outside dismisses.
        filtersForm
            .onChange(of: filters) { _, _ in onApply() }
            .padding()
            .frame(width: 300)
        #else
        NavigationStack {
            filtersForm
            .navigationTitle(String(localized: "search.filters"))
            #if os(iOS)
            .navigationBarTitleDisplayMode(.inline)
            #endif
            .toolbar {
                ToolbarItem(placement: .cancellationAction) {
                    Button(String(localized: "common.cancel")) {
                        dismiss()
                    }
                }
                ToolbarItem(placement: .confirmationAction) {
                    Button(String(localized: "common.apply")) {
                        onApply()
                        dismiss()
                    }
                }
            }
        }
        #endif
    }

    private var filtersForm: some View {
        Form {
            // Sort, Upload Date, Duration in one section
            Section {
                Picker(String(localized: "search.sort"), selection: $filters.sort) {
                    ForEach(SearchSortOption.allCases) { option in
                        Text(option.title).tag(option)
                    }
                }

                Picker(String(localized: "search.uploadDate"), selection: $filters.date) {
                    ForEach(SearchDateFilter.allCases) { option in
                        Text(option.title).tag(option)
                    }
                }

                Picker(String(localized: "search.duration"), selection: $filters.duration) {
                    ForEach(SearchDurationFilter.allCases) { option in
                        Text(option.title).tag(option)
                    }
                }
            }

            // Reset Button
            Section {
                Button(role: .destructive) {
                    let currentType = filters.type
                    filters = .defaults
                    filters.type = currentType
                } label: {
                    HStack {
                        Spacer()
                        Text(String(localized: "search.filters.reset"))
                        Spacer()
                    }
                }
                .disabled(filters.isDefault)
            }
        }
    }
}



// MARK: - Preview

#Preview {
    NavigationStack {
        SearchView()
    }
    .appEnvironment(.preview)
}
