//
//  OpenLinkSheet.swift
//  Yattee
//
//  Sheet for entering URLs to play or download via Yattee Server's yt-dlp extraction.
//  Supports multiple URLs (one per line) with batch processing.
//

import SwiftUI
#if canImport(UIKit)
import UIKit
#elseif canImport(AppKit)
import AppKit
#endif

// MARK: - Extraction Item Model

/// Tracks extraction status for each URL in batch processing.
private struct ExtractedItem: Identifiable {
    let id = UUID()
    let url: URL
    var displayHost: String { url.host ?? url.absoluteString }
    var status: ExtractionStatus = .pending
    var video: Video?
    var streams: [Stream] = []
    var captions: [Caption] = []
    var storyboards: [Storyboard] = []
}

private enum ExtractionStatus {
    case pending
    case extracting
    case success
    case failed(String)
}

// MARK: - OpenLinkSheet

/// Sheet for entering URLs to play or download from external sites.
/// Supports multiple URLs (one per line, max 20).
struct OpenLinkSheet: View {
    @Environment(\.dismiss) private var dismiss
    let prefilledURL: URL?

    init(prefilledURL: URL? = nil) {
        self.prefilledURL = prefilledURL
    }

    var body: some View {
        #if os(macOS)
        OpenLinkFormView(prefilledURL: prefilledURL, onRequestDismiss: { dismiss() })
            .frame(minWidth: 520, minHeight: 440)
        #else
        NavigationStack {
            OpenLinkFormView(prefilledURL: prefilledURL, onRequestDismiss: { dismiss() })
                .toolbar {
                    ToolbarItem(placement: .cancellationAction) {
                        Button(String(localized: "common.cancel")) {
                            dismiss()
                        }
                    }
                }
        }
        #endif
    }
}

// MARK: - OpenLinkView

/// Standalone view for entering URLs — used as a tab root (e.g. tvOS sidebar).
/// Not wrapped in a NavigationStack; the containing tab provides one.
struct OpenLinkView: View {
    var body: some View {
        OpenLinkFormView(prefilledURL: nil, onRequestDismiss: nil)
    }
}

// MARK: - OpenLinkFormView

/// Shared form body used by both OpenLinkSheet (sheet) and OpenLinkView (tab root).
struct OpenLinkFormView: View {
    @Environment(\.appEnvironment) private var appEnvironment

    let prefilledURL: URL?
    let onRequestDismiss: (() -> Void)?

    @State private var urlText: String
    @State private var clipboardURLs: [URL] = []
    @FocusState private var isTextEditorFocused: Bool

    // Extraction state
    @State private var isExtracting = false
    @State private var extractedItems: [ExtractedItem] = []
    @State private var hasErrors = false

    // Download flow states
    @State private var showingDownloadSheet = false
    @State private var pendingDownloadItems: [ExtractedItem] = []

    /// Maximum number of URLs allowed.
    fileprivate static let maxURLs = 20

    init(prefilledURL: URL?, onRequestDismiss: (() -> Void)?) {
        self.prefilledURL = prefilledURL
        self.onRequestDismiss = onRequestDismiss
        _urlText = State(initialValue: prefilledURL?.absoluteString ?? "")
    }

    private func dismissIfRequested() {
        onRequestDismiss?()
    }

    var body: some View {
        platformBody
            .onAppear {
                checkClipboard()
                if urlText.isEmpty {
                    isTextEditorFocused = true
                }
            }
            #if !os(tvOS)
            .sheet(isPresented: $showingDownloadSheet, onDismiss: {
                // Close sheet when download sheet is dismissed (if no errors and we're in sheet mode)
                if !hasErrors {
                    dismissIfRequested()
                }
            }) {
                BatchDownloadQualitySheet(videoCount: pendingDownloadItems.count) { quality, includeSubtitles in
                    Task {
                        await downloadPendingItems(quality: quality, includeSubtitles: includeSubtitles)
                    }
                }
            }
            #endif
    }

    @ViewBuilder
    private var platformBody: some View {
        #if os(macOS)
        macOSBody
        #else
        Form {
            urlInputSection
            extractionResultsSection
            actionButtonsSection
            yatteeServerWarningSection
        }
        #if !os(tvOS)
        .navigationTitle(String(localized: "openLink.title"))
        #endif
        #if os(iOS)
        .navigationBarTitleDisplayMode(.inline)
        .scrollDismissesKeyboard(.immediately)
        #endif
        #endif
    }

