//
//  VideoInfoView.swift
//  Yattee
//
//  Full-screen video information page with technical metadata and comments.
//

import SwiftUI
import NukeUI

/// Initialization mode for VideoInfoView - either a loaded video or just an ID to fetch.
enum VideoInfoInitMode: Sendable {
    case video(Video)
    case videoID(VideoID)
}

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

    private let initMode: VideoInfoInitMode

    // Video loading state (for videoID mode)
    @State private var loadedVideo: Video?
    @State private var isLoadingInitialVideo = false
    @State private var initialVideoLoadError: String?
    
    // Navigation state - track current position in queue
    @State private var currentVideoIndex: Int?
    @State private var scrollViewID = UUID()
    @State private var scrollOffset: CGFloat = 0

    // Carousel scroll state (iOS only) - separate from currentVideoIndex to avoid mid-scroll updates
    #if os(iOS)
    @State private var carouselScrollPosition: Int?
    #endif
    
    // Continuation loading state
    @State private var extendedVideoList: [Video] = []
    @State private var isLoadingMoreVideos = false
    @State private var loadMoreError: String?
    
    @State private var isBookmarked = false
    @State private var showingPlaylistSheet = false
    @State private var showingCommentsSheet = false
    @State private var showingRemoveBookmarkAlert = false
    @State private var currentBookmark: Bookmark?
    @State private var bookmarkTags: [String] = []
    @State private var bookmarkNote: String = ""
    @State private var bookmarkSaveTask: Task<Void, Never>?
    @State private var isEditingBookmarkNote = false
    @State private var isEditingBookmarkTags = false
    @FocusState private var isBookmarkNoteFocused: Bool
    #if os(tvOS)
    @FocusState private var isPlayFocused: Bool
    @State private var isDescriptionScrollLocked = false
    #endif

    // Comments state (independent from PlayerState)
    @State private var comments: [Comment] = []
    @State private var commentsState: CommentsLoadState = .idle
    @State private var commentsContinuation: String?

    // Collapsible section states
    @State private var isStatsExpanded = true
    @State private var isDescriptionExpanded = true
    @State private var isRelatedExpanded = true
    @State private var isCommentsExpanded = true
    @State private var isWatchHistoryExpanded = false
    @State private var isBookmarkExpanded = true
    @State private var isDownloadExpanded = false
    @State private var isOriginalTitleExpanded = true

    // MARK: - Initializers
    
    /// Initialize with a loaded video.
    init(video: Video) {
        self.initMode = .video(video)
    }
    
    /// Initialize with a video ID to fetch.
    init(videoID: VideoID) {
        self.initMode = .videoID(videoID)
    }
    
    // MARK: - Computed Properties
    
    /// The video from init or loaded from API (nil while loading in videoID mode).
    private var video: Video? {
        switch initMode {
        case .video(let v): return v
        case .videoID: return loadedVideo
        }
    }

    // Watch history state
    @State private var watchEntry: WatchEntry?

    // Download state
    @State private var download: Download?
    @State private var showingDownloadSheet = false
    @State private var isEnqueuingDownload = false
    @State private var showingRemoveDownloadConfirmation = false
    
    // Resume action sheet state
    @State private var resumeSheetData: ResumeSheetData?
    
    // Video details cache - stores full video details loaded from API
    @State private var loadedVideoDetails: [String: Video] = [:]
    @State private var isLoadingVideoDetails = false
    /// Combined video list including originally loaded videos and extended videos from continuation
    private var allVideos: [Video]? {
        guard let originalList = videoQueueContext?.videoList else { return nil }
        return originalList + extendedVideoList
    }
    
    /// The base video from queue or init parameter (before details are loaded).
    /// Returns nil only when in videoID mode and video hasn't loaded yet.
    private var baseVideo: Video? {
        if videoQueueContext != nil,
           let index = currentVideoIndex,
           let list = allVideos,
           index >= 0 && index < list.count {
            return list[index]
        }
        return video
    }
    
    /// The video currently being displayed - prefers cached full details if available.
    /// Returns nil only when in videoID mode and video hasn't loaded yet.
    private var displayedVideo: Video? {
        guard let base = baseVideo else { return nil }
        return loadedVideoDetails[base.id.videoID] ?? base
    }

    private var accentColor: Color {
        appEnvironment?.settingsManager.resolvedAccentColor ?? .accentColor
    }

    private var dataManager: DataManager? { appEnvironment?.dataManager }
    private var contentService: ContentService? { appEnvironment?.contentService }
    private var instancesManager: InstancesManager? { appEnvironment?.instancesManager }
    private var navigationCoordinator: NavigationCoordinator? { appEnvironment?.navigationCoordinator }
    private var playerService: PlayerService? { appEnvironment?.playerService }
    private var queueManager: QueueManager? { appEnvironment?.queueManager }
    #if !os(tvOS)
    private var downloadManager: DownloadManager? { appEnvironment?.downloadManager }
    #endif

    /// Returns the first enabled Yattee Server instance URL, if any.
    private var yatteeServerURL: URL? {
        instancesManager?.yatteeServerInstances.first { $0.isEnabled }?.url
    }

    /// Whether this video is from YouTube (global YouTube provider).
    private var isYouTube: Bool {
        guard let video = displayedVideo else { return false }
        if case .global(let provider) = video.id.source {
            return provider == ContentSource.youtubeProvider
        }
        return false
    }

    /// Whether comments are supported for this video source.
    private var supportsComments: Bool {
        // Only YouTube videos support comments via Invidious API
        isYouTube
    }

    /// Whether the description section should be visible.
    private var shouldShowDescriptionSection: Bool {
        guard !isLoadingVideoDetails else { return true }
        guard let description = displayedVideo?.description else { return false }
        return !description.isEmpty
    }

    /// Whether the stats section should be visible.
    /// Duration alone does not warrant showing stats.
    private var shouldShowStatsSection: Bool {
        guard let video = displayedVideo else { return false }
        return video.viewCount != nil || video.likeCount != nil || video.formattedPublishedDate != nil
    }

    /// Whether the original title section should be visible (when DeArrow replaces title).
    private var shouldShowOriginalTitleSection: Bool {
        guard let video = displayedVideo,
              let settingsManager = appEnvironment?.settingsManager,
              settingsManager.deArrowEnabled,
              settingsManager.deArrowReplaceTitles,
              let provider = appEnvironment?.deArrowBrandingProvider,
              provider.title(for: video) != nil else {
            return false
        }
        return true
    }

    /// Display string for the video source (YouTube, PeerTube • instance, or MediaSourceType • share name).
    private var videoSourceDisplay: String? {
        guard let video = displayedVideo else { return nil }
        switch video.id.source {
        case .global(let provider):
            return provider.prefix(1).uppercased() + provider.dropFirst()
        case .federated(_, let instance):
            return "PeerTube • \(instance.host ?? instance.absoluteString)"
        case .extracted(let extractor, _):
            // For media sources, show type + share name
            if let mediaSourceID = video.mediaSourceID,
               let mediaSource = appEnvironment?.mediaSourcesManager.source(byID: mediaSourceID) {
                return "\(mediaSource.type.displayName) • \(mediaSource.name)"
            }
            // Fallback for other extracted sources (e.g., Bilibili, Vimeo)
            let formatted = extractor.replacingOccurrences(of: "_", with: " ")
            return formatted.prefix(1).uppercased() + formatted.dropFirst()
        }
    }

    /// Navigation title - uses source label if available, otherwise video title
    private var displayTitle: String {
        videoQueueContext?.sourceLabel ?? displayedVideo?.title ?? ""
    }
    
    /// Thumbnail width - larger for carousel on iOS
    private var thumbnailWidth: CGFloat {
        #if os(iOS)
        return videoQueueContext != nil ? 280 : 240
        #else
        return 240
        #endif
    }
    
    /// Dynamic label for the play button - shows "Continue at XX:XX" if video has meaningful progress
    /// and resume setting is continueWatching or ask.
    private var playButtonLabel: String {
        guard let video = displayedVideo,
              !video.isLive,
              let savedProgress = dataManager?.watchProgress(for: video.id.videoID),
              savedProgress >= 5,
              video.duration > 0,
              savedProgress < video.duration * 0.9 else {
            return String(localized: "video.context.play")
        }
        
        let resumeAction = appEnvironment?.settingsManager.resumeAction ?? .continueWatching
        switch resumeAction {
        case .continueWatching, .ask:
            return String(localized: "resume.action.continueAt \(savedProgress.formattedAsTimestamp)")
        case .startFromBeginning:
            return String(localized: "video.context.play")
        }
    }

    var body: some View {
        Group {
            if isLoadingInitialVideo {
                ProgressView()
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
            } else if let error = initialVideoLoadError {
                videoLoadErrorView(error)
            } else if displayedVideo != nil {
                videoContent
            }
        }
        // Opaque background so the header gradient (which fades to windowBackgroundColor)
        // blends seamlessly even when presented outside a navigation stack.
        .opaqueWindowBackground()
        .task {
            await loadInitialVideoIfNeeded()
        }
        #if os(tvOS)
        .navigationTitle("")
        #else
        .navigationTitle(displayTitle)
        #endif
        #if os(iOS)
        .navigationBarTitleDisplayMode(.inline)
        #endif
        .onAppear {
            // Initialize current index from queue context
            if currentVideoIndex == nil, let context = videoQueueContext {
                currentVideoIndex = context.videoIndex
            }
            
            // Load initial video data (only if video is already loaded)
            guard displayedVideo != nil else { return }
            #if !os(tvOS)
            loadVideoData()
            #else
            if let video = displayedVideo {
                isBookmarked = dataManager?.isBookmarked(videoID: video.id.videoID) ?? false
                if isBookmarked, let bookmark = dataManager?.bookmark(for: video.id.videoID) {
                    currentBookmark = bookmark
                    bookmarkTags = bookmark.tags
                    bookmarkNote = bookmark.note ?? ""
                } else {
                    currentBookmark = nil
                    bookmarkTags = []
                    bookmarkNote = ""
                }
                watchEntry = dataManager?.watchEntry(for: video.id.videoID)
            }
            loadComments()
            DispatchQueue.main.async {
                isPlayFocused = true
            }
            #endif

            // Load full video details from API
            Task {
                await loadVideoDetails()
            }
        }
        .onDisappear {
            // Cancel any pending bookmark save
            bookmarkSaveTask?.cancel()
        }
        .onChange(of: currentVideoIndex) { oldValue, newValue in
            // Skip if this is the initial value setting (handled by onAppear)
            guard oldValue != nil else { return }

            // Reset state when navigating to different video
            showingCommentsSheet = false
            showingPlaylistSheet = false
            showingDownloadSheet = false
            showingRemoveBookmarkAlert = false
            bookmarkSaveTask?.cancel()
            comments = []
            commentsState = .idle
            commentsContinuation = nil
            scrollViewID = UUID() // Reset scroll position
            // Don't reset scrollOffset here - let the new scroll view report its geometry
            // Resetting to 0 causes the blur to jump to an incorrect position briefly
            
            // Load new video data
            #if !os(tvOS)
            loadVideoData()
            
            // Pre-load more videos if we're at 95% of the list
            if shouldPreloadMore {
                Task {
                    await loadMoreVideos()
                }
            }
            #else
            if let video = displayedVideo {
                isBookmarked = dataManager?.isBookmarked(videoID: video.id.videoID) ?? false
                if isBookmarked, let bookmark = dataManager?.bookmark(for: video.id.videoID) {
                    currentBookmark = bookmark
                    bookmarkTags = bookmark.tags
                    bookmarkNote = bookmark.note ?? ""
                } else {
                    currentBookmark = nil
                    bookmarkTags = []
                    bookmarkNote = ""
                }
                watchEntry = dataManager?.watchEntry(for: video.id.videoID)
            }
            loadComments()
            DispatchQueue.main.async {
                isPlayFocused = true
            }
            #endif

            // Load full video details from API
            Task {
                await loadVideoDetails()
            }
        }
        .sheet(isPresented: $showingPlaylistSheet) {
            if let video = displayedVideo {
                PlaylistSelectorSheet(video: video)
            }
        }
        #if os(tvOS)
        .fullScreenCover(isPresented: $showingCommentsSheet) {
            commentsSheetContent
        }
        #else
        .sheet(isPresented: $showingCommentsSheet) {
            commentsSheetContent
        }
        #endif
        .sheet(item: $resumeSheetData) { data in
            ResumeActionSheet(
                video: data.video,
                resumeTime: data.resumeTime,
                onContinue: { playVideoWithStartTime(data.resumeTime) },
                onStartOver: { playVideoWithStartTime(0) }
            )
        }
        #if !os(tvOS)
        .sheet(isPresented: $showingDownloadSheet) {
            if let video = displayedVideo {
                DownloadQualitySheet(video: video)
            }
        }
        .onChange(of: downloadManager?.completedDownloads) { _, newValue in
            // Update download state when completedDownloads changes
            if let video = displayedVideo {
                download = downloadManager?.download(for: video.id)
            }
        }
        .onChange(of: downloadManager?.activeDownloads) { _, newValue in
            // Update download state when activeDownloads changes (download started)
            if download == nil, let video = displayedVideo {
                download = downloadManager?.download(for: video.id)
            }
        }
        #endif
        .alert(String(localized: "bookmark.remove.title"), isPresented: $showingRemoveBookmarkAlert) {
            Button(String(localized: "common.cancel"), role: .cancel) {}
            Button(String(localized: "bookmark.remove.confirm"), role: .destructive) {
                removeBookmark()
            }
        } message: {
            Text(String(localized: "bookmark.remove.message"))
        }
        #if !os(tvOS)
        .alert(String(localized: "videoInfo.download.remove.title"), isPresented: $showingRemoveDownloadConfirmation) {
            Button(String(localized: "common.cancel"), role: .cancel) {}
            Button(String(localized: "videoInfo.download.remove.confirm"), role: .destructive) {
                if let download = download {
                    Task {
                        await downloadManager?.delete(download)
                    }
                }
            }
        } message: {
            Text(String(localized: "videoInfo.download.remove.message"))
        }
        #endif
    }
    
    // MARK: - Video Content
    
    /// Main video content view (shown after video is loaded).
    @ViewBuilder
    private var videoContent: some View {
        #if os(tvOS)
        tvOSVideoContent
        #else
        iOSVideoContent
        #endif
    }

    #if !os(tvOS)
    @ViewBuilder
    private var iOSVideoContent: some View {
        GeometryReader { geometry in
            let safeAreaTop = geometry.safeAreaInsets.top

            ZStack(alignment: .top) {
                // Blurred background layer - moves up with scroll to keep edge hidden
                // Add safeAreaTop to offset so background starts moving immediately when user scrolls from rest
                // (at rest, scrollOffset ≈ -safeAreaTop, so offset = max(0, 0) = 0)
                // Extra top padding keeps the clipped top edge hidden above the visible area during scroll
                let extraTopPadding: CGFloat = 150

                let blurOffset: CGFloat = -max(scrollOffset + safeAreaTop, 0) - extraTopPadding

                Color.clear
                    .frame(height: headerBackgroundHeight + extraTopPadding)
                    .frame(maxWidth: .infinity)
                    .background(alignment: .bottom) {
                        blurredThumbnailBackground
                    }
                    .clipped()
                    .offset(y: blurOffset)
                    .animation(.easeOut(duration: 0.2), value: scrollOffset)
                    .ignoresSafeArea()

                ScrollView {
                    VStack(alignment: .leading, spacing: 0) {
                        // Header with thumbnail, title, and channel
                        headerSection

                        // Action buttons
                        actionButtons

                        // Bookmark details section (only show if bookmarked)
                        if isBookmarked, let bookmark = currentBookmark {
                            Divider()
                                .padding(.horizontal)

                            bookmarkDetailsSection(bookmark)
                        }

                        // Description section (collapsible) - only show if description available
                        if shouldShowDescriptionSection {
                            Divider()
                                .padding(.horizontal)

                            descriptionSection
                        }

                        // Original title section - only show when DeArrow title is active
                        if shouldShowOriginalTitleSection {
                            Divider()
                                .padding(.horizontal)

                            originalTitleSection
                        }

                        // Stats section (collapsible) - only show if meaningful stats exist
                        if shouldShowStatsSection {
                            Divider()
                                .padding(.horizontal)

                            statsSection
                        }

                        Divider()
                            .padding(.horizontal)

                        // Comments section (collapsible)
                        commentsSection

                        // Related videos section (collapsible, only shown if videos exist)
                        if let relatedVideos = displayedVideo?.relatedVideos, !relatedVideos.isEmpty {
                            Divider()
                                .padding(.horizontal)

                            relatedVideosSection(relatedVideos)
                        }


                        // Watch history section (only show if entry exists)
                        if let entry = watchEntry {
                            Divider()
                                .padding(.horizontal)

                            watchHistorySection(entry)
                        }

                        #if !os(tvOS)
                        // Download section (only show if downloaded)
                        if let download = download, download.status == .completed {
                            Divider()
                                .padding(.horizontal)

                            downloadSection(download)
                        }
                        #endif
                    }
                    .id(currentVideoIndex) // Force re-render when video changes
                }
                #if !os(tvOS)
                .scrollDismissesKeyboard(.interactively)
                #endif
                .softTopScrollEdgeEffect()
                .modifier(VideoInfoScrollOffsetModifier(scrollOffset: $scrollOffset))

                #if os(macOS)
                videoNavigationKeyboardShortcuts
                #endif
            }
        }
    }
    #endif

    #if os(macOS)
    // Bare left/right arrow keys switch to previous/next video in the queue.
    // Command-modified arrows are reserved for player seek shortcuts (see PlaybackCommands),
    // so these don't collide. Disabled buttons don't fire their shortcuts, which also
    // prevents activation when no queue context exists.
    @ViewBuilder
    private var videoNavigationKeyboardShortcuts: some View {
        Group {
            Button(action: navigateToPrevious) { EmptyView() }
                .keyboardShortcut(.leftArrow, modifiers: [])
                .disabled(!canNavigatePrevious)

            Button(action: navigateToNext) { EmptyView() }
                .keyboardShortcut(.rightArrow, modifiers: [])
                .disabled(!canNavigateNext)
        }
        .frame(width: 0, height: 0)
        .opacity(0)
        .accessibilityHidden(true)
    }
    #endif

    #if os(tvOS)
    // MARK: - tvOS Two-Column Layout

    @ViewBuilder
    private var tvOSVideoContent: some View {
        GeometryReader { geometry in
            let leftWidth = geometry.size.width * 0.30
            HStack(alignment: .top, spacing: 40) {
                tvOSLeftColumn
                    .frame(width: leftWidth, alignment: .leading)
                    .focusSection()

                tvOSRightColumn
                    .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
                    .focusSection()
            }
            .padding(.horizontal, 60)
            .padding(.vertical, 40)
        }
    }

    private var tvOSLeftColumn: some View {
        VStack(alignment: .leading, spacing: 24) {
            if let video = displayedVideo {
                tvOSThumbnail(for: video)
                    .frame(maxWidth: .infinity)
                    .aspectRatio(16.0 / 9.0, contentMode: .fit)
                    .clipShape(RoundedRectangle(cornerRadius: 12))

                Text(video.displayTitle(using: appEnvironment?.deArrowBrandingProvider))
                    .font(.title3)
                    .fontWeight(.semibold)
                    .foregroundStyle(.white)
                    .fixedSize(horizontal: false, vertical: true)
                    .frame(maxWidth: .infinity, alignment: .leading)

                tvOSChannelRow(for: video)

                Button(action: playVideo) {
                    Label(playButtonLabel, systemImage: "play.fill")
                        .frame(maxWidth: .infinity, minHeight: 60)
                        .font(.headline)
                }
                .buttonStyle(.borderedProminent)
                .focused($isPlayFocused)

                Button {
                    showingPlaylistSheet = true
                } label: {
                    Label(String(localized: "video.context.addToPlaylist"), systemImage: "text.badge.plus")
                        .frame(maxWidth: .infinity, minHeight: 50)
                }
                .buttonStyle(.bordered)

                Button {
                    toggleBookmark()
                } label: {
                    Label(
                        isBookmarked ? String(localized: "video.removeBookmark") : String(localized: "video.bookmark"),
                        systemImage: isBookmarked ? "bookmark.fill" : "bookmark"
                    )
                    .frame(maxWidth: .infinity, minHeight: 50)
                }
                .buttonStyle(.bordered)
            }

            Spacer(minLength: 0)
        }
    }

    @ViewBuilder
    private func tvOSThumbnail(for video: Video) -> some View {
        let deArrowURL = appEnvironment?.deArrowBrandingProvider.thumbnailURL(for: video)
        let thumbnailURLs = [deArrowURL].compactMap { $0 } + video.thumbnailURLsByQuality

        FallbackLazyImage(urls: thumbnailURLs) { state in
            if let image = state.image {
                image
                    .resizable()
                    .aspectRatio(16.0 / 9.0, contentMode: .fill)
            } else {
                Rectangle()
                    .fill(.quaternary)
                    .overlay {
                        Text(video.displayTitle(using: appEnvironment?.deArrowBrandingProvider))
                            .font(.caption)
                            .fontWeight(.medium)
                            .foregroundStyle(.secondary)
                            .multilineTextAlignment(.center)
                            .lineLimit(3)
                            .padding(8)
                    }
            }
        }
    }

    @ViewBuilder
    private func tvOSChannelRow(for video: Video) -> some View {
        if video.author.hasRealChannelInfo {
            Button {
                navigationCoordinator?.navigateToChannel(for: video)
            } label: {
                channelRowContent(for: video)
            }
            .buttonStyle(.plain)
        } else {
            channelRowContent(for: video)
        }
    }

    private var tvOSRightColumn: some View {
        GeometryReader { geometry in
            ScrollView {
                VStack(alignment: .leading, spacing: 0) {
                    if !isDescriptionScrollLocked, isBookmarked, let bookmark = currentBookmark {
                        bookmarkDetailsSection(bookmark)

                        Divider()
                            .padding(.horizontal)
                            .padding(.vertical, 16)
                    }

                    if isLoadingVideoDetails {
                        HStack {
                            Spacer()
                            ProgressView()
                            Spacer()
                        }
                        .frame(height: 450)
                        .padding(.horizontal)

                        Divider()
                            .padding(.horizontal)
                            .padding(.vertical, 16)
                    } else if let description = displayedVideo?.description, !description.isEmpty {
                        TVScrollableDescription(
                            description: description,
                            isScrollLocked: $isDescriptionScrollLocked,
                            showsHeader: false
                        )
                        .frame(height: isDescriptionScrollLocked ? geometry.size.height : 450)
                        .padding(.horizontal)

                        if !isDescriptionScrollLocked {
                            Divider()
                                .padding(.horizontal)
                                .padding(.vertical, 16)
                        }
                    }

                    if !isDescriptionScrollLocked {
                        if shouldShowOriginalTitleSection {
                            originalTitleSection

                            Divider()
                                .padding(.horizontal)
                                .padding(.vertical, 16)
                        }

                        if shouldShowStatsSection {
                            statsSection

                            Divider()
                                .padding(.horizontal)
                                .padding(.vertical, 16)
                        }

                        commentsSection

                        if let relatedVideos = displayedVideo?.relatedVideos, !relatedVideos.isEmpty {
                            Divider()
                                .padding(.horizontal)
                                .padding(.vertical, 16)

                            relatedVideosSection(relatedVideos)
                        }

                        if let entry = watchEntry {
                            Divider()
                                .padding(.horizontal)
                                .padding(.vertical, 16)

                            watchHistorySection(entry)
                        }
                    }
                }
                .padding(.vertical, 20)
                .id(currentVideoIndex)
                .animation(.easeInOut(duration: 0.25), value: isDescriptionScrollLocked)
            }
            .scrollClipDisabled()
            .scrollDisabled(isDescriptionScrollLocked)
        }
    }
    #endif

    /// Error view shown when video fails to load (videoID init mode only).
    @ViewBuilder
    private func videoLoadErrorView(_ message: String) -> some View {
        ContentUnavailableView {
            Label(String(localized: "video.error.title"), systemImage: "exclamationmark.triangle")
        } description: {
            Text(message)
        } actions: {
            Button(String(localized: "common.retry")) {
                Task {
                    await loadInitialVideoIfNeeded()
                }
            }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }

    // MARK: - Header Section

    private var headerSection: some View {
        VStack(alignment: .center, spacing: 0) {
            #if os(iOS)
            if videoQueueContext != nil, let videos = allVideos, !videos.isEmpty {
                // iOS with queue: full-width carousel with peek thumbnails
                thumbnailCarousel
            } else {
                // iOS without queue or empty queue: just the thumbnail
                singleVideoCard
            }
            #else
            // Non-iOS platforms: just the thumbnail
            singleVideoCard
            #endif
        }
        .padding(.top)
    }

    /// Blurred thumbnail background for ambient effect behind the header
    @ViewBuilder
    private var blurredThumbnailBackground: some View {
        BlurredImageBackground(
            url: displayedVideo.flatMap { appEnvironment?.deArrowBrandingProvider.thumbnailURL(for: $0) } ?? displayedVideo?.reliableThumbnailURL,
            videoID: displayedVideo?.id.videoID,
            blurRadius: BlurredImageBackground.platformBlurRadius,
            scale: 1.8,
            gradientColor: headerBackgroundColor,
            contentOpacity: blurredBackgroundOpacity
        )
        .frame(height: headerBackgroundHeight)
    }

    /// Calculated height for the blurred background based on content
    private var headerBackgroundHeight: CGFloat {
        // Thumbnail height + title area + channel area + padding
        let titleHeight: CGFloat = 50  // Title with line limit 2
        let channelHeight: CGFloat = 60 // Avatar + name + subscribers
        let padding: CGFloat = 60      // Top and bottom padding
        return thumbnailHeight + titleHeight + channelHeight + padding
    }

    /// Opacity for the blurred background that fades as user scrolls down
    private var blurredBackgroundOpacity: Double {
        let fadeDistance = headerBackgroundHeight * 0.5  // Fade 2x faster to hide hard edge before it's visible
        let progress = min(max(scrollOffset / fadeDistance, 0), 1)
        return 1.0 - progress
    }

    /// Platform-specific background color for gradient fade
    private var headerBackgroundColor: Color {
        #if os(iOS)
        Color(uiColor: .systemBackground)
        #elseif os(macOS)
        Color(nsColor: .windowBackgroundColor)
        #else
        Color.black
        #endif
    }
    
    /// Single video card without carousel (used when no queue context or non-iOS)
    @ViewBuilder
    private var singleVideoCard: some View {
        if let video = displayedVideo {
            videoCard(for: video, isLoadingMore: false, showTitle: true, isCurrent: true)
                .frame(maxWidth: .infinity)
                .padding(.horizontal)
        }
    }
    
    /// Spacing between carousel items
    private var carouselSpacing: CGFloat { 16 }
    
    /// Opacity for adjacent thumbnails (non-current videos)
    private var peekOpacity: Double { 0.5 }
    
    /// Full-width thumbnail carousel with peek effect (iOS only)
    /// Uses horizontal ScrollView with scrollTargetBehavior for native gesture handling
    @ViewBuilder
    private var thumbnailCarousel: some View {
        #if os(iOS)
        ScrollViewReader { proxy in
            ScrollView(.horizontal, showsIndicators: false) {
                LazyHStack(alignment: .top, spacing: carouselSpacing) {
                    if let videos = allVideos {
                        ForEach(Array(videos.enumerated()), id: \.element.id) { index, video in
                            let isCurrent = index == currentVideoIndex
                            // Prefer freshly loaded details for thumbnail (so expired proxy URLs get replaced) and avatar.
                            // Keep `video` as the card's stable identity so title/channel don't flicker.
                            let detailedVideo = loadedVideoDetails[video.id.videoID]
                            videoCard(for: video, thumbnailFrom: detailedVideo, authorFrom: detailedVideo, isLoadingMore: isLoadingMoreVideos && isCurrent, showTitle: true, isCurrent: isCurrent)
                                .opacity(isCurrent ? 1.0 : peekOpacity)
                                .containerRelativeFrame(.horizontal)
                                .id(index)
                                .onTapGesture {
                                    if !isCurrent {
                                        withAnimation(.easeInOut(duration: 0.3)) {
                                            carouselScrollPosition = index
                                            currentVideoIndex = index
                                        }
                                    }
                                }
                        }
                        
                        // Placeholder for loading more
                        if videoQueueContext?.canLoadMore == true {
                            videoCardPlaceholder(isLoading: isLoadingMoreVideos)
                                .opacity(peekOpacity)
                                .containerRelativeFrame(.horizontal)
                                .id("load-more-placeholder")
                        }
                    }
                }
                .scrollTargetLayout()
            }
            .scrollTargetBehavior(.viewAligned(limitBehavior: .alwaysByOne))
            .scrollPosition(id: $carouselScrollPosition)
            .onScrollPhaseChange { oldPhase, newPhase in
                // Only update currentVideoIndex when scrolling ends (user lifts finger)
                if newPhase == .idle, let newIndex = carouselScrollPosition, newIndex != currentVideoIndex {
                    currentVideoIndex = newIndex
                }
            }
            .onChange(of: currentVideoIndex) { oldValue, newValue in
                // Sync scroll position when currentVideoIndex changes from outside (e.g., onAppear)
                if let newValue, newValue != carouselScrollPosition {
                    carouselScrollPosition = newValue
                    proxy.scrollTo(newValue, anchor: .center)
                }
            }
            .onAppear {
                // Initialize scroll position
                if carouselScrollPosition == nil, let index = currentVideoIndex {
                    carouselScrollPosition = index
                }
            }
        }
        #endif
    }
    
    /// Height of thumbnail based on width and 16:9 aspect ratio
    private var thumbnailHeight: CGFloat {
        thumbnailWidth * 9 / 16
    }
    
    /// A single video card with thumbnail, title, and channel info
    /// - Parameters:
    ///   - video: The video to display (used for title/channel - stable reference across carousel scrolls)
    ///   - thumbnailFrom: Optional video to source the thumbnail URL from (use the freshly loaded details so expired proxy URLs get replaced)
    ///   - authorFrom: Optional video to get author info from (for avatar URL from detailed video)
    ///   - isLoadingMore: Whether to show loading overlay for continuation loading
    ///   - showTitle: Whether to show the title and channel (animates in/out)
    ///   - isCurrent: Whether this is the currently selected video (thumbnail tap plays video)
    private func videoCard(for video: Video, thumbnailFrom: Video? = nil, authorFrom: Video? = nil, isLoadingMore: Bool, showTitle: Bool, isCurrent: Bool) -> some View {
        let thumbnailSource = thumbnailFrom ?? video
        let deArrowURL = appEnvironment?.deArrowBrandingProvider.thumbnailURL(for: thumbnailSource)
        let thumbnailURLs = [deArrowURL].compactMap { $0 } + thumbnailSource.thumbnailURLsByQuality
        return VStack(spacing: 12) {
            // Thumbnail with loading overlay
            ZStack {
                FallbackLazyImage(urls: thumbnailURLs) { state in
                    if let image = state.image {
                        image
                            .resizable()
                            .aspectRatio(16/9, contentMode: .fill)
                            .clipShape(RoundedRectangle(cornerRadius: 8))
                    } else {
                        Rectangle()
                            .fill(.quaternary)
                            .clipShape(RoundedRectangle(cornerRadius: 8))
                            .overlay {
                                Text(video.displayTitle(using: appEnvironment?.deArrowBrandingProvider))
                                    .font(.caption)
                                    .fontWeight(.medium)
                                    .foregroundStyle(.secondary)
                                    .multilineTextAlignment(.center)
                                    .lineLimit(3)
                                    .padding(8)
                            }
                    }
                }
                .aspectRatio(16/9, contentMode: .fit)
                .frame(width: thumbnailWidth)

                // Loading overlay when fetching more videos (continuation)
                if isLoadingMore {
                    RoundedRectangle(cornerRadius: 8)
                        .fill(.black.opacity(0.4))
                        .frame(width: thumbnailWidth)
                        .aspectRatio(16/9, contentMode: .fit)
                    
                    ProgressView()
                        .tint(.white)
                }
            }
            .onTapGesture {
                if isCurrent {
                    playVideo()
                }
            }
            
            // Title
            Text(video.displayTitle(using: appEnvironment?.deArrowBrandingProvider))
                .font(.headline)
                .lineLimit(2)
                .multilineTextAlignment(.center)
                .frame(width: thumbnailWidth)
                .opacity(showTitle ? 1.0 : 0.0)
            
            // Channel row (horizontal) - only tappable if we have real channel info
            // Use authorFrom for author info (includes avatar URL) if available
            Group {
                let authorVideo = authorFrom ?? video
                if authorVideo.author.hasRealChannelInfo {
                    Button {
                        navigationCoordinator?.navigateToChannel(for: authorVideo)
                    } label: {
                        channelRowContent(for: authorVideo)
                    }
                    .buttonStyle(.plain)
                } else {
                    channelRowContent(for: authorVideo)
                }
            }
            .opacity(showTitle ? 1.0 : 0.0)
        }
    }

    /// Channel row content used in both tappable and non-tappable variants
    private func channelRowContent(for video: Video) -> some View {
        let enrichedAuthor = appEnvironment.map { video.author.enriched(using: $0.dataManager) } ?? video.author
        return HStack(spacing: 10) {
            ChannelAvatarView(
                author: enrichedAuthor,
                size: 40,
                yatteeServerURL: yatteeServerURL,
                source: video.authorSource
            )

            VStack(alignment: .leading, spacing: 2) {
                Text(enrichedAuthor.name)
                    .font(.subheadline)
                    .fontWeight(.medium)
                    .lineLimit(1)

                Group {
                    if let subscribers = enrichedAuthor.formattedSubscriberCount {
                        Text(subscribers)
                    } else if isLoadingVideoDetails && video.supportsAPIStats {
                        Text("1.2M subscribers")
                            .redacted(reason: .placeholder)
                    }
                }
                .font(.caption)
                .foregroundStyle(.secondary)
                .lineLimit(1)
            }
        }
    }

    /// Placeholder card shown when loading more videos
    private func videoCardPlaceholder(isLoading: Bool) -> some View {
        VStack(spacing: 12) {
            // Thumbnail placeholder
            ZStack {
                RoundedRectangle(cornerRadius: 8)
                    .fill(.quaternary)
                    .aspectRatio(16/9, contentMode: .fit)
                    .frame(width: thumbnailWidth)
                
                if isLoading {
                    ProgressView()
                        .tint(.secondary)
                }
            }
            
            // Title placeholder
            VStack(spacing: 8) {
                RoundedRectangle(cornerRadius: 4)
                    .fill(.quaternary)
                    .frame(width: thumbnailWidth * 0.8, height: 16)
                
                RoundedRectangle(cornerRadius: 4)
                    .fill(.quaternary)
                    .frame(width: thumbnailWidth * 0.5, height: 12)
            }
            
            // Channel placeholder
            VStack(spacing: 4) {
                // Avatar placeholder (circle)
                Circle()
                    .fill(.quaternary)
                    .frame(width: 56, height: 56)
                
                // Name placeholder
                RoundedRectangle(cornerRadius: 4)
                    .fill(.quaternary)
                    .frame(width: thumbnailWidth * 0.4, height: 14)
                
                // Subscriber count placeholder
                RoundedRectangle(cornerRadius: 4)
                    .fill(.quaternary)
                    .frame(width: thumbnailWidth * 0.25, height: 12)
            }
        }
        .frame(width: thumbnailWidth)
    }

    // MARK: - Action Buttons

    /// Vertical action button with large icon on top and text below
    private func verticalActionButton(
        icon: String,
        label: String,
        action: @escaping () -> Void
    ) -> some View {
        Button(action: action) {
            #if os(macOS)
            Label(label, systemImage: icon)
                .font(.body)
                .frame(maxWidth: .infinity)
                .padding(.vertical, 4)
            #else
            VStack(spacing: 6) {
                Image(systemName: icon)
                    .font(.system(size: 18))
                Text(label)
                    .font(.caption)
            }
            .frame(maxWidth: .infinity, minHeight: 50)
            #endif
        }
        #if os(macOS)
        .buttonStyle(.bordered)
        #else
        .buttonStyle(.borderedProminent)
        #endif
    }

    private var actionButtons: some View {
        VStack(spacing: 12) {
            #if !os(tvOS)
            // Play, Download and Share (vertical layout)
            HStack(spacing: 12) {
                // Play button
                verticalActionButton(
                    icon: "play.fill",
                    label: String(localized: "video.context.play"),
                    action: playVideo
                )
                #if os(macOS)
                .tint(accentColor)
                #endif

                // Download button
                verticalDownloadActionButton

                // Share button
                if let video = displayedVideo {
                    ShareLink(item: video.shareURL) {
                        #if os(macOS)
                        Label(String(localized: "video.share"), systemImage: "square.and.arrow.up")
                            .font(.body)
                            .frame(maxWidth: .infinity)
                            .padding(.vertical, 4)
                        #else
                        VStack(spacing: 6) {
                            Image(systemName: "square.and.arrow.up")
                                .font(.system(size: 18))
                            Text(String(localized: "video.share"))
                                .font(.caption)
                        }
                        .frame(maxWidth: .infinity, minHeight: 50)
                        #endif
                    }
                    #if os(macOS)
                    .buttonStyle(.bordered)
                    #else
                    .buttonStyle(.borderedProminent)
                    #endif
                }
            }
            .fixedSize(horizontal: false, vertical: true)
            #endif

            // Add to Playlist and Bookmark
            HStack(spacing: 12) {
                Button {
                    showingPlaylistSheet = true
                } label: {
                    Label(String(localized: "video.context.addToPlaylist"), systemImage: "text.badge.plus")
                        .frame(maxWidth: .infinity)
                        .font(.subheadline)
                }
                .buttonStyle(.bordered)

                Button {
                    toggleBookmark()
                } label: {
                    Label(
                        isBookmarked ? String(localized: "video.removeBookmark") : String(localized: "video.bookmark"),
                        systemImage: isBookmarked ? "bookmark.fill" : "bookmark"
                    )
                    .frame(maxWidth: .infinity)
                    .font(.subheadline)
                }
                .buttonStyle(.bordered)
            }
        }
        #if !os(macOS)
        .fontWeight(.semibold)
        #endif
        .frame(maxWidth: 400)
        .frame(maxWidth: .infinity)
        .padding(.horizontal)
        .padding(.vertical, 12)
    }

    #if !os(tvOS)
    /// Vertical download action button with four states: default, enqueueing, downloading, downloaded
    @ViewBuilder
    private var verticalDownloadActionButton: some View {
        let isDownloaded = download?.status == .completed
        let isDownloading = displayedVideo.flatMap { downloadManager?.isDownloading($0.id) } ?? false

        if isDownloaded {
            // Downloaded state - tap to show delete confirmation
            Button(role: .destructive) {
                showingRemoveDownloadConfirmation = true
            } label: {
                #if os(macOS)
                Label(String(localized: "video.downloaded"), systemImage: "checkmark.circle.fill")
                    .font(.body)
                    .frame(maxWidth: .infinity)
                    .padding(.vertical, 4)
                #else
                VStack(spacing: 6) {
                    Image(systemName: "checkmark.circle.fill")
                        .font(.system(size: 18))
                    Text(String(localized: "video.downloaded"))
                        .font(.caption)
                }
                .frame(maxWidth: .infinity, minHeight: 50)
                #endif
            }
            #if os(macOS)
            .buttonStyle(.bordered)
            #else
            .buttonStyle(.borderedProminent)
            #endif
        } else if isDownloading || isEnqueuingDownload {
            // Downloading/enqueueing state - shows progress
            Button {
                // No action while downloading
            } label: {
                #if os(macOS)
                HStack(spacing: 6) {
                    ProgressView()
                        .controlSize(.small)
                    if isEnqueuingDownload {
                        Text(String(localized: "video.downloading"))
                            .allowsTightening(true)
                    } else if let video = displayedVideo,
                              let progress = downloadManager?.downloadProgressByVideo[video.id],
                              !progress.isIndeterminate {
                        Text("\(Int(progress.progress * 100))%")
                            .monospacedDigit()
                    } else {
                        Text(String(localized: "video.downloading"))
                            .allowsTightening(true)
                    }
                }
                .font(.body)
                .frame(maxWidth: .infinity)
                .padding(.vertical, 4)
                #else
                VStack(spacing: 6) {
                    ProgressView()
                        .controlSize(.regular)
                    if isEnqueuingDownload {
                        Text(String(localized: "video.downloading"))
                            .allowsTightening(true)
                            .font(.caption)
                    } else if let video = displayedVideo,
                              let progress = downloadManager?.downloadProgressByVideo[video.id],
                              !progress.isIndeterminate {
                        Text("\(Int(progress.progress * 100))%")
                            .font(.caption)
                            .monospacedDigit()
                    } else {
                        Text(String(localized: "video.downloading"))
                            .allowsTightening(true)
                            .font(.caption)
                    }
                }
                .frame(maxWidth: .infinity, minHeight: 50)
                #endif
            }
            #if os(macOS)
            .buttonStyle(.bordered)
            #else
            .buttonStyle(.borderedProminent)
            #endif
            .disabled(true)
        } else if let video = displayedVideo {
            // Default state - download button
            Button {
                startDownload(for: video)
            } label: {
                #if os(macOS)
                Label(String(localized: "video.download"), systemImage: "arrow.down.circle")
                    .font(.body)
                    .frame(maxWidth: .infinity)
                    .padding(.vertical, 4)
                #else
                VStack(spacing: 6) {
                    Image(systemName: "arrow.down.circle")
                        .font(.system(size: 18))
                    Text(String(localized: "video.download"))
                        .font(.caption)
                }
                .frame(maxWidth: .infinity, minHeight: 50)
                #endif
            }
            #if os(macOS)
            .buttonStyle(.bordered)
            #else
            .buttonStyle(.borderedProminent)
            #endif
        }
    }

    /// Starts a download either automatically or by showing the quality sheet.
    private func startDownload(for video: Video) {
        guard let appEnvironment else {
            showingDownloadSheet = true
            return
        }

        // Media source videos (SMB/WebDAV/local) use direct file URLs - no API call needed
        if video.isFromMediaSource {
            isEnqueuingDownload = true
            Task {
                do {
                    try await appEnvironment.downloadManager.autoEnqueueMediaSource(
                        video,
                        mediaSourcesManager: appEnvironment.mediaSourcesManager,
                        webDAVClient: appEnvironment.webDAVClient,
                        smbClient: appEnvironment.smbClient
                    )
                } catch {
                    appEnvironment.toastManager.show(
                        category: .error,
                        title: String(localized: "download.error.title"),
                        subtitle: error.localizedDescription,
                        icon: "exclamationmark.triangle",
                        iconColor: .red
                    )
                }
                await MainActor.run {
                    isEnqueuingDownload = false
                }
            }
            return
        }

        let downloadSettings = appEnvironment.downloadSettings

        // Check if auto-download mode
        if downloadSettings.preferredDownloadQuality != .ask,
           let instance = appEnvironment.instancesManager.instance(for: video) {
            isEnqueuingDownload = true
            Task {
                do {
                    try await appEnvironment.downloadManager.autoEnqueue(
                        video,
                        preferredQuality: downloadSettings.preferredDownloadQuality,
                        preferredAudioLanguage: appEnvironment.settingsManager.preferredAudioLanguage,
                        preferredSubtitlesLanguage: appEnvironment.settingsManager.preferredSubtitlesLanguage,
                        includeSubtitles: downloadSettings.includeSubtitlesInAutoDownload,
                        contentService: appEnvironment.contentService,
                        instance: instance
                    )
                } catch {
                    appEnvironment.toastManager.show(
                        category: .error,
                        title: String(localized: "download.error.title"),
                        subtitle: error.localizedDescription,
                        icon: "exclamationmark.triangle",
                        iconColor: .red
                    )
                }
                await MainActor.run {
                    isEnqueuingDownload = false
                }
            }
        } else {
            showingDownloadSheet = true
        }
    }

    private func downloadSection(_ download: Download) -> some View {
        CollapsibleSection(title: String(localized: "videoInfo.section.download"), isExpanded: $isDownloadExpanded) {
            VStack(alignment: .leading, spacing: 12) {
                LazyVGrid(columns: [
                    GridItem(.flexible()),
                    GridItem(.flexible())
                ], spacing: 12) {
                    // Quality
                    infoRow(
                        label: String(localized: "videoInfo.download.quality"),
                        value: download.quality
                    )

                    // File size
                    infoRow(
                        label: String(localized: "videoInfo.download.fileSize"),
                        value: formatBytes(download.totalBytes)
                    )

                    // Video codec
                    if let codec = download.videoCodec {
                        infoRow(
                            label: String(localized: "videoInfo.download.videoCodec"),
                            value: codec.uppercased()
                        )
                    }

                    // Audio codec
                    if let codec = download.audioCodec {
                        infoRow(
                            label: String(localized: "videoInfo.download.audioCodec"),
                            value: codec.uppercased()
                        )
                    }

                    // Downloaded at
                    if let completedAt = download.completedAt {
                        infoRow(
                            label: String(localized: "videoInfo.download.downloadedAt"),
                            value: completedAt.formatted(date: .abbreviated, time: .shortened)
                        )
                    }

                    // Bitrates
                    if let videoBitrate = download.videoBitrate {
                        infoRow(
                            label: String(localized: "videoInfo.download.videoBitrate"),
                            value: formatBitrate(videoBitrate)
                        )
                    }

                    if let audioBitrate = download.audioBitrate {
                        infoRow(
                            label: String(localized: "videoInfo.download.audioBitrate"),
                            value: formatBitrate(audioBitrate)
                        )
                    }
                }

                Button(role: .destructive) {
                    showingRemoveDownloadConfirmation = true
                } label: {
                    Label(String(localized: "videoInfo.download.remove"), systemImage: "trash")
                        .frame(maxWidth: .infinity)
                }
                .buttonStyle(.bordered)
                .tint(.red)
                .padding(.top, 8)
            }
        }
    }

    private func formatBytes(_ bytes: Int64) -> String {
        let formatter = ByteCountFormatter()
        formatter.countStyle = .file
        return formatter.string(fromByteCount: bytes)
    }

    private func formatBitrate(_ bitrate: Int) -> String {
        if bitrate >= 1_000_000 {
            return String(format: "%.1f Mbps", Double(bitrate) / 1_000_000)
        } else {
            return String(format: "%d kbps", bitrate / 1000)
        }
    }
    #endif
    // MARK: - Stats Section

    @ViewBuilder
    private var statsSection: some View {
        if let video = displayedVideo {
            CollapsibleSection(title: String(localized: "videoInfo.section.stats"), isExpanded: $isStatsExpanded) {
                LazyVGrid(columns: [
                    GridItem(.flexible()),
                    GridItem(.flexible())
                ], spacing: 12) {
                    if let viewCount = video.viewCount {
                        infoRow(
                            label: String(localized: "videoInfo.views"),
                            value: CountFormatter.compact(viewCount)
                        )
                    }

                    if let likeCount = video.likeCount {
                        infoRow(
                            label: String(localized: "videoInfo.likes"),
                            value: CountFormatter.compact(likeCount)
                        )
                    }

                    if let publishedText = video.formattedPublishedDate {
                        infoRow(
                            label: String(localized: "videoInfo.published"),
                            value: publishedText
                        )
                    }

                    infoRow(
                        label: String(localized: "videoInfo.duration"),
                        value: video.formattedDuration
                    )

                    if let source = videoSourceDisplay {
                        infoRow(
                            label: String(localized: "videoInfo.source"),
                            value: source
                        )
                    }
                }
            }
        }
    }

    // MARK: - Watch History Section

    private func watchHistorySection(_ entry: WatchEntry) -> some View {
        CollapsibleSection(title: String(localized: "videoInfo.section.watchHistory"), isExpanded: $isWatchHistoryExpanded) {
            LazyVGrid(columns: [
                GridItem(.flexible()),
                GridItem(.flexible())
            ], spacing: 12) {
                // First watched
                infoRow(
                    label: String(localized: "videoInfo.firstWatched"),
                    value: entry.createdAt.formatted(date: .abbreviated, time: .shortened)
                )

                // Last watched
                infoRow(
                    label: String(localized: "videoInfo.lastWatched"),
                    value: entry.updatedAt.formatted(date: .abbreviated, time: .shortened)
                )

                // Progress
                VStack(alignment: .leading, spacing: 4) {
                    Text(String(localized: "videoInfo.progress"))
                        .font(.caption)
                        .foregroundStyle(.secondary)

                    HStack(spacing: 8) {
                        // Progress bar
                        GeometryReader { geometry in
                            ZStack(alignment: .leading) {
                                Capsule()
                                    .fill(.quaternary)
                                    .frame(height: 6)

                                Capsule()
                                    .fill(entry.isFinished ? Color.green : accentColor)
                                    .frame(width: geometry.size.width * entry.progress, height: 6)
                            }
                        }
                        .frame(height: 6)

                        // Percentage
                        Text("\(Int(entry.progress * 100))%")
                            .font(.caption)
                            .fontWeight(.medium)
                            .monospacedDigit()
                    }
                }
                .frame(maxWidth: .infinity, alignment: .leading)

                // Completed
                VStack(alignment: .leading, spacing: 2) {
                    Text(String(localized: "videoInfo.completed"))
                        .font(.caption)
                        .foregroundStyle(.secondary)

                    if let finishedAt = entry.finishedAt {
                        Text(finishedAt.formatted(date: .abbreviated, time: .shortened))
                            .font(.subheadline)
                            .fontWeight(.medium)
                    } else if entry.isFinished {
                        Text(String(localized: "common.yes"))
                            .font(.subheadline)
                            .fontWeight(.medium)
                    } else {
                        Text(String(localized: "videoInfo.notCompleted"))
                            .font(.subheadline)
                            .foregroundStyle(.secondary)
                    }
                }
                .frame(maxWidth: .infinity, alignment: .leading)
            }
        }
    }

    // MARK: - Bookmark Details Section
    
    private func bookmarkDetailsSection(_ bookmark: Bookmark) -> some View {
        CollapsibleSection(title: String(localized: "videoInfo.section.bookmarkDetails"), isExpanded: $isBookmarkExpanded) {
            VStack(alignment: .leading, spacing: 12) {
                // Bookmarked date
                VStack(alignment: .leading, spacing: 2) {
                    Text(String(localized: "videoInfo.bookmarkedAt"))
                        .font(.caption)
                        .foregroundStyle(.secondary)
                    
                    Text(bookmark.createdAt.formatted(date: .abbreviated, time: .shortened))
                        .font(.subheadline)
                        .fontWeight(.medium)
                }
                
                // Tags section
                VStack(alignment: .leading, spacing: 8) {
                    Text(String(localized: "videoInfo.bookmark.tags"))
                        .font(.subheadline)
                        .fontWeight(.medium)

                    if bookmarkTags.isEmpty && !isEditingBookmarkTags {
                        Button {
                            isEditingBookmarkTags = true
                        } label: {
                            Label(String(localized: "videoInfo.bookmark.addTags"), systemImage: "plus.circle")
                        }
                        .buttonStyle(.bordered)
                        .controlSize(.small)
                    } else {
                        TagInputView(tags: $bookmarkTags, isFocused: isEditingBookmarkTags)
                            .onChange(of: bookmarkTags) { _, newTags in
                                debouncedSaveBookmark()
                                if newTags.isEmpty {
                                    isEditingBookmarkTags = false
                                }
                            }
                    }
                }
                
                // Notes section
                VStack(alignment: .leading, spacing: 8) {
                    Text(String(localized: "videoInfo.bookmark.notes"))
                        .font(.subheadline)
                        .fontWeight(.medium)

                    if bookmarkNote.isEmpty && !isEditingBookmarkNote {
                        Button {
                            isEditingBookmarkNote = true
                            isBookmarkNoteFocused = true
                        } label: {
                            Label(String(localized: "videoInfo.bookmark.addNotes"), systemImage: "plus.circle")
                        }
                        .buttonStyle(.bordered)
                        .controlSize(.small)
                    } else {
                        #if os(tvOS)
                        TextField(String(localized: "videoInfo.bookmark.notes"), text: $bookmarkNote, axis: .vertical)
                            .frame(minHeight: 100)
                            .font(.subheadline)
                            .onChange(of: bookmarkNote) { _, _ in
                                debouncedSaveBookmark()
                            }
                        #else
                        TextEditor(text: $bookmarkNote)
                            .focused($isBookmarkNoteFocused)
                            .frame(minHeight: 100)
                            .font(.subheadline)
                            #if os(iOS)
                            .scrollContentBackground(.hidden)
                            .background(Color(uiColor: .secondarySystemGroupedBackground))
                            .clipShape(RoundedRectangle(cornerRadius: 8))
                            #elseif os(macOS)
                            .scrollContentBackground(.hidden)
                            .background(Color(nsColor: .controlBackgroundColor))
                            .clipShape(RoundedRectangle(cornerRadius: 8))
                            #endif
                            .onChange(of: bookmarkNote) { _, newValue in
                                if newValue.isEmpty {
                                    // Save immediately when cleared to prevent data loss
                                    bookmarkSaveTask?.cancel()
                                    saveBookmark()
                                } else {
                                    debouncedSaveBookmark()
                                }
                            }
                            .onChange(of: isBookmarkNoteFocused) { _, focused in
                                if focused {
                                    isEditingBookmarkNote = true
                                } else if bookmarkNote.isEmpty {
                                    isEditingBookmarkNote = false
                                }
                            }
                        #endif

                        // Character count when approaching limit
                        if bookmarkNote.count > 900 {
                            HStack {
                                Spacer()
                                Text("\(bookmarkNote.count)/1000")
                                    .font(.caption)
                                    .foregroundStyle(bookmarkNote.count > 1000 ? .red : .secondary)
                                    .monospacedDigit()
                            }
                        }
                    }
                }
            }
        }
    }

    private func infoRow(label: String, value: String) -> some View {
        VStack(alignment: .leading, spacing: 2) {
            Text(label)
                #if os(macOS)
                .font(.subheadline)
                #else
                .font(.caption)
                #endif
                .foregroundStyle(.secondary)

            Text(value)
                #if os(macOS)
                .font(.body)
                #else
                .font(.subheadline)
                #endif
                .fontWeight(.medium)
        }
        .frame(maxWidth: .infinity, alignment: .leading)
    }

    // MARK: - Description Section

    private var descriptionSection: some View {
        CollapsibleSection(title: String(localized: "video.description"), isExpanded: $isDescriptionExpanded) {
            if isLoadingVideoDetails {
                // Loading state
                HStack {
                    Spacer()
                    ProgressView()
                    Spacer()
                }
                .padding(.vertical, 20)
            } else if let description = displayedVideo?.description, !description.isEmpty {
                // Description available
                Text(DescriptionText.attributed(description, linkColor: accentColor))
                    #if os(macOS)
                    .font(.body)
                    #else
                    .font(.subheadline)
                    #endif
                    .foregroundStyle(.secondary)
                    .tint(accentColor)
                    .handleTimestampLinks(using: playerService)
                    #if !os(tvOS)
                    .textSelection(.enabled)
                    #endif
            }
        }
    }

    // MARK: - Original Title Section

    private var originalTitleSection: some View {
        CollapsibleSection(title: String(localized: "video.originalTitle"), isExpanded: $isOriginalTitleExpanded) {
            if let title = displayedVideo?.title {
                Text(title)
                    #if os(macOS)
                    .font(.body)
                    #else
                    .font(.subheadline)
                    #endif
                    .foregroundStyle(.secondary)
                    #if !os(tvOS)
                    .textSelection(.enabled)
                    #endif
            }
        }
    }

    // MARK: - Related Videos Section

    private func relatedVideosSection(_ videos: [Video]) -> some View {
        #if os(tvOS)
        let rowSpacing: CGFloat = 32
        #else
        let rowSpacing: CGFloat = 12
        #endif
        return CollapsibleSection(title: String(localized: "videoInfo.section.relatedVideos"), isExpanded: $isRelatedExpanded) {
            LazyVStack(spacing: rowSpacing) {
                ForEach(Array(videos.enumerated()), id: \.element.id) { index, relatedVideo in
                    VideoRowView(video: relatedVideo, style: .regular)
                        .tappableVideo(
                            relatedVideo,
                            queueSource: .manual,
                            sourceLabel: String(localized: "videoInfo.section.relatedVideos"),
                            videoList: videos,
                            videoIndex: index,
                            loadMoreVideos: nil
                        )
                        #if !os(tvOS)
                        .videoSwipeActions(video: relatedVideo)
                        #endif

                    #if !os(tvOS)
                    if index < videos.count - 1 {
                        Divider()
                            .padding(.leading, VideoRowStyle.regular.thumbnailWidth + 12)
                    }
                    #endif
                }
            }
        }
    }

    // MARK: - Comments Section

    @ViewBuilder
    private var commentsSection: some View {
        if supportsComments {
            CollapsibleSection(title: String(localized: "videoInfo.section.comments"), isExpanded: $isCommentsExpanded) {
                switch commentsState {
                case .idle, .loading:
                    HStack {
                        Spacer()
                        ProgressView()
                        Spacer()
                    }
                    .padding(.vertical, 20)

                case .disabled:
                    HStack {
                        Spacer()
                        VStack(spacing: 8) {
                            Image(systemName: "exclamationmark.bubble")
                                .font(.title2)
                                .foregroundStyle(.secondary)
                            Text(String(localized: "videoInfo.comments.disabled"))
                                #if os(macOS)
                                .font(.body)
                                #else
                                .font(.subheadline)
                                #endif
                                .foregroundStyle(.secondary)
                        }
                        Spacer()
                    }
                    .padding(.vertical, 20)

                case .error:
                    HStack {
                        Spacer()
                        VStack(spacing: 8) {
                            Image(systemName: "exclamationmark.triangle")
                                .font(.title2)
                                .foregroundStyle(.secondary)
                            Text(String(localized: "videoInfo.comments.error"))
                                #if os(macOS)
                                .font(.body)
                                #else
                                .font(.subheadline)
                                #endif
                                .foregroundStyle(.secondary)
                        }
                        Spacer()
                    }
                    .padding(.vertical, 20)

                case .loaded, .loadingMore:
                    if comments.isEmpty {
                        HStack {
                            Spacer()
                            Text(String(localized: "videoInfo.comments.empty"))
                                #if os(macOS)
                                .font(.body)
                                #else
                                .font(.subheadline)
                                #endif
                                .foregroundStyle(.secondary)
                            Spacer()
                        }
                        .padding(.vertical, 20)
                    } else if let video = displayedVideo {
                        VStack(alignment: .leading, spacing: 0) {
                            // Show first 3 comments as preview
                            ForEach(comments.prefix(3)) { comment in
                                CommentView(
                                    comment: comment,
                                    videoID: video.id.videoID,
                                    source: video.id.source,
                                    isReply: false
                                )
                                .padding(.vertical, 4)
                            }

                            // View All Comments button
                            if comments.count > 3 || commentsContinuation != nil {
                                Button {
                                    showingCommentsSheet = true
                                } label: {
                                    HStack {
                                        Text(String(localized: "videoInfo.viewAllComments"))
                                            .fontWeight(.medium)
                                        Image(systemName: "chevron.right")
                                    }
                                    #if os(macOS)
                                    .font(.body)
                                    #else
                                    .font(.subheadline)
                                    #endif
                                    #if !os(tvOS)
                                    .foregroundStyle(accentColor)
                                    #endif
                                }
                                .buttonStyle(.plain)
                                #if os(tvOS)
                                .padding(.vertical, 16)
                                #else
                                .padding(.top, 12)
                                #endif
                            }
                        }
                    }
                }
            }
        }
    }

    // MARK: - Comments Sheet

    @ViewBuilder
    private var commentsSheetContent: some View {
        if let video = displayedVideo {
            NavigationStack {
                ScrollView {
                    LazyVStack(alignment: .leading, spacing: 0) {
                        ForEach(comments) { comment in
                            CommentView(
                                comment: comment,
                                videoID: video.id.videoID,
                                source: video.id.source,
                                isReply: false
                            )
                            .padding(.horizontal)
                            .padding(.vertical, 4)
                            .onAppear {
                                // Infinite scroll: load more when last comment appears
                                if comment.id == comments.last?.id {
                                    loadMoreComments()
                                }
                            }
                        }

                        // Loading indicator at the bottom
                        if commentsState == .loadingMore {
                            HStack {
                                Spacer()
                                ProgressView()
                                Spacer()
                            }
                            .padding()
                        }
                    }
                }
                #if os(tvOS)
                .background(Color.black.ignoresSafeArea())
                #endif
                .navigationTitle(String(localized: "videoInfo.section.comments"))
                #if os(iOS)
                .navigationBarTitleDisplayMode(.inline)
                #endif
                #if !os(tvOS)
                .toolbar {
                    sheetCloseToolbarItem { showingCommentsSheet = false }
                }
                #endif
            }
            #if os(macOS)
            .frame(minWidth: 500, minHeight: 550)
            #endif
        }
    }

    // MARK: - Actions

    private func toggleBookmark() {
        guard let dataManager, let video = displayedVideo else { return }

        if isBookmarked {
            // Already bookmarked - show confirmation to remove
            showingRemoveBookmarkAlert = true
        } else {
            // Not bookmarked - add bookmark
            dataManager.addBookmark(for: video)
            isBookmarked = true
            
            // Fetch newly created bookmark and load its data
            if let bookmark = dataManager.bookmark(for: video.id.videoID) {
                currentBookmark = bookmark
                bookmarkTags = bookmark.tags
                bookmarkNote = bookmark.note ?? ""
            }
        }
    }
    
    private func removeBookmark() {
        guard let dataManager, let video = displayedVideo else { return }
        
        // Cancel any pending save
        bookmarkSaveTask?.cancel()
        
        // Remove bookmark
        dataManager.removeBookmark(for: video.id.videoID)
        isBookmarked = false
        currentBookmark = nil
        bookmarkTags = []
        bookmarkNote = ""
    }
    
    private func debouncedSaveBookmark() {
        // Cancel existing save task
        bookmarkSaveTask?.cancel()
        
        // Create new debounced save task
        bookmarkSaveTask = Task {
            // Wait 1 second before saving
            try? await Task.sleep(for: .seconds(1))
            
            // Check if task was cancelled
            guard !Task.isCancelled else { return }
            
            // Save bookmark
            await MainActor.run {
                saveBookmark()
            }
        }
    }
    
    private func saveBookmark() {
        guard let dataManager, let video = displayedVideo else { return }
        
        // Truncate note if too long
        let finalNote = bookmarkNote.trimmingCharacters(in: .whitespacesAndNewlines)
        let truncatedNote = String(finalNote.prefix(1000))
        
        // Update bookmark
        dataManager.updateBookmark(
            videoID: video.id.videoID,
            tags: bookmarkTags,
            note: truncatedNote.isEmpty ? nil : truncatedNote
        )
        
        // Refresh bookmark data
        currentBookmark = dataManager.bookmark(for: video.id.videoID)
    }

    private func loadComments() {
        // Don't load comments for non-YouTube sources
        guard supportsComments else { return }
        guard commentsState == .idle else { return }
        guard let video = displayedVideo,
              let contentService, let instancesManager,
              let instance = instancesManager.instance(for: video) else {
            commentsState = .error
            return
        }

        commentsState = .loading

        Task {
            do {
                let page = try await contentService.comments(
                    videoID: video.id.videoID,
                    instance: instance,
                    continuation: nil
                )
                await MainActor.run {
                    comments = page.comments
                    commentsContinuation = page.continuation
                    commentsState = .loaded
                }
            } catch let error as APIError where error == .commentsDisabled {
                await MainActor.run {
                    commentsState = .disabled
                }
            } catch {
                await MainActor.run {
                    commentsState = .error
                }
            }
        }
    }

    private func loadMoreComments() {
        guard commentsState != .loadingMore else { return }
        guard let continuation = commentsContinuation else { return }
        guard let video = displayedVideo,
              let contentService, let instancesManager,
              let instance = instancesManager.instance(for: video) else { return }

        commentsState = .loadingMore

        Task {
            do {
                let page = try await contentService.comments(
                    videoID: video.id.videoID,
                    instance: instance,
                    continuation: continuation
                )
                await MainActor.run {
                    comments.append(contentsOf: page.comments)
                    commentsContinuation = page.continuation
                    commentsState = .loaded
                }
            } catch {
                await MainActor.run {
                    commentsState = .loaded // Don't show error on load more failure
                }
            }
        }
    }
    
    // MARK: - Video Details Loading
    
    /// Load full video details from the API (fails silently)
    @MainActor
    private func loadVideoDetails() async {
        guard let base = baseVideo else {
            isLoadingVideoDetails = false
            return
        }
        let videoID = base.id.videoID

        // Skip if already loaded
        guard loadedVideoDetails[videoID] == nil else {
            isLoadingVideoDetails = false
            return
        }

        // Check the video source type for extracted content
        if case .extracted(let extractor, let originalURL) = base.id.source {
            // Skip API fetch for local media sources (SMB, WebDAV, local files)
            if extractor == MediaFile.smbProvider
                || extractor == MediaFile.webdavProvider
                || extractor == MediaFile.localFolderProvider {
                isLoadingVideoDetails = false
                return
            }

            // For other extracted content (Bilibili, etc.), use the extract endpoint
            guard let contentService,
                  let instancesManager,
                  let instance = instancesManager.yatteeServerInstances.first else {
                isLoadingVideoDetails = false
                return
            }

            isLoadingVideoDetails = true
            do {
                // Use extractURL method - just use the video part
                let (fullVideo, _, _) = try await contentService.extractURL(originalURL, instance: instance)
                await prefetchNewThumbnailIfNeeded(old: base, new: fullVideo)
                invalidateStaleThumbnails(old: base, new: fullVideo)
                loadedVideoDetails[videoID] = fullVideo
                CachedChannelData.cacheAuthor(fullVideo.author)
            } catch {
                // Fail silently - use partial video data we have
            }
            isLoadingVideoDetails = false
            return
        }

        // For YouTube/global content, use existing video endpoint
        guard let contentService,
              let instancesManager,
              let instance = instancesManager.instance(for: base) else {
            isLoadingVideoDetails = false
            return
        }

        isLoadingVideoDetails = true

        do {
            let fullVideo = try await contentService.video(
                id: videoID,
                instance: instance
            )
            await prefetchNewThumbnailIfNeeded(old: base, new: fullVideo)
            invalidateStaleThumbnails(old: base, new: fullVideo)
            loadedVideoDetails[videoID] = fullVideo
            CachedChannelData.cacheAuthor(fullVideo.author)
        } catch {
            // Fail silently - just use the partial video data we have
        }

        isLoadingVideoDetails = false
    }

    /// Warms the Nuke cache for the freshly fetched thumbnail *before* we
    /// swap it into the view. Without this, the view observes a URL change,
    /// flips `LazyImage` to the new URL, and briefly shows a placeholder
    /// while the download happens — the "thumbnail flash". By the time the
    /// swap lands, the image is already cached.
    @MainActor
    private func prefetchNewThumbnailIfNeeded(old: Video, new: Video) async {
        guard let newURL = new.bestThumbnail?.url else { return }
        let oldKey = old.bestThumbnail.map { ImageLoadingService.cacheKey(for: $0.url) }
        let newKey = ImageLoadingService.cacheKey(for: newURL)
        guard oldKey != newKey else { return }
        await ImageLoadingService.shared.prefetchImage(for: newURL)
    }

    /// Evicts image cache entries for thumbnails whose URLs changed between
    /// the queued/list copy of the video and the freshly fetched details, so
    /// other views holding the old URLs re-fetch instead of reusing broken
    /// payloads from Nuke's cache.
    @MainActor
    private func invalidateStaleThumbnails(old: Video, new: Video) {
        // Compare by normalized cache key so URLs that only differ in a
        // rotating signing `token` aren't considered stale and evicted.
        let newKeys = Set(new.thumbnails.map { ImageLoadingService.cacheKey(for: $0.url) })
        for thumb in old.thumbnails where !newKeys.contains(ImageLoadingService.cacheKey(for: thumb.url)) {
            ImageLoadingService.shared.removeCachedImage(for: thumb.url)
        }
    }
    
    /// Load initial video from API (for videoID init mode).
    private func loadInitialVideoIfNeeded() async {
        guard case .videoID(let videoID) = initMode else { return }

        guard let contentService,
              let instancesManager,
              let instance = instancesManager.instance(for: videoID.source) else {
            initialVideoLoadError = String(localized: "error.noInstance")
            return
        }
        
        isLoadingInitialVideo = true
        initialVideoLoadError = nil
        
        do {
            loadedVideo = try await contentService.video(id: videoID.videoID, instance: instance)
            isLoadingInitialVideo = false
            
            // Now that video is loaded, trigger initial data loading
            #if !os(tvOS)
            loadVideoData()
            #else
            if let video = displayedVideo {
                isBookmarked = dataManager?.isBookmarked(videoID: video.id.videoID) ?? false
                watchEntry = dataManager?.watchEntry(for: video.id.videoID)
            }
            loadComments()
            #endif
        } catch {
            initialVideoLoadError = error.localizedDescription
            isLoadingInitialVideo = false
        }
    }
    
    // MARK: - Queue Context Helpers

    /// Play the video, respecting the user's resume action setting for partially watched videos.
    private func playVideo() {
        guard let video = displayedVideo, let env = appEnvironment else { return }

        // Live streams have no fixed timeline, so a resume position is meaningless -
        // never ask, always join at the live edge.
        guard !video.isLive else {
            playVideoWithStartTime(0)
            return
        }

        // Get saved watch progress from database
        let savedProgress = env.dataManager.watchProgress(for: video.id.videoID)
        let videoDuration = video.duration
        // When duration is 0 (not yet loaded), use a large threshold to avoid false "completed" detection
        let completionThreshold = videoDuration > 0 ? videoDuration * 0.9 : Double.greatestFiniteMagnitude
        // Minimum threshold - treat < 5 seconds as "not watched" to avoid asking for very short progress
        let minimumThreshold: TimeInterval = 5
        
        // Only consider resume logic if there's meaningful saved progress (>5s) and video wasn't completed
        if let savedProgress, savedProgress >= minimumThreshold, savedProgress < completionThreshold {
            let resumeActionSetting = env.settingsManager.resumeAction
            
            switch resumeActionSetting {
            case .continueWatching:
                // Use saved progress as start time
                playVideoWithStartTime(savedProgress)
            case .startFromBeginning:
                // Always start from beginning
                playVideoWithStartTime(0)
            case .ask:
                // Show the resume action sheet
                resumeSheetData = ResumeSheetData(video: video, resumeTime: savedProgress)
            }
        } else {
            // No saved progress or video was completed - play from beginning
            playVideoWithStartTime(0)
        }
    }
    
    /// Plays the video with the specified start time.
    private func playVideoWithStartTime(_ time: TimeInterval) {
        guard let video = displayedVideo else { return }

        // Media-browser playback must go through `playFromMediaBrowser` so the
        // queue manager sets up on-demand stream/caption resolution — otherwise
        // Samba/WebDAV files cannot play.
        if let ctx = videoQueueContext,
           let mb = ctx.mediaBrowserPlayback,
           let queueManager = queueManager {
            let playableFiles = mb.allFilesInFolder.filter { $0.isPlayable }
            let index = playableFiles.firstIndex(where: { $0.toVideo().id.videoID == video.id.videoID })
                ?? currentVideoIndex
                ?? ctx.videoIndex
                ?? 0
            queueManager.playFromMediaBrowser(
                files: playableFiles,
                index: index,
                source: mb.source,
                allFilesInFolder: mb.allFilesInFolder
            )
            return
        }

        guard let context = videoQueueContext,
              context.hasQueueInfo,
              let queueManager = queueManager,
              let list = context.videoList else {
            // No queue context - play single video
            if time > 0 {
                playerService?.openVideo(video, startTime: time)
            } else {
                playerService?.openVideo(video)
            }
            return
        }
        
        // Use current index if navigating, otherwise use original context index
        let playIndex = currentVideoIndex ?? context.videoIndex ?? 0
        
        // Play with queue context
        queueManager.playFromList(
            videos: list,
            index: playIndex,
            queueSource: context.queueSource,
            sourceLabel: context.sourceLabel,
            startTime: time
        )
    }
    
    // MARK: - Video Navigation
    
    #if !os(tvOS)
    /// Whether we can navigate to the previous video
    private var canNavigatePrevious: Bool {
        guard let index = currentVideoIndex else { return false }
        return index > 0
    }
    
    /// Whether we can navigate to the next video
    private var canNavigateNext: Bool {
        guard let index = currentVideoIndex else {
            return false
        }
        
        // Check if we can navigate within loaded videos
        if let videos = allVideos, index < videos.count - 1 {
            return true
        }
        
        // Check if we can load more videos
        return videoQueueContext?.canLoadMore == true
    }
    
    /// Whether we should pre-load more videos (at 95% of current list)
    private var shouldPreloadMore: Bool {
        guard let videos = allVideos,
              let index = currentVideoIndex,
              videoQueueContext?.canLoadMore == true,
              !isLoadingMoreVideos,
              loadMoreError == nil else {
            return false
        }
        
        let threshold = Int(Double(videos.count) * 0.95)
        return index >= threshold
    }
    
    /// Navigate to the previous video in the queue
    private func navigateToPrevious() {
        guard canNavigatePrevious, let index = currentVideoIndex else { return }
        currentVideoIndex = index - 1
    }
    
    /// Navigate to the next video in the queue
    private func navigateToNext() {
        guard let index = currentVideoIndex else {
            return
        }
        
        // Clear any previous errors when navigating
        loadMoreError = nil
        
        // If we're at the last loaded video and can load more, trigger loading
        if let videos = allVideos,
           index == videos.count - 1,
           videoQueueContext?.canLoadMore == true {
            Task {
                await loadMoreVideos()
                // After loading, navigate to next video if available
                if let newVideos = allVideos, index < newVideos.count - 1 {
                    await MainActor.run {
                        currentVideoIndex = index + 1
                    }
                }
            }
        } else if canNavigateNext {
            // Normal navigation within loaded videos
            currentVideoIndex = index + 1
        }
    }
    
    /// Load more videos via continuation callback
    @MainActor
    private func loadMoreVideos() async {
        guard !isLoadingMoreVideos,
              let callback = videoQueueContext?.loadMoreVideos else {
            return
        }
        
        isLoadingMoreVideos = true
        loadMoreError = nil
        
        do {
            let (newVideos, _) = try await callback()
            
            // Limit extended list to 500 videos to prevent memory issues
            let remainingCapacity = max(0, 500 - extendedVideoList.count)
            let videosToAdd = Array(newVideos.prefix(remainingCapacity))
            
            extendedVideoList.append(contentsOf: videosToAdd)
        } catch {
            loadMoreError = error.localizedDescription
        }
        
        isLoadingMoreVideos = false
    }
    
    /// Load video-specific data (bookmark, watch history, comments, etc.)
    private func loadVideoData() {
        guard let video = displayedVideo else { return }
        
        isBookmarked = dataManager?.isBookmarked(videoID: video.id.videoID) ?? false
        
        // Load bookmark details if bookmarked
        if isBookmarked, let bookmark = dataManager?.bookmark(for: video.id.videoID) {
            currentBookmark = bookmark
            bookmarkTags = bookmark.tags
            bookmarkNote = bookmark.note ?? ""
        } else {
            currentBookmark = nil
            bookmarkTags = []
            bookmarkNote = ""
        }
        
        watchEntry = dataManager?.watchEntry(for: video.id.videoID)
        #if !os(tvOS)
        download = downloadManager?.download(for: video.id)
        #endif
        loadComments()
    }
    #endif
}

