//
//  Video.swift
//  Yattee
//
//  Core video model representing a video from any source.
//

@preconcurrency import Foundation

/// Represents a video from any content source.
struct Video: Identifiable, Codable, Sendable {
    /// Unique identifier combining source and video ID.
    let id: VideoID

    /// The video title.
    let title: String

    /// The video description (may be truncated in list views).
    let description: String?

    /// The channel/author information.
    let author: Author

    /// Duration in seconds.
    let duration: TimeInterval

    /// When the video was published.
    let publishedAt: Date?

    /// Human-readable published date from the API.
    let publishedText: String?

    /// View count if available.
    let viewCount: Int?

    /// Like count if available.
    let likeCount: Int?

    /// Available thumbnails.
    let thumbnails: [Thumbnail]

    /// Whether this is a live stream.
    let isLive: Bool

    /// Whether this is an upcoming premiere/stream.
    let isUpcoming: Bool

    /// Scheduled start time for upcoming content.
    let scheduledStartTime: Date?

    /// Related/recommended videos (populated from video details API).
    let relatedVideos: [Video]?

    // MARK: - Explicit Initializer

    init(
        id: VideoID,
        title: String,
        description: String?,
        author: Author,
        duration: TimeInterval,
        publishedAt: Date?,
        publishedText: String?,
        viewCount: Int?,
        likeCount: Int?,
        thumbnails: [Thumbnail],
        isLive: Bool,
        isUpcoming: Bool,
        scheduledStartTime: Date?,
        relatedVideos: [Video]? = nil
    ) {
        self.id = id
        self.title = title
        self.description = description
        self.author = author
        self.duration = duration
        self.publishedAt = publishedAt
        self.publishedText = publishedText
        self.viewCount = viewCount
        self.likeCount = likeCount
        self.thumbnails = thumbnails
        self.isLive = isLive
        self.isUpcoming = isUpcoming
        self.scheduledStartTime = scheduledStartTime
        self.relatedVideos = relatedVideos
    }

    // MARK: - Computed Properties

    var bestThumbnail: Thumbnail? {
        thumbnails.sorted { $0.quality > $1.quality }.first
    }

    /// Thumbnail URLs ordered best-quality-first.
    ///
    /// Used as a fallback chain: backends (and YouTube's CDN) often advertise
    /// `maxres`/`sddefault` variants that don't actually exist for a given video
    /// and 404. Consumers try these in order and drop to the next when one fails.
    var thumbnailURLsByQuality: [URL] {
        thumbnails.sorted { $0.quality > $1.quality }.map(\.url)
    }

    /// Best thumbnail URL rewritten to the always-available `hqdefault` variant.
    ///
    /// For consumers that render or fetch a single URL with no way to fall back
    /// through the quality chain (Now Playing artwork, Top Shelf, remote control,
    /// navigation covers): the best advertised variant is often `maxresdefault`,
    /// which 404s for many older videos.
    var reliableThumbnailURL: URL? {
        Thumbnail.reliableURL(for: bestThumbnail?.url)
    }

    var formattedDuration: String {
        guard !isLive else { return "LIVE" }
        guard duration > 0 else { return "" }

        let hours = Int(duration) / 3600
        let minutes = (Int(duration) % 3600) / 60
        let seconds = Int(duration) % 60

        if hours > 0 {
            return String(format: "%d:%02d:%02d", hours, minutes, seconds)
        } else {
            return String(format: "%d:%02d", minutes, seconds)
        }
    }

    var formattedViewCount: String? {
        guard let viewCount else { return nil }
        return CountFormatter.compact(viewCount)
    }

    /// Formatted published date, preferring parsed Date over API-provided text.
    var formattedPublishedDate: String? {
        if let publishedAt {
            let formatter = RelativeDateTimeFormatter()
            formatter.unitsStyle = .full
            return formatter.localizedString(for: publishedAt, relativeTo: Date())
        }
        return publishedText
    }

    /// URL for sharing this video.
    var shareURL: URL {
        switch id.source {
        case .global(let provider):
            if provider == ContentSource.youtubeProvider {
                return URL(string: "https://www.youtube.com/watch?v=\(id.videoID)")!
            }
            // Future: handle other global providers
            return URL(string: "https://www.youtube.com/watch?v=\(id.videoID)")!
        case .federated(let provider, let instance):
            if provider == ContentSource.peertubeProvider {
                return URL(string: "\(instance.absoluteString)/w/\(id.videoID)")!
            }
            // Future: handle other federated providers
            return instance.appendingPathComponent("videos/\(id.videoID)")
        case .extracted(_, let originalURL):
            return originalURL
        }
    }