    #if os(macOS)
    @ViewBuilder
    private var macOSBody: some View {
        VStack(alignment: .leading, spacing: 14) {
            Text(String(localized: "openLink.title"))
                .font(.title3)
                .fontWeight(.semibold)

            VStack(alignment: .leading, spacing: 6) {
                TextEditor(text: $urlText)
                    .font(.system(.body, design: .monospaced))
                    .focused($isTextEditorFocused)
                    .disabled(isExtracting)
                    .scrollContentBackground(.hidden)
                    .padding(6)
                    .frame(minHeight: 90, maxHeight: 140)
                    .background(
                        RoundedRectangle(cornerRadius: 6)
                            .fill(Color(nsColor: .textBackgroundColor))
                    )
                    .overlay(
                        RoundedRectangle(cornerRadius: 6)
                            .strokeBorder(Color.secondary.opacity(0.35), lineWidth: 1)
                    )

                HStack {
                    Text(supportedSitesHint)
                        .font(.caption)
                        .foregroundStyle(.secondary)
                    Spacer()
                    if isTooManyURLs {
                        Label(
                            String(localized: "openLink.tooManyUrls \(Self.maxURLs)"),
                            systemImage: "exclamationmark.triangle"
                        )
                        .foregroundStyle(.orange)
                        .font(.caption)
                    } else if urlCount > 0 {
                        Text(String(localized: "openLink.urlCount \(urlCount)"))
                            .font(.caption)
                            .foregroundStyle(.secondary)
                    }
                }
            }

            if !clipboardURLs.isEmpty, !isExtracting {
                let clipboardText = clipboardURLs.map(\.absoluteString).joined(separator: "\n")
                if clipboardText != urlText {
                    Button {
                        urlText = clipboardText
                    } label: {
                        HStack(spacing: 8) {
                            Image(systemName: "doc.on.clipboard")
                            if clipboardURLs.count > 1 {
                                Text(String(localized: "openLink.pasteMultiple \(clipboardURLs.count)"))
                            } else {
                                Text(String(localized: "openLink.pasteClipboard"))
                            }
                            Text("·")
                                .foregroundStyle(.secondary)
                            Text(clipboardURLs.first?.host ?? "")
                                .foregroundStyle(.secondary)
                                .lineLimit(1)
                                .truncationMode(.middle)
                            Spacer(minLength: 0)
                        }
                        .font(.subheadline)
                    }
                    .buttonStyle(.bordered)
                    .controlSize(.regular)
                }
            }

            if !extractedItems.isEmpty {
                Divider()
                macOSResultsList
            }

            if !hasYatteeServer, hasExternalURLs, !isExtracting {
                HStack(alignment: .top, spacing: 8) {
                    Image(systemName: "exclamationmark.triangle.fill")
                        .foregroundStyle(.yellow)
                    VStack(alignment: .leading, spacing: 2) {
                        Text(String(localized: "openLink.yatteeServerNotConfigured"))
                            .font(.subheadline)
                        Text(String(localized: "openLink.yatteeServerMessage"))
                            .font(.caption)
                            .foregroundStyle(.secondary)
                    }
                }
            }

            Spacer(minLength: 0)

            HStack(spacing: 8) {
                if onRequestDismiss != nil {
                    Button(String(localized: "common.cancel")) {
                        onRequestDismiss?()
                    }
                    .keyboardShortcut(.cancelAction)
                }
                Spacer()
                if isExtracting {
                    ProgressView()
                        .controlSize(.small)
                    Text(String(localized: "openLink.extracting"))
                        .foregroundStyle(.secondary)
                        .font(.subheadline)
                } else {
                    Button {
                        isTextEditorFocused = false
                        Task { await downloadAllURLs() }
                    } label: {
                        Label(
                            isMultipleURLs
                                ? String(localized: "openLink.downloadAll")
                                : String(localized: "openLink.download"),
                            systemImage: "arrow.down.circle"
                        )
                    }
                    .disabled(!isValidInput)

                    Button {
                        isTextEditorFocused = false
                        Task { await openAllURLs() }
                    } label: {
                        Label(
                            isMultipleURLs
                                ? String(localized: "openLink.openAll")
                                : String(localized: "openLink.open"),
                            systemImage: "play.fill"
                        )
                    }
                    .buttonStyle(.borderedProminent)
                    .keyboardShortcut(.defaultAction)
                    .disabled(!isValidInput)
                }
            }
        }
        .padding(20)
    }

