//
//  Download.swift
//  Yattee
//
//  Represents a video download.
//

import Foundation

/// Represents a video download.
struct Download: Identifiable, Codable, Sendable, Equatable {
    let id: UUID
    let videoID: VideoID
    let title: String
    let channelName: String
    let channelID: String
    let channelThumbnailURL: URL?
    let channelSubscriberCount: Int?
    /// Channel/author URL for external sources (e.g., bilibili channel page)
    let channelURL: URL?
    let thumbnailURL: URL?
    let duration: TimeInterval
    let description: String?
    let viewCount: Int?
    let likeCount: Int?
    let dislikeCount: Int?
    let publishedAt: Date?
    let publishedText: String?
    let streamURL: URL

    /// Optional separate audio stream URL (for video-only streams)
    let audioStreamURL: URL?
    /// Optional caption URL for subtitle download
    let captionURL: URL?
    /// Language code for the audio track (e.g., "en", "ja")
    let audioLanguage: String?
    /// Language code for the caption (e.g., "en", "de")
    let captionLanguage: String?
    /// Custom HTTP headers required for downloading (cookies, referer, etc.)
    let httpHeaders: [String: String]?
    /// Storyboard metadata for offline seek preview
    let storyboard: Storyboard?

    var status: DownloadStatus
    var progress: Double
    var totalBytes: Int64
    var downloadedBytes: Int64
    var quality: String
    var formatID: String
    var localVideoPath: String?
    var localAudioPath: String?
    /// Path to downloaded caption file
    var localCaptionPath: String?
    /// Directory containing downloaded storyboard sprite sheets
    var localStoryboardPath: String?
    /// Path to downloaded video thumbnail for offline Now Playing artwork
    var localThumbnailPath: String?
    /// Path to downloaded channel thumbnail for offline display
    var localChannelThumbnailPath: String?
    var startedAt: Date?
    var completedAt: Date?
    var error: String?
    var priority: DownloadPriority
    var autoDelete: Bool
    var resumeData: Data?
    /// Resume data for audio download
    var audioResumeData: Data?
    var retryCount: Int
    /// Non-fatal warnings during download (e.g., "Subtitles failed to download")
    var warnings: [String]

    /// Current phase of multi-file download
    var downloadPhase: DownloadPhase
    /// Progress for video file (0.0 to 1.0)
    var videoProgress: Double
    /// Progress for audio file (0.0 to 1.0)
    var audioProgress: Double
    /// Total bytes for video file
    var videoTotalBytes: Int64
    /// Total bytes for audio file
    var audioTotalBytes: Int64

    /// Video codec (e.g., "avc1", "vp9", "av01")
    let videoCodec: String?
    /// Audio codec (e.g., "mp4a", "opus")
    let audioCodec: String?
    /// Video bitrate in bits per second
    let videoBitrate: Int?
    /// Audio bitrate in bits per second
    let audioBitrate: Int?

    /// Current download speed in bytes per second (not persisted)
    var downloadSpeed: Int64 = 0
    /// Last time bytes were recorded for speed calculation (not persisted)
    var lastSpeedUpdateTime: Date?
    /// Last bytes count for speed calculation (not persisted)
    var lastSpeedBytes: Int64 = 0

    /// Per-stream speed tracking (not persisted)
    var videoDownloadSpeed: Int64 = 0
    var audioDownloadSpeed: Int64 = 0
    var captionDownloadSpeed: Int64 = 0
    /// Caption progress (0.0 to 1.0)
    var captionProgress: Double = 0
    /// Total bytes for caption file
    var captionTotalBytes: Int64 = 0
    /// Storyboard download speed (not persisted)
    var storyboardDownloadSpeed: Int64 = 0
    /// Storyboard progress (0.0 to 1.0)
    var storyboardProgress: Double = 0
    /// Total bytes for all storyboard files
    var storyboardTotalBytes: Int64 = 0

    /// Bytes downloaded for video (for indeterminate progress display, not persisted)
    var videoDownloadedBytes: Int64 = 0
    /// Bytes downloaded for audio (for indeterminate progress display, not persisted)
    var audioDownloadedBytes: Int64 = 0
    /// Bytes downloaded for caption (for indeterminate progress display, not persisted)
    var captionDownloadedBytes: Int64 = 0

    // MARK: - Size Unknown Detection