    /// The ContentSource for the author's channel.
    /// For federated content, the author may be on a different instance than the video.
    var authorSource: ContentSource {
        switch id.source {
        case .global:
            return id.source
        case .federated(let provider, let videoInstance):
            // For PeerTube, the author might be on a different federated instance
            if let authorInstance = author.instance, authorInstance.host != videoInstance.host {
                return .federated(provider: provider, instance: authorInstance)
            }
            return id.source
        case .extracted:
            return id.source
        }
    }

    /// Whether this video supports comments via the API.
    /// Currently only YouTube videos support comments through Invidious.
    var supportsComments: Bool {
        if case .global(let provider) = id.source {
            return provider == ContentSource.youtubeProvider
        }
        return false
    }

    /// Whether this video supports detailed statistics (views, likes, date) via API.
    /// Extracted videos (yt-dlp sources) don't have API stats available.
    var supportsAPIStats: Bool {
        switch id.source {
        case .global, .federated:
            return true
        case .extracted:
            return false
        }
    }

}

// MARK: - Video ID

/// Unique identifier for a video, combining source and video ID.
struct VideoID: Codable, Hashable, Sendable {
    /// The content source.
    let source: ContentSource

    /// The video ID within that source.
    let videoID: String

    /// For PeerTube, videos can be identified by UUID as well.
    let uuid: String?

    init(source: ContentSource, videoID: String, uuid: String? = nil) {
        self.source = source
        self.videoID = videoID
        self.uuid = uuid
    }

    /// Creates a global video ID (e.g., YouTube).
    static func global(_ videoID: String, provider: String = ContentSource.youtubeProvider) -> VideoID {
        VideoID(source: .global(provider: provider), videoID: videoID)
    }

    /// Creates a federated video ID (e.g., PeerTube).
    static func federated(_ videoID: String, provider: String = ContentSource.peertubeProvider, instance: URL, uuid: String? = nil) -> VideoID {
        VideoID(source: .federated(provider: provider, instance: instance), videoID: videoID, uuid: uuid)
    }

    /// Creates an extracted video ID for sites supported by yt-dlp.
    static func extracted(_ videoID: String, extractor: String, originalURL: URL) -> VideoID {
        VideoID(source: .extracted(extractor: extractor, originalURL: originalURL), videoID: videoID)
    }
}

extension VideoID: Identifiable {
    var id: String {
        switch source {
        case .global(let provider):
            return "global:\(provider):\(videoID)"
        case .federated(let provider, let instance):
            return "federated:\(provider):\(instance.host ?? ""):\(videoID)"
        case .extracted(let extractor, _):
            return "extracted:\(extractor):\(videoID)"
        }
    }

    /// Whether this video is from an SMB media source.
    var isSMBSource: Bool {
        if case .extracted(let extractor, _) = source {
            return extractor == MediaFile.smbProvider
        }
        return false
    }
}

// MARK: - Author

/// Represents a video author/channel.
struct Author: Codable, Hashable, Sendable {
    let id: String
    let name: String
    let thumbnailURL: URL?
    let subscriberCount: Int?

    /// For PeerTube, the instance where this author is hosted.
    let instance: URL?

    /// For external sources, the channel/author URL for navigation.
    let url: URL?

    /// Whether real channel info was returned from the server (vs fallback to extractor name).
    let hasRealChannelInfo: Bool

    init(id: String, name: String, thumbnailURL: URL? = nil, subscriberCount: Int? = nil, instance: URL? = nil, url: URL? = nil, hasRealChannelInfo: Bool = true) {
        self.id = id
        self.name = name
        self.thumbnailURL = thumbnailURL
        self.subscriberCount = subscriberCount
        self.instance = instance
        self.url = url
        self.hasRealChannelInfo = hasRealChannelInfo
    }

    var formattedSubscriberCount: String? {
        guard let subscriberCount else { return nil }
        return CountFormatter.compact(subscriberCount) + " " + String(localized: "channel.subscribers")
    }
}

// MARK: - Thumbnail

/// Represents a video thumbnail.
struct Thumbnail: Codable, Hashable, Sendable {
    let url: URL
    let quality: Quality
    let width: Int?
    let height: Int?

    enum Quality: String, Codable, Comparable, Sendable {
        case `default`
        case medium
        case high
        case standard
        case maxres

        static func < (lhs: Quality, rhs: Quality) -> Bool {
            let order: [Quality] = [.default, .medium, .high, .standard, .maxres]
            guard let lIndex = order.firstIndex(of: lhs),
                  let rIndex = order.firstIndex(of: rhs) else {
                return false
            }
            return lIndex < rIndex
        }
    }