    @ViewBuilder
    private var macOSResultsList: some View {
        VStack(alignment: .leading, spacing: 6) {
            if isExtracting {
                let processed = extractedItems.filter { item in
                    if case .pending = item.status { return false }
                    return true
                }.count
                Text(String(localized: "openLink.extractingProgress \(processed) \(extractedItems.count)"))
                    .font(.caption)
                    .foregroundStyle(.secondary)
            } else {
                Text(String(localized: "openLink.results"))
                    .font(.caption)
                    .foregroundStyle(.secondary)
            }

            ScrollView {
                VStack(alignment: .leading, spacing: 4) {
                    ForEach(extractedItems) { item in
                        HStack(spacing: 10) {
                            Group {
                                switch item.status {
                                case .pending:
                                    Image(systemName: "circle").foregroundStyle(.secondary)
                                case .extracting:
                                    ProgressView().controlSize(.small)
                                case .success:
                                    Image(systemName: "checkmark.circle.fill").foregroundStyle(.green)
                                case .failed:
                                    Image(systemName: "xmark.circle.fill").foregroundStyle(.red)
                                }
                            }
                            .frame(width: 18)

                            VStack(alignment: .leading, spacing: 1) {
                                if let video = item.video {
                                    Text(video.title).lineLimit(1)
                                    Text(item.displayHost)
                                        .font(.caption).foregroundStyle(.secondary).lineLimit(1)
                                } else {
                                    Text(item.url.absoluteString)
                                        .font(.caption).foregroundStyle(.secondary).lineLimit(2)
                                }
                                if case .failed(let error) = item.status {
                                    Text(error).font(.caption2).foregroundStyle(.red)
                                }
                            }
                            Spacer()
                        }
                    }
                }
            }
            .frame(maxHeight: 120)
        }
    }
    #endif

    // MARK: - URL Input Section

    @ViewBuilder
    private var urlInputSection: some View {
        Section {
            #if os(tvOS)
            // tvOS doesn't have TextEditor, use TextField for single URL
            TextField(String(localized: "openLink.urlPlaceholder"), text: $urlText)
                .textContentType(.URL)
                .focused($isTextEditorFocused)
                .disabled(isExtracting)
            #else
            TextEditor(text: $urlText)
                .frame(minHeight: 100, maxHeight: 200)
                .font(.system(.body, design: .monospaced))
                #if os(iOS)
                .autocapitalization(.none)
                .keyboardType(.URL)
                #endif
                .focused($isTextEditorFocused)
                .disabled(isExtracting)
            #endif

            #if !os(tvOS)
            // URL count indicator (not shown on tvOS since it only supports single URL)
            HStack {
                if isTooManyURLs {
                    Label(
                        String(localized: "openLink.tooManyUrls \(Self.maxURLs)"),
                        systemImage: "exclamationmark.triangle"
                    )
                    .foregroundStyle(.orange)
                    .font(.caption)
                } else if urlCount > 0 {
                    Text(String(localized: "openLink.urlCount \(urlCount)"))
                        .foregroundStyle(.secondary)
                        .font(.caption)
                }
                Spacer()
            }

            // Clipboard paste button (not available on tvOS)
            if !clipboardURLs.isEmpty, !isExtracting {
                let clipboardText = clipboardURLs.map(\.absoluteString).joined(separator: "\n")
                if clipboardText != urlText {
                    Button {
                        urlText = clipboardText
                    } label: {
                        HStack {
                            Image(systemName: "doc.on.clipboard")
                            VStack(alignment: .leading) {
                                if clipboardURLs.count > 1 {
                                    Text(String(localized: "openLink.pasteMultiple \(clipboardURLs.count)"))
                                        .font(.subheadline)
                                } else {
                                    Text(String(localized: "openLink.pasteClipboard"))
                                        .font(.subheadline)
                                }
                                Text(clipboardURLs.first?.host ?? "")
                                    .font(.caption)
                                    .foregroundStyle(.secondary)
                                    .lineLimit(1)
                            }
                            Spacer()
                        }
                    }
                }
            }
            #endif
        } footer: {
            Text(supportedSitesHint)
        }
    }