    /// Whether video stream size is unknown (server didn't provide Content-Length)
    var videoSizeUnknown: Bool {
        videoTotalBytes <= 0 && videoProgress < 1.0
    }

    /// Whether audio stream size is unknown
    var audioSizeUnknown: Bool {
        audioStreamURL != nil && audioTotalBytes <= 0 && audioProgress < 1.0
    }

    /// Whether caption stream size is unknown
    var captionSizeUnknown: Bool {
        captionURL != nil && captionTotalBytes <= 0 && captionProgress < 1.0
    }

    /// Whether any active stream has unknown size (for overall indeterminate display)
    var hasIndeterminateProgress: Bool {
        let videoIndeterminate = videoProgress < 1.0 && videoTotalBytes <= 0
        let audioIndeterminate = audioStreamURL != nil && audioProgress < 1.0 && audioTotalBytes <= 0
        return videoIndeterminate || audioIndeterminate
    }

    // Don't persist speed-related fields
    enum CodingKeys: String, CodingKey {
        case id, videoID, title, channelName, channelID, channelThumbnailURL, channelSubscriberCount, channelURL
        case thumbnailURL, duration, description, viewCount, likeCount, dislikeCount
        case publishedAt, publishedText, streamURL, audioStreamURL, captionURL
        case audioLanguage, captionLanguage, httpHeaders, storyboard, status, progress, totalBytes, downloadedBytes
        case quality, formatID, localVideoPath, localAudioPath, localCaptionPath, localStoryboardPath, localThumbnailPath, localChannelThumbnailPath
        case startedAt, completedAt, error, priority, autoDelete, resumeData, audioResumeData
        case retryCount, warnings, downloadPhase, videoProgress, audioProgress, videoTotalBytes, audioTotalBytes
        case videoCodec, audioCodec, videoBitrate, audioBitrate
        case storyboardProgress, storyboardTotalBytes
    }

    // Custom decoder for backwards compatibility with downloads saved before 'warnings' was added
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        id = try container.decode(UUID.self, forKey: .id)
        videoID = try container.decode(VideoID.self, forKey: .videoID)
        title = try container.decode(String.self, forKey: .title)
        channelName = try container.decode(String.self, forKey: .channelName)
        channelID = try container.decode(String.self, forKey: .channelID)
        channelThumbnailURL = try container.decodeIfPresent(URL.self, forKey: .channelThumbnailURL)
        channelSubscriberCount = try container.decodeIfPresent(Int.self, forKey: .channelSubscriberCount)
        channelURL = try container.decodeIfPresent(URL.self, forKey: .channelURL)
        thumbnailURL = try container.decodeIfPresent(URL.self, forKey: .thumbnailURL)
        duration = try container.decode(TimeInterval.self, forKey: .duration)
        description = try container.decodeIfPresent(String.self, forKey: .description)
        viewCount = try container.decodeIfPresent(Int.self, forKey: .viewCount)
        likeCount = try container.decodeIfPresent(Int.self, forKey: .likeCount)
        dislikeCount = try container.decodeIfPresent(Int.self, forKey: .dislikeCount)
        publishedAt = try container.decodeIfPresent(Date.self, forKey: .publishedAt)
        publishedText = try container.decodeIfPresent(String.self, forKey: .publishedText)
        streamURL = try container.decode(URL.self, forKey: .streamURL)
        audioStreamURL = try container.decodeIfPresent(URL.self, forKey: .audioStreamURL)
        captionURL = try container.decodeIfPresent(URL.self, forKey: .captionURL)
        audioLanguage = try container.decodeIfPresent(String.self, forKey: .audioLanguage)
        captionLanguage = try container.decodeIfPresent(String.self, forKey: .captionLanguage)
        httpHeaders = try container.decodeIfPresent([String: String].self, forKey: .httpHeaders)
        storyboard = try container.decodeIfPresent(Storyboard.self, forKey: .storyboard)