    init(url: URL, quality: Quality = .default, width: Int? = nil, height: Int? = nil) {
        self.url = url
        self.quality = quality
        self.width = width
        self.height = height
    }

    /// Expands a single stored YouTube-style thumbnail URL (`/vi/<id>/<variant>.jpg`)
    /// into a best-first fallback chain. Persistence models (playlists, watch history,
    /// bookmarks) store only the best advertised URL — usually `maxresdefault.jpg`,
    /// which doesn't exist for many older videos and 404s. Reconstructing the
    /// lower-quality variants lets thumbnail views fall back the same way they do
    /// for live API results. Non-matching URLs get a single-entry chain.
    /// Rewrites a YouTube-style thumbnail URL (`/vi/<id>/<variant>.jpg`) to the
    /// `hqdefault.jpg` variant, which exists for effectively every video (unlike
    /// `maxresdefault`/`sddefault`, which 404 for many older uploads). Use where a
    /// single URL is displayed without fallback, e.g. playlist covers. Non-matching
    /// URLs are returned unchanged.
    static func reliableURL(for url: URL?) -> URL? {
        guard let url else { return nil }
        let variants = ["maxresdefault.jpg", "sddefault.jpg", "hqdefault.jpg", "mqdefault.jpg", "default.jpg"]
        let path = url.path
        guard path.range(of: #"/vi/[^/]+/"#, options: .regularExpression) != nil,
              let match = variants.first(where: { path.hasSuffix($0) }),
              var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
            return url
        }
        components.path = String(path.dropLast(match.count)) + "hqdefault.jpg"
        return components.url ?? url
    }

    static func fallbackChain(for url: URL?) -> [Thumbnail] {
        guard let url else { return [] }
        let variants: [(suffix: String, quality: Quality)] = [
            ("maxresdefault.jpg", .maxres),
            ("sddefault.jpg", .standard),
            ("hqdefault.jpg", .high),
            ("mqdefault.jpg", .medium),
            ("default.jpg", .default),
        ]
        let path = url.path
        guard path.range(of: #"/vi/[^/]+/"#, options: .regularExpression) != nil,
              let current = variants.first(where: { path.hasSuffix($0.suffix) }),
              let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
            return [Thumbnail(url: url, quality: .medium)]
        }
        let basePath = String(path.dropLast(current.suffix.count))
        return variants.compactMap { variant in
            guard variant.quality <= current.quality else { return nil }
            var variantComponents = components
            variantComponents.path = basePath + variant.suffix
            guard let variantURL = variantComponents.url else { return nil }
            return Thumbnail(url: variantURL, quality: variant.quality)
        }
    }
}

// MARK: - Preview Support

extension Video {
    /// A sample video for SwiftUI previews.
    static var preview: Video {
        Video(
            id: .global("dQw4w9WgXcQ"),
            title: "Sample Video Title",
            description: "This is a sample video description for preview purposes.",
            author: Author(id: "UC123", name: "Sample Channel"),
            duration: 212,
            publishedAt: Date().addingTimeInterval(-86400 * 3),
            publishedText: "3 days ago",
            viewCount: 1_234_567,
            likeCount: 50_000,
            thumbnails: [
                Thumbnail(
                    url: URL(string: "https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg")!,
                    quality: .maxres,
                    width: 1280,
                    height: 720
                )
            ],
            isLive: false,
            isUpcoming: false,
            scheduledStartTime: nil
        )
    }

    /// A sample live video for SwiftUI previews.
    static var livePreview: Video {
        Video(
            id: .global("live123"),
            title: "Live Stream Example",
            description: "This is a live stream.",
            author: Author(id: "UC456", name: "Live Channel"),
            duration: 0,
            publishedAt: Date(),
            publishedText: "Started streaming",
            viewCount: 5_432,
            likeCount: nil,
            thumbnails: [
                Thumbnail(
                    url: URL(string: "https://i.ytimg.com/vi/live123/maxresdefault.jpg")!,
                    quality: .maxres
                )
            ],
            isLive: true,
            isUpcoming: false,
            scheduledStartTime: nil
        )
    }
}

// MARK: - Equatable & Hashable

extension Video: Equatable {
    /// Custom Equatable implementation that only compares by ID for performance.
    /// Two videos with the same ID are considered equal during SwiftUI diffing.
    /// This avoids expensive comparisons of all properties (including nested Author and [Thumbnail]).
    static func == (lhs: Video, rhs: Video) -> Bool {
        lhs.id == rhs.id
    }
}

extension Video: Hashable {
    /// Custom Hashable implementation consistent with Equatable.
    /// Only hashes the ID since equality is determined by ID alone.
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
}