    /// Dynamic hint text based on enabled backend instances.
    private var supportedSitesHint: String {
        guard let instancesManager = appEnvironment?.instancesManager else {
            return String(localized: "openLink.hint.noInstances")
        }

        let hasEnabledYatteeServer = !instancesManager.enabledYatteeServerInstances.isEmpty
        let hasEnabledInvidiousPiped = instancesManager.enabledInstances.contains {
            $0.type == .invidious || $0.type == .piped
        }

        if hasEnabledYatteeServer {
            return String(localized: "openLink.hint.yatteeServer")
        } else if hasEnabledInvidiousPiped {
            return String(localized: "openLink.hint.youtubeOnly")
        } else {
            return String(localized: "openLink.hint.noInstances")
        }
    }

    // MARK: - Extraction Results Section

    @ViewBuilder
    private var extractionResultsSection: some View {
        if !extractedItems.isEmpty {
            Section {
                ForEach(extractedItems) { item in
                    HStack(spacing: 12) {
                        // Status indicator
                        Group {
                            switch item.status {
                            case .pending:
                                Image(systemName: "circle")
                                    .foregroundStyle(.secondary)
                            case .extracting:
                                ProgressView()
                                    .controlSize(.small)
                            case .success:
                                Image(systemName: "checkmark.circle.fill")
                                    .foregroundStyle(.green)
                            case .failed:
                                Image(systemName: "xmark.circle.fill")
                                    .foregroundStyle(.red)
                            }
                        }
                        .frame(width: 20)

                        // URL info
                        VStack(alignment: .leading, spacing: 2) {
                            if let video = item.video {
                                Text(video.title)
                                    .lineLimit(1)
                                Text(item.displayHost)
                                    .font(.caption)
                                    .foregroundStyle(.secondary)
                                    .lineLimit(1)
                            } else {
                                // No video extracted - show full URL (useful for failures)
                                Text(item.url.absoluteString)
                                    .font(.caption)
                                    .foregroundStyle(.secondary)
                                    .lineLimit(2)
                            }
                            if case .failed(let error) = item.status {
                                Text(error)
                                    .font(.caption2)
                                    .foregroundStyle(.red)
                            }
                        }

                        Spacer()
                    }
                }
            } header: {
                if isExtracting {
                    let processed = extractedItems.filter { item in
                        switch item.status {
                        case .pending: return false
                        default: return true
                        }
                    }.count
                    Text(String(localized: "openLink.extractingProgress \(processed) \(extractedItems.count)"))
                } else {
                    Text(String(localized: "openLink.results"))
                }
            }
        }
    }

    // MARK: - Action Buttons Section

    @ViewBuilder
    private var actionButtonsSection: some View {
        Section {
            if isExtracting {
                HStack {
                    ProgressView()
                    Text(String(localized: "openLink.extracting"))
                        .foregroundStyle(.secondary)
                }
            } else {
                // Open/Play button
                Button {
                    isTextEditorFocused = false
                    Task { await openAllURLs() }
                } label: {
                    Label(
                        isMultipleURLs
                            ? String(localized: "openLink.openAll")
                            : String(localized: "openLink.open"),
                        systemImage: "play.fill"
                    )
                }
                .disabled(!isValidInput)

                #if !os(tvOS)
                // Download button
                Button {
                    isTextEditorFocused = false
                    Task { await downloadAllURLs() }
                } label: {
                    Label(
                        isMultipleURLs
                            ? String(localized: "openLink.downloadAll")
                            : String(localized: "openLink.download"),
                        systemImage: "arrow.down.circle"
                    )
                }
                .disabled(!isValidInput)
                #endif
            }
        }
    }

    // MARK: - Yattee Server Warning Section

    @ViewBuilder
    private var yatteeServerWarningSection: some View {
        if !hasYatteeServer && hasExternalURLs && !isExtracting {
            Section {
                HStack {
                    Image(systemName: "exclamationmark.triangle.fill")
                        .foregroundStyle(.yellow)
                    Text(String(localized: "openLink.yatteeServerNotConfigured"))
                        .font(.subheadline)
                }
                Text(String(localized: "openLink.yatteeServerMessage"))
                    .font(.caption)
                    .foregroundStyle(.secondary)
            }
        }
    }

