import Foundation
import SwiftData
import SharedModels
import AVFoundation
import YouTubeKit

/// Downloads YouTube videos for offline viewing (Issue #21, updated for #30).
///
/// Two-path strategy since YouTube phased out high-quality progressive streams:
///   1. **Progressive** — single MP4 with audio+video combined (rare, ≤360p).
///      Download directly to the final path.
///   2. **Adaptive** — DASH: separate video + audio tracks. Download each to
///      temp files, then mux with AVAssetExportSession (`.passthrough` preset,
///      no re-encoding) into the final MP4.
///
/// YouTubeKit returns stream URLs only; combining tracks is the app's job.
/// See the library's README Example 3 for the progressive-only pattern.
///
/// **Known fragility**: YouTubeKit scrapes YouTube's private stream API and
/// will break when YouTube changes their internal format. Streaming (iframe
/// embed elsewhere) keeps working regardless.
@MainActor
@Observable
final class VideoDownloadService {

    // MARK: - Types

    /// Per-download state surfaced to the UI. `progress == nil` means show an
    /// indeterminate spinner (e.g. muxing phase).
    struct DownloadStatus: Equatable, Sendable {
        var progress: Double?
        var label: String
    }

    enum DownloadError: Error, LocalizedError {
        case extractionFailed(String)
        case noSuitableStream
        case downloadFailed(String)
        case muxFailed(String)
        case fileWriteFailed(String)

        var errorDescription: String? {
            switch self {
            case .extractionFailed(let why): "Could not extract video: \(why)"
            case .noSuitableStream: "No downloadable video+audio streams found for this video."
            case .downloadFailed(let why): "Download failed: \(why)"
            case .muxFailed(let why): "Could not combine audio and video: \(why)"
            case .fileWriteFailed(let why): "Could not save video: \(why)"
            }
        }
    }

    /// In-flight downloads by videoId, with a phase label and progress.
    var activeDownloads: [String: DownloadStatus] = [:]

    static let shared = VideoDownloadService()

    // MARK: - Paths

    private static var videosDirectory: URL {
        let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
        return docs.appendingPathComponent("videos", isDirectory: true)
    }

    private static var tempDirectory: URL {
        FileManager.default.temporaryDirectory
    }

    private static func ensureDirectory() throws {
        let url = videosDirectory
        if !FileManager.default.fileExists(atPath: url.path) {
            try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
        }
    }

    static func fileURL(for videoId: String) -> URL {
        videosDirectory.appendingPathComponent("\(videoId).mp4")
    }

    private static func tempURL(videoId: String, kind: String, ext: String) -> URL {
        tempDirectory.appendingPathComponent("\(videoId).\(kind).\(ext)")
    }

    /// True if a downloaded MP4 exists for this videoId.
    static func isDownloaded(videoId: String) -> Bool {
        FileManager.default.fileExists(atPath: fileURL(for: videoId).path)
    }

    // MARK: - Public download entry point