        status = try container.decode(DownloadStatus.self, forKey: .status)
        progress = try container.decode(Double.self, forKey: .progress)
        totalBytes = try container.decode(Int64.self, forKey: .totalBytes)
        downloadedBytes = try container.decode(Int64.self, forKey: .downloadedBytes)
        quality = try container.decode(String.self, forKey: .quality)
        formatID = try container.decode(String.self, forKey: .formatID)
        localVideoPath = try container.decodeIfPresent(String.self, forKey: .localVideoPath)
        localAudioPath = try container.decodeIfPresent(String.self, forKey: .localAudioPath)
        localCaptionPath = try container.decodeIfPresent(String.self, forKey: .localCaptionPath)
        localStoryboardPath = try container.decodeIfPresent(String.self, forKey: .localStoryboardPath)
        localThumbnailPath = try container.decodeIfPresent(String.self, forKey: .localThumbnailPath)
        localChannelThumbnailPath = try container.decodeIfPresent(String.self, forKey: .localChannelThumbnailPath)
        startedAt = try container.decodeIfPresent(Date.self, forKey: .startedAt)
        completedAt = try container.decodeIfPresent(Date.self, forKey: .completedAt)
        error = try container.decodeIfPresent(String.self, forKey: .error)
        priority = try container.decode(DownloadPriority.self, forKey: .priority)
        autoDelete = try container.decode(Bool.self, forKey: .autoDelete)
        resumeData = try container.decodeIfPresent(Data.self, forKey: .resumeData)
        audioResumeData = try container.decodeIfPresent(Data.self, forKey: .audioResumeData)
        retryCount = try container.decode(Int.self, forKey: .retryCount)

        // Backwards compatibility: 'warnings' was added later, default to empty array
        warnings = try container.decodeIfPresent([String].self, forKey: .warnings) ?? []

        downloadPhase = try container.decode(DownloadPhase.self, forKey: .downloadPhase)
        videoProgress = try container.decode(Double.self, forKey: .videoProgress)
        audioProgress = try container.decode(Double.self, forKey: .audioProgress)
        videoTotalBytes = try container.decode(Int64.self, forKey: .videoTotalBytes)
        audioTotalBytes = try container.decode(Int64.self, forKey: .audioTotalBytes)

        videoCodec = try container.decodeIfPresent(String.self, forKey: .videoCodec)
        audioCodec = try container.decodeIfPresent(String.self, forKey: .audioCodec)
        videoBitrate = try container.decodeIfPresent(Int.self, forKey: .videoBitrate)
        audioBitrate = try container.decodeIfPresent(Int.self, forKey: .audioBitrate)