    // MARK: - Computed Properties

    private var hasYatteeServer: Bool {
        appEnvironment?.instancesManager.yatteeServerInstance != nil
    }

    /// Parse URLs from input text, one per line.
    private var parsedURLs: [URL] {
        urlText
            .components(separatedBy: .newlines)
            .map { $0.trimmingCharacters(in: .whitespaces) }
            .filter { !$0.isEmpty }
            .compactMap { URL(string: $0) }
            .filter { url in
                guard let scheme = url.scheme?.lowercased() else { return false }
                return (scheme == "http" || scheme == "https") && url.host != nil
            }
            .prefix(Self.maxURLs)
            .map { $0 }
    }

    private var urlCount: Int { parsedURLs.count }
    private var isValidInput: Bool { !parsedURLs.isEmpty }
    private var isMultipleURLs: Bool { urlCount > 1 }

    private var isTooManyURLs: Bool {
        urlText
            .components(separatedBy: .newlines)
            .filter { !$0.trimmingCharacters(in: .whitespaces).isEmpty }
            .count > Self.maxURLs
    }

    /// Whether any of the parsed URLs are external (non-YouTube/PeerTube).
    private var hasExternalURLs: Bool {
        let router = URLRouter()
        return parsedURLs.contains { url in
            if let destination = router.route(url) {
                if case .externalVideo = destination { return true }
                return false
            }
            return true
        }
    }

    // MARK: - Clipboard

    private func checkClipboard() {
        clipboardURLs = []

        #if os(iOS)
        if let string = UIPasteboard.general.string {
            clipboardURLs = parseURLsFromString(string)
        }
        #elseif os(macOS)
        if let string = NSPasteboard.general.string(forType: .string) {
            clipboardURLs = parseURLsFromString(string)
        }
        #endif
    }

    private func parseURLsFromString(_ string: String) -> [URL] {
        string
            .components(separatedBy: .newlines)
            .map { $0.trimmingCharacters(in: .whitespaces) }
            .filter { !$0.isEmpty }
            .compactMap { URL(string: $0) }
            .filter { url in
                guard let scheme = url.scheme?.lowercased() else { return false }
                return (scheme == "http" || scheme == "https") && url.host != nil
            }
            .prefix(Self.maxURLs)
            .map { $0 }
    }

    // MARK: - Open (Play) Action

    private func openAllURLs() async {
        let urls = parsedURLs
        guard !urls.isEmpty, let appEnvironment else { return }

        isExtracting = true
        hasErrors = false
        extractedItems = urls.map { ExtractedItem(url: $0) }

        var successCount = 0
        var failedCount = 0
        var firstVideoPlayed = false

        for (index, url) in urls.enumerated() {
            extractedItems[index].status = .extracting

            do {
                let (video, streams) = try await extractVideo(from: url, appEnvironment: appEnvironment)
                extractedItems[index].status = .success
                extractedItems[index].video = video
                extractedItems[index].streams = streams
                successCount += 1

                if !firstVideoPlayed {
                    // Play first video - this expands player
                    playVideo(video, sourceURL: url, appEnvironment: appEnvironment)
                    firstVideoPlayed = true
                } else {
                    // Add to queue
                    appEnvironment.queueManager.addToQueue(video, queueSource: .manual)
                }
            } catch {
                extractedItems[index].status = .failed(error.localizedDescription)
                failedCount += 1
                hasErrors = true
            }
        }

        isExtracting = false

        // Show completion toast and handle dismissal
        if failedCount == 0 {
            if successCount > 1 {
                appEnvironment.toastManager.showSuccess(
                    String(localized: "openLink.queuedSuccess.title"),
                    subtitle: String(localized: "openLink.queuedSuccess.subtitle \(successCount)")
                )
            }
            dismissIfRequested()
        } else if successCount > 0 {
            appEnvironment.toastManager.show(
                category: .error,
                title: String(localized: "openLink.queuedPartial.title"),
                subtitle: String(localized: "openLink.queuedPartial.subtitle \(successCount) \(failedCount)")
            )
            // Keep sheet open so user can see errors
        } else {
            appEnvironment.toastManager.show(
                category: .error,
                title: String(localized: "openLink.allFailed.title"),
                subtitle: String(localized: "openLink.allFailed.subtitle \(failedCount)")
            )
            // Keep sheet open
        }
    }