    func download(
        videoId: String,
        title: String,
        into modelContext: ModelContext
    ) async throws {
        guard activeDownloads[videoId] == nil else { return }
        activeDownloads[videoId] = DownloadStatus(progress: 0, label: "Preparing…")

        defer { activeDownloads.removeValue(forKey: videoId) }

        try Self.ensureDirectory()

        let destURL = Self.fileURL(for: videoId)

        // Resolve streams once; decide progressive vs adaptive.
        let plan: DownloadPlan
        do {
            plan = try await Self.resolvePlan(videoId: videoId)
        } catch let e as DownloadError {
            throw e
        } catch {
            throw DownloadError.extractionFailed(error.localizedDescription)
        }

        switch plan {
        case .progressive(let url):
            activeDownloads[videoId] = DownloadStatus(progress: 0, label: "Downloading…")
            try await downloadStream(from: url, to: destURL) { [weak self] p in
                Task { @MainActor [weak self] in
                    self?.activeDownloads[videoId] = DownloadStatus(
                        progress: p,
                        label: "Downloading \(Int(p * 100))%"
                    )
                }
            }

        case .adaptive(let videoURL, let audioURL):
            let tempVideo = Self.tempURL(videoId: videoId, kind: "video", ext: "mp4")
            let tempAudio = Self.tempURL(videoId: videoId, kind: "audio", ext: "m4a")
            defer {
                try? FileManager.default.removeItem(at: tempVideo)
                try? FileManager.default.removeItem(at: tempAudio)
            }

            // Phase 1 of 3: video track (0–55% of overall progress)
            try await downloadStream(from: videoURL, to: tempVideo) { [weak self] p in
                Task { @MainActor [weak self] in
                    self?.activeDownloads[videoId] = DownloadStatus(
                        progress: p * 0.55,
                        label: "Video \(Int(p * 100))%"
                    )
                }
            }

            // Phase 2 of 3: audio track (55–80%)
            try await downloadStream(from: audioURL, to: tempAudio) { [weak self] p in
                Task { @MainActor [weak self] in
                    self?.activeDownloads[videoId] = DownloadStatus(
                        progress: 0.55 + p * 0.25,
                        label: "Audio \(Int(p * 100))%"
                    )
                }
            }

            // Phase 3 of 3: mux (indeterminate)
            activeDownloads[videoId] = DownloadStatus(progress: nil, label: "Finalizing…")
            do {
                try await Self.mux(videoURL: tempVideo, audioURL: tempAudio, to: destURL)
            } catch let e as DownloadError {
                throw e
            } catch {
                throw DownloadError.muxFailed(error.localizedDescription)
            }
        }

        // Record in SwiftData.
        let attrs = try? FileManager.default.attributesOfItem(atPath: destURL.path)
        let byteCount = (attrs?[.size] as? Int) ?? 0
        let entry = DownloadedVideo(
            videoId: videoId,
            title: title,
            filename: "\(videoId).mp4",
            byteCount: byteCount
        )
        modelContext.insert(entry)
        try? modelContext.save()
    }

    /// Deletes the downloaded file and its SwiftData row.
    func delete(videoId: String, modelContext: ModelContext) {
        let url = Self.fileURL(for: videoId)
        try? FileManager.default.removeItem(at: url)

        let descriptor = FetchDescriptor<DownloadedVideo>(
            predicate: #Predicate<DownloadedVideo> { $0.videoId == videoId }
        )
        if let existing = try? modelContext.fetch(descriptor) {
            for entry in existing {
                modelContext.delete(entry)
            }
            try? modelContext.save()
        }
    }

    /// Total bytes used by all downloads.
    static func totalBytesUsed() -> Int {
        let url = videosDirectory
        guard let contents = try? FileManager.default.contentsOfDirectory(
            at: url, includingPropertiesForKeys: [.fileSizeKey]
        ) else { return 0 }
        return contents.reduce(0) { acc, file in
            let size = (try? file.resourceValues(forKeys: [.fileSizeKey]).fileSize) ?? 0
            return acc + size
        }
    }

    // MARK: - Stream resolution

    private enum DownloadPlan {
        case progressive(URL)
        case adaptive(video: URL, audio: URL)
    }

    /// Picks progressive MP4 if available (single download); otherwise the
    /// best adaptive MP4 video + M4A audio pair for later muxing.
    nonisolated private static func resolvePlan(videoId: String) async throws -> DownloadPlan {
        let youtube = YouTube(videoID: videoId)
        let streams = try await youtube.streams

        // 1. Progressive path — uses the library's own recommended filter chain.
        if let progressive = streams
            .filterVideoAndAudio()
            .filter({ $0.fileExtension == .mp4 && $0.isNativelyPlayable })
            .highestResolutionStream() {
            return .progressive(progressive.url)
        }

        // 2. Adaptive path — highest-resolution MP4 video track + highest-bitrate M4A audio.
        let videoStream = streams
            .filterVideoOnly()
            .filter { $0.fileExtension == .mp4 && $0.isNativelyPlayable }
            .highestResolutionStream()
        let audioStream = streams
            .filterAudioOnly()
            .filter {
                ($0.fileExtension == .m4a || $0.fileExtension == .mp4) && $0.isNativelyPlayable
            }
            .highestAudioBitrateStream()

        if let v = videoStream, let a = audioStream {
            return .adaptive(video: v.url, audio: a.url)
        }

        throw DownloadError.noSuitableStream
    }