        // Backwards compatibility: storyboard progress fields were added later
        storyboardProgress = try container.decodeIfPresent(Double.self, forKey: .storyboardProgress) ?? 0
        storyboardTotalBytes = try container.decodeIfPresent(Int64.self, forKey: .storyboardTotalBytes) ?? 0
    }

    init(
        video: Video,
        quality: String,
        formatID: String,
        streamURL: URL,
        audioStreamURL: URL? = nil,
        captionURL: URL? = nil,
        audioLanguage: String? = nil,
        captionLanguage: String? = nil,
        httpHeaders: [String: String]? = nil,
        storyboard: Storyboard? = nil,
        dislikeCount: Int? = nil,
        priority: DownloadPriority = .normal,
        autoDelete: Bool = false,
        videoCodec: String? = nil,
        audioCodec: String? = nil,
        videoBitrate: Int? = nil,
        audioBitrate: Int? = nil
    ) {
        self.id = UUID()
        self.videoID = video.id
        self.title = video.title
        self.channelName = video.author.name
        self.channelID = video.author.id
        self.channelThumbnailURL = video.author.thumbnailURL
        self.channelSubscriberCount = video.author.subscriberCount
        self.channelURL = video.author.url
        self.thumbnailURL = video.bestThumbnail?.url
        self.duration = video.duration
        self.description = video.description
        self.viewCount = video.viewCount
        self.likeCount = video.likeCount
        self.dislikeCount = dislikeCount
        self.publishedAt = video.publishedAt
        self.publishedText = video.publishedText
        self.streamURL = streamURL
        self.audioStreamURL = audioStreamURL
        self.captionURL = captionURL
        self.audioLanguage = audioLanguage
        self.captionLanguage = captionLanguage
        self.httpHeaders = httpHeaders
        self.storyboard = storyboard

        self.status = .queued
        self.progress = 0
        self.totalBytes = 0
        self.downloadedBytes = 0
        self.quality = quality
        self.formatID = formatID
        self.localVideoPath = nil
        self.localAudioPath = nil
        self.localCaptionPath = nil
        self.localStoryboardPath = nil
        self.localThumbnailPath = nil
        self.localChannelThumbnailPath = nil
        self.startedAt = nil
        self.completedAt = nil
        self.error = nil
        self.priority = priority
        self.autoDelete = autoDelete
        self.resumeData = nil
        self.audioResumeData = nil
        self.retryCount = 0
        self.warnings = []
        self.downloadPhase = .video
        self.videoProgress = 0
        self.audioProgress = 0
        self.videoTotalBytes = 0
        self.audioTotalBytes = 0
        self.videoCodec = videoCodec
        self.audioCodec = audioCodec
        self.videoBitrate = videoBitrate
        self.audioBitrate = audioBitrate
    }

    /// Resolves the local thumbnail file URL, if the thumbnail has been
    /// downloaded to disk. Remote `thumbnailURL`s for proxied sources
    /// (Invidious / Piped / Yattee server) expire, so prefer the local copy
    /// whenever it is available.
    func localThumbnailURL(in downloadsDirectory: URL) -> URL? {
        guard let localThumbnailPath else { return nil }
        return downloadsDirectory.appendingPathComponent(localThumbnailPath)
    }

    /// Convert to a Video model for display purposes.
    /// - Parameter downloadsDirectory: When provided and a local thumbnail
    ///   exists on disk, the resulting `Video` uses a `file://` URL pointing
    ///   at the local thumbnail instead of the (potentially expired) remote
    ///   `thumbnailURL` captured at download time.
    func toVideo(downloadsDirectory: URL? = nil) -> Video {
        let resolvedThumbnailURL: URL? = {
            if let downloadsDirectory, let localURL = localThumbnailURL(in: downloadsDirectory) {
                return localURL
            }
            return thumbnailURL
        }()

        return Video(
            id: videoID,
            title: title,
            description: description,
            author: Author(
                id: channelID,
                name: channelName,
                thumbnailURL: channelThumbnailURL,
                subscriberCount: channelSubscriberCount,
                url: channelURL
            ),
            duration: duration,
            publishedAt: publishedAt,
            publishedText: publishedText,
            viewCount: viewCount,
            likeCount: likeCount,
            thumbnails: Thumbnail.fallbackChain(for: resolvedThumbnailURL),
            isLive: false,
            isUpcoming: false,
            scheduledStartTime: nil
        )
    }
}

// MARK: - Preview Data

extension Download {
    /// A sample completed download for SwiftUI previews.
    static var preview: Download {
        var download = Download(
            video: .preview,
            quality: "1080p",
            formatID: "137",
            streamURL: URL(string: "https://example.com/video.mp4")!,
            audioStreamURL: URL(string: "https://example.com/audio.m4a")!,
            captionURL: URL(string: "https://example.com/captions.vtt")!,
            audioLanguage: "en",
            captionLanguage: "en",
            videoCodec: "avc1",
            audioCodec: "mp4a",
            videoBitrate: 5_000_000,
            audioBitrate: 128_000
        )
        download.status = .completed
        download.progress = 1.0
        download.videoProgress = 1.0
        download.audioProgress = 1.0
        download.videoTotalBytes = 150_000_000
        download.audioTotalBytes = 5_000_000
        download.totalBytes = 155_000_000
        download.downloadedBytes = 155_000_000
        download.localVideoPath = "/Downloads/video.mp4"
        download.localAudioPath = "/Downloads/audio.m4a"
        download.localCaptionPath = "/Downloads/captions.vtt"
        download.completedAt = Date()
        return download
    }

    /// A sample muxed download (no separate audio) for SwiftUI previews.
    static var muxedPreview: Download {
        var download = Download(
            video: .preview,
            quality: "720p",
            formatID: "22",
            streamURL: URL(string: "https://example.com/muxed.mp4")!,
            videoCodec: "avc1",
            audioCodec: "mp4a",
            videoBitrate: 2_500_000,
            audioBitrate: 128_000
        )
        download.status = .completed
        download.progress = 1.0
        download.videoProgress = 1.0
        download.videoTotalBytes = 80_000_000
        download.totalBytes = 80_000_000
        download.downloadedBytes = 80_000_000
        download.localVideoPath = "/Downloads/muxed.mp4"
        download.completedAt = Date()
        return download
    }
}