    /// Extracts video from URL, routing through appropriate API.
    private func extractVideo(
        from url: URL,
        appEnvironment: AppEnvironment
    ) async throws -> (Video, [Stream]) {
        let router = URLRouter()
        let destination = router.route(url)

        switch destination {
        case .video(let source, _):
            guard case .id(let videoID) = source else {
                throw OpenLinkError.notAVideo
            }
            // YouTube/PeerTube - use content-aware instance selection
            guard let instance = appEnvironment.instancesManager.instance(for: videoID.source) else {
                throw OpenLinkError.noInstanceAvailable
            }
            let (video, streams, _, _) = try await appEnvironment.contentService
                .videoWithProxyStreamsAndCaptionsAndStoryboards(
                    id: videoID.videoID,
                    instance: instance
                )
            return (video, streams)

        case .directMedia(let mediaURL):
            // Direct media URL - no extraction needed
            let video = DirectMediaHelper.createVideo(from: mediaURL)
            let stream = DirectMediaHelper.createStream(from: mediaURL)
            return (video, [stream])

        case .externalVideo, nil:
            // External URL - use Yattee Server
            guard let instance = appEnvironment.instancesManager.yatteeServerInstance else {
                throw OpenLinkError.noYatteeServer
            }
            let (video, streams, _) = try await appEnvironment.contentService
                .extractURL(url, instance: instance)
            return (video, streams)

        default:
            throw OpenLinkError.notAVideo
        }
    }

    private func playVideo(_ video: Video, sourceURL: URL, appEnvironment: AppEnvironment) {
        // Don't pass a specific stream - let the player's selectStreamAndBackend
        // choose the best video+audio combination. Using streams.first would
        // incorrectly select audio-only streams for sites like Bilibili.
        let router = URLRouter()
        let startTime = router.parseTimestamp(router.unwrapped(sourceURL))
        appEnvironment.playerService.openVideo(video, startTime: startTime, forceStartTime: startTime != nil)
    }

    // MARK: - Download Action

    #if !os(tvOS)
    private func downloadAllURLs() async {
        let urls = parsedURLs
        guard !urls.isEmpty, let appEnvironment else { return }

        isExtracting = true
        hasErrors = false
        extractedItems = urls.map { ExtractedItem(url: $0) }

        let downloadSettings = appEnvironment.downloadSettings
        var successCount = 0
        var failedCount = 0

        for (index, url) in urls.enumerated() {
            extractedItems[index].status = .extracting

            do {
                let (video, streams, captions, storyboards) = try await extractVideoFull(from: url, appEnvironment: appEnvironment)
                extractedItems[index].status = .success
                extractedItems[index].video = video
                extractedItems[index].streams = streams
                extractedItems[index].captions = captions
                extractedItems[index].storyboards = storyboards
                successCount += 1

                // If auto-download is configured, enqueue immediately
                if downloadSettings.preferredDownloadQuality != .ask {
                    try await enqueueDownload(
                        video: video,
                        streams: streams,
                        captions: captions,
                        storyboards: storyboards,
                        quality: downloadSettings.preferredDownloadQuality,
                        includeSubtitles: downloadSettings.includeSubtitlesInAutoDownload,
                        appEnvironment: appEnvironment
                    )
                }
            } catch {
                extractedItems[index].status = .failed(error.localizedDescription)
                failedCount += 1
                hasErrors = true
            }
        }

        isExtracting = false

        // Handle completion based on download mode
        if downloadSettings.preferredDownloadQuality == .ask {
            // Show quality picker for all extracted videos
            pendingDownloadItems = extractedItems.filter { $0.video != nil }
            if !pendingDownloadItems.isEmpty {
                showingDownloadSheet = true
            } else if failedCount > 0 {
                appEnvironment.toastManager.show(
                    category: .error,
                    title: String(localized: "openLink.allFailed.title"),
                    subtitle: String(localized: "openLink.allFailed.subtitle \(failedCount)")
                )
            }
        } else {
            // Auto-download mode - show completion toast
            if failedCount == 0 {
                if successCount >= 2 {
                    appEnvironment.toastManager.showSuccess(
                        String(localized: "openLink.downloadQueued.title"),
                        subtitle: String(localized: "openLink.downloadQueued.subtitle \(successCount)")
                    )
                }
                dismissIfRequested()
            } else if successCount > 0 {
                appEnvironment.toastManager.show(
                    category: .error,
                    title: String(localized: "openLink.downloadPartial.title"),
                    subtitle: String(localized: "openLink.downloadPartial.subtitle \(successCount) \(failedCount)")
                )
                // Keep sheet open
            } else {
                appEnvironment.toastManager.show(
                    category: .error,
                    title: String(localized: "openLink.allFailed.title"),
                    subtitle: String(localized: "openLink.allFailed.subtitle \(failedCount)")
                )
                // Keep sheet open
            }
        }
    }