// MARK: - Collapsible Section

/// A collapsible section with animated expand/collapse behavior.
private struct CollapsibleSection<Content: View>: View {
    let title: String
    @Binding var isExpanded: Bool
    @ViewBuilder let content: () -> Content

    var body: some View {
        VStack(alignment: .leading, spacing: 0) {
            // Header button
            Button {
                withAnimation(.easeInOut(duration: 0.25)) {
                    isExpanded.toggle()
                }
            } label: {
                HStack {
                    Text(title)
                        .font(.headline)

                    Spacer()

                    Image(systemName: "chevron.right")
                        .font(.subheadline)
                        .fontWeight(.semibold)
                        .foregroundStyle(.secondary)
                        .rotationEffect(.degrees(isExpanded ? 90 : 0))
                }
                .contentShape(Rectangle())
            }
            .buttonStyle(.plain)
            .padding()

            // Content
            if isExpanded {
                content()
                    .padding(.horizontal)
                    .padding(.bottom)
                    .transition(.opacity)
            }
        }
    }
}

// MARK: - Scroll Offset Modifier

private struct VideoInfoScrollOffsetModifier: ViewModifier {
    @Binding var scrollOffset: CGFloat

    func body(content: Content) -> some View {
        content
            .onScrollGeometryChange(for: CGFloat.self) { geometry in
                geometry.contentOffset.y
            } action: { _, newValue in
                if scrollOffset != newValue {
                    scrollOffset = newValue
                }
            }
    }
}

// MARK: - Preview

#Preview {
    NavigationStack {
        VideoInfoView(video: .preview)
            .videoQueueContext(.init(video: .preview, queueSource: .manual, sourceLabel: "Manual", videoList: [.preview, .livePreview], videoIndex: 0, startTime: 0, loadMoreVideos: .none))
    }
}