    // MARK: - Download helper

    nonisolated private func downloadStream(
        from url: URL,
        to dest: URL,
        onProgress: @escaping @Sendable (Double) -> Void
    ) async throws {
        let delegate = DownloadProgressDelegate(onProgress: onProgress)
        do {
            let (tempURL, _) = try await URLSession.shared.download(
                for: URLRequest(url: url),
                delegate: delegate
            )
            if FileManager.default.fileExists(atPath: dest.path) {
                try FileManager.default.removeItem(at: dest)
            }
            try FileManager.default.moveItem(at: tempURL, to: dest)
        } catch {
            throw DownloadError.downloadFailed(error.localizedDescription)
        }
    }

    // MARK: - Muxing

    /// Combines a video-only file and an audio-only file into a single MP4.
    /// Uses `.passthrough` preset — no re-encoding, just container rewrite.
    /// Falls back to `.highestQuality` (which re-encodes) if passthrough
    /// rejects the composition due to codec incompatibility.
    nonisolated private static func mux(
        videoURL: URL,
        audioURL: URL,
        to outputURL: URL
    ) async throws {
        let videoAsset = AVURLAsset(url: videoURL)
        let audioAsset = AVURLAsset(url: audioURL)

        let videoTracks = try await videoAsset.loadTracks(withMediaType: .video)
        let audioTracks = try await audioAsset.loadTracks(withMediaType: .audio)

        guard let srcVideo = videoTracks.first,
              let srcAudio = audioTracks.first else {
            throw DownloadError.muxFailed("Downloaded streams missing expected tracks")
        }

        let composition = AVMutableComposition()
        guard let compVideo = composition.addMutableTrack(
                  withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid
              ),
              let compAudio = composition.addMutableTrack(
                  withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid
              ) else {
            throw DownloadError.muxFailed("Could not create composition tracks")
        }

        let videoDuration = try await videoAsset.load(.duration)
        let audioDuration = try await audioAsset.load(.duration)
        let duration = CMTimeMinimum(videoDuration, audioDuration)
        let range = CMTimeRange(start: .zero, duration: duration)

        try compVideo.insertTimeRange(range, of: srcVideo, at: .zero)
        try compAudio.insertTimeRange(range, of: srcAudio, at: .zero)

        if FileManager.default.fileExists(atPath: outputURL.path) {
            try FileManager.default.removeItem(at: outputURL)
        }

        // Try passthrough (no re-encode) first; fall back to quality export on failure.
        let presets = [AVAssetExportPresetPassthrough, AVAssetExportPresetHighestQuality]
        var lastError: Error?

        for preset in presets {
            guard let exporter = AVAssetExportSession(asset: composition, presetName: preset) else {
                continue
            }
            do {
                try await exporter.export(to: outputURL, as: .mp4)
                return
            } catch {
                lastError = error
                // Clean up partial output before retrying with next preset.
                try? FileManager.default.removeItem(at: outputURL)
                continue
            }
        }

        throw DownloadError.muxFailed(lastError?.localizedDescription ?? "Unknown export failure")
    }
}

// MARK: - URLSession progress delegate

private final class DownloadProgressDelegate: NSObject, URLSessionDownloadDelegate, @unchecked Sendable {
    let onProgress: @Sendable (Double) -> Void

    init(onProgress: @escaping @Sendable (Double) -> Void) {
        self.onProgress = onProgress
    }

    func urlSession(
        _ session: URLSession,
        downloadTask: URLSessionDownloadTask,
        didWriteData bytesWritten: Int64,
        totalBytesWritten: Int64,
        totalBytesExpectedToWrite: Int64
    ) {
        guard totalBytesExpectedToWrite > 0 else { return }
        onProgress(Double(totalBytesWritten) / Double(totalBytesExpectedToWrite))
    }

    func urlSession(
        _ session: URLSession,
        downloadTask: URLSessionDownloadTask,
        didFinishDownloadingTo location: URL
    ) {
        // Not used — `URLSession.download(for:delegate:)` returns the temp URL directly.
    }
}