    /// Extracts video with full details (including captions and storyboards) for download.
    private func extractVideoFull(
        from url: URL,
        appEnvironment: AppEnvironment
    ) async throws -> (Video, [Stream], [Caption], [Storyboard]) {
        let router = URLRouter()
        let destination = router.route(url)

        switch destination {
        case .video(let source, _):
            guard case .id(let videoID) = source else {
                throw OpenLinkError.notAVideo
            }
            // YouTube/PeerTube - use content-aware instance selection
            guard let instance = appEnvironment.instancesManager.instance(for: videoID.source) else {
                throw OpenLinkError.noInstanceAvailable
            }
            let (video, streams, captions, storyboards) = try await appEnvironment.contentService
                .videoWithProxyStreamsAndCaptionsAndStoryboards(
                    id: videoID.videoID,
                    instance: instance
                )
            return (video, streams, captions, storyboards)

        case .directMedia(let mediaURL):
            // Direct media URL - no extraction needed, no captions/storyboards
            let video = DirectMediaHelper.createVideo(from: mediaURL)
            let stream = DirectMediaHelper.createStream(from: mediaURL)
            return (video, [stream], [], [])

        case .externalVideo, nil:
            // External URL - use Yattee Server (doesn't support storyboards)
            guard let instance = appEnvironment.instancesManager.yatteeServerInstance else {
                throw OpenLinkError.noYatteeServer
            }
            let (video, streams, captions) = try await appEnvironment.contentService
                .extractURL(url, instance: instance)
            return (video, streams, captions, [])

        default:
            throw OpenLinkError.notAVideo
        }
    }

    /// Downloads pending items after user selects quality.
    private func downloadPendingItems(quality: DownloadQuality, includeSubtitles: Bool) async {
        guard let appEnvironment else { return }

        var successCount = 0
        var failedCount = 0

        for item in pendingDownloadItems {
            guard let video = item.video else { continue }

            do {
                try await enqueueDownload(
                    video: video,
                    streams: item.streams,
                    captions: item.captions,
                    storyboards: item.storyboards,
                    quality: quality,
                    includeSubtitles: includeSubtitles,
                    appEnvironment: appEnvironment
                )
                successCount += 1
            } catch {
                failedCount += 1
            }
        }

        pendingDownloadItems = []

        // Show completion toast
        if failedCount == 0 {
            if successCount >= 2 {
                appEnvironment.toastManager.showSuccess(
                    String(localized: "openLink.downloadQueued.title"),
                    subtitle: String(localized: "openLink.downloadQueued.subtitle \(successCount)")
                )
            }
            if !hasErrors {
                dismissIfRequested()
            }
        } else {
            appEnvironment.toastManager.show(
                category: .error,
                title: String(localized: "openLink.downloadPartial.title"),
                subtitle: String(localized: "openLink.downloadPartial.subtitle \(successCount) \(failedCount)")
            )
        }
    }

    /// Enqueues a single video for download with already-fetched streams.
    private func enqueueDownload(
        video: Video,
        streams: [Stream],
        captions: [Caption],
        storyboards: [Storyboard],
        quality: DownloadQuality,
        includeSubtitles: Bool,
        appEnvironment: AppEnvironment
    ) async throws {
        // Select best video stream
        let videoStream = selectBestVideoStream(from: streams, maxQuality: quality)
        guard let videoStream else {
            throw DownloadError.noStreamAvailable
        }

        // Select audio stream if needed
        var audioStream: Stream?
        if videoStream.isVideoOnly {
            audioStream = selectBestAudioStream(
                from: streams,
                preferredLanguage: appEnvironment.settingsManager.preferredAudioLanguage
            )
        }

        // Select caption if enabled
        var caption: Caption?
        if includeSubtitles, let preferredLang = appEnvironment.settingsManager.preferredSubtitlesLanguage {
            caption = selectBestCaption(from: captions, preferredLanguage: preferredLang)
        }

        let audioCodec = videoStream.isMuxed ? videoStream.audioCodec : audioStream?.audioCodec
        let audioBitrate = videoStream.isMuxed ? nil : audioStream?.bitrate

        try await appEnvironment.downloadManager.enqueue(
            video,
            quality: videoStream.qualityLabel,
            formatID: videoStream.format,
            streamURL: videoStream.url,
            audioStreamURL: videoStream.isVideoOnly ? audioStream?.url : nil,
            captionURL: caption?.url,
            audioLanguage: audioStream?.audioLanguage,
            captionLanguage: caption?.languageCode,
            httpHeaders: videoStream.httpHeaders,
            storyboard: storyboards.highest(),
            dislikeCount: nil,
            videoCodec: videoStream.videoCodec,
            audioCodec: audioCodec,
            videoBitrate: videoStream.bitrate,
            audioBitrate: audioBitrate
        )
    }

    // MARK: - Stream Selection Helpers

    private func selectBestVideoStream(from streams: [Stream], maxQuality: DownloadQuality) -> Stream? {
        let maxRes = maxQuality.maxResolution

        let videoStreams = streams
            .filter { !$0.isAudioOnly && $0.resolution != nil }
            .filter {
                let format = StreamFormat.detect(from: $0)
                return format != .hls && format != .dash
            }
            .sorted { s1, s2 in
                let res1 = s1.resolution ?? .p360
                let res2 = s2.resolution ?? .p360
                if res1 != res2 { return res1 > res2 }
                if s1.isMuxed != s2.isMuxed { return s1.isMuxed }
                return HardwareCapabilities.shared.codecPriority(for: s1.videoCodec) >
                       HardwareCapabilities.shared.codecPriority(for: s2.videoCodec)
            }

        guard let maxRes else {
            return videoStreams.first
        }

        if let stream = videoStreams.first(where: { ($0.resolution ?? .p360) <= maxRes }) {
            return stream
        }

        return videoStreams.last
    }

    private func selectBestAudioStream(from streams: [Stream], preferredLanguage: String?) -> Stream? {
        let audioStreams = streams.filter { $0.isAudioOnly }

        if let preferred = preferredLanguage {
            if let match = audioStreams.first(where: { ($0.audioLanguage ?? "").hasPrefix(preferred) }) {
                return match
            }
        }

        if let original = audioStreams.first(where: { $0.isOriginalAudio }) {
            return original
        }

        return audioStreams.first
    }

    private func selectBestCaption(from captions: [Caption], preferredLanguage: String) -> Caption? {
        if let exact = captions.first(where: { $0.languageCode == preferredLanguage }) {
            return exact
        }
        if let prefix = captions.first(where: {
            $0.languageCode.hasPrefix(preferredLanguage) || $0.baseLanguageCode == preferredLanguage
        }) {
            return prefix
        }
        return nil
    }
    #endif
}

// MARK: - Errors

private enum OpenLinkError: LocalizedError {
    case noInstanceAvailable
    case noYatteeServer
    case notAVideo

    var errorDescription: String? {
        switch self {
        case .noInstanceAvailable:
            return String(localized: "openLink.noInstance")
        case .noYatteeServer:
            return String(localized: "openLink.noYatteeServer")
        case .notAVideo:
            return String(localized: "openLink.notAVideo")
        }
    }
}

// MARK: - Previews

#Preview {
    OpenLinkSheet()
}

#Preview("With URL") {
    OpenLinkSheet(prefilledURL: URL(string: "https://vimeo.com/123456789"))
}

#Preview("Multiple URLs") {
    OpenLinkSheet(prefilledURL: URL(string: "https://youtube.com/watch?v=abc123"))
}
