import AVFoundation
import Defaults
import Foundation

// swiftlint:disable:next final_class
class Stream: Equatable, Hashable, Identifiable {
    enum Resolution: Comparable, Codable, Defaults.Serializable {
        case predefined(PredefinedResolution)
        case custom(height: Int, refreshRate: Int)

        enum PredefinedResolution: String, CaseIterable, Codable {
            // 8K UHD (16:9) Resolutions
            case hd4320p60, hd4320p30

            // 4K UHD (16:9) Resolutions
            case hd2160p60, hd2160p30

            // 1440p (16:9) Resolutions
            case hd1440p60, hd1440p30

            // 1080p (Full HD, 16:9) Resolutions
            case hd1080p60, hd1080p30

            // 720p (HD, 16:9) Resolutions
            case hd720p60, hd720p30

            // Standard Definition (SD) Resolutions
            case sd480p30
            case sd360p30
            case sd240p30
            case sd144p30
        }

        var name: String {
            switch self {
            case let .predefined(predefined):
                return predefined.rawValue
            case let .custom(height, refreshRate):
                return "\(height)p\(refreshRate != 30 ? ", \(refreshRate) fps" : "")"
            }
        }

        var height: Int {
            switch self {
            case let .predefined(predefined):
                return predefined.height
            case let .custom(height, _):
                return height
            }
        }

        var refreshRate: Int {
            switch self {
            case let .predefined(predefined):
                return predefined.refreshRate
            case let .custom(_, refreshRate):
                return refreshRate
            }
        }

        var bitrate: Int {
            switch self {
            case let .predefined(predefined):
                return predefined.bitrate
            case let .custom(height, refreshRate):
                // Find the closest predefined resolution based on height and refresh rate
                let closestPredefined = Stream.Resolution.PredefinedResolution.allCases.min {
                    abs($0.height - height) + abs($0.refreshRate - refreshRate) <
                        abs($1.height - height) + abs($1.refreshRate - refreshRate)
                }
                // Return the bitrate of the closest predefined resolution or a default bitrate if no close match is found
                return closestPredefined?.bitrate ?? 5_000_000
            }
        }

        static func from(resolution: String, fps: Int? = nil) -> Self {
            if let predefined = PredefinedResolution(rawValue: resolution) {
                return .predefined(predefined)
            }

            // Attempt to parse height and refresh rate
            if let height = Int(resolution.components(separatedBy: "p").first ?? ""), height > 0 {
                let refreshRate = fps ?? 30
                return .custom(height: height, refreshRate: refreshRate)
            }

            // Default behavior if parsing fails
            return .custom(height: 720, refreshRate: 30)
        }

        static func < (lhs: Self, rhs: Self) -> Bool {
            lhs.height == rhs.height ? (lhs.refreshRate < rhs.refreshRate) : (lhs.height < rhs.height)
        }

        enum CodingKeys: String, CodingKey {
            case predefined
            case custom
            case height
            case refreshRate
        }

        init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)

            if let predefinedValue = try? container.decode(PredefinedResolution.self, forKey: .predefined) {
                self = .predefined(predefinedValue)
            } else if let height = try? container.decode(Int.self, forKey: .height),
                      let refreshRate = try? container.decode(Int.self, forKey: .refreshRate)
            {
                self = .custom(height: height, refreshRate: refreshRate)
            } else {
                // Set default resolution to 720p 30 if decoding fails
                self = .custom(height: 720, refreshRate: 30)
            }
        }

        func encode(to encoder: Encoder) throws {
            var container = encoder.container(keyedBy: CodingKeys.self)
            switch self {
            case let .predefined(predefinedValue):
                try container.encode(predefinedValue, forKey: .predefined)
            case let .custom(height, refreshRate):
                try container.encode(height, forKey: .height)
                try container.encode(refreshRate, forKey: .refreshRate)
            }
        }
    }

    enum Kind: String, Comparable {
        case hls, adaptive, stream

        private var sortOrder: Int {
            switch self {
            case .hls:
                return 0
            case .stream:
                return 1
            case .adaptive:
                return 2
            }
        }

        static func < (lhs: Self, rhs: Self) -> Bool {
            lhs.sortOrder < rhs.sortOrder
        }
    }

    enum Format: String {
        case avc1
        case mp4
        case av1
        case webm
        case hls
        case stream
        case unknown

        var description: String {
            switch self {
            case .webm:
                return "WebM"
            case .hls:
                return "adaptive (HLS)"
            case .stream:
                return "Stream"
            default:
                return rawValue.uppercased()
            }
        }

        static func from(_ string: String) -> Self {
            let lowercased = string.lowercased()

            if lowercased.contains("avc1") {
                return .avc1
            }
            if lowercased.contains("mpeg_4") || lowercased.contains("mp4") {
                return .mp4
            }
            if lowercased.contains("av01") {
                return .av1
            }
            if lowercased.contains("webm") {
                return .webm
            }
            if lowercased.contains("stream") {
                return .stream
            }
            if lowercased.contains("hls") {
                return .hls
            }
            return .unknown
        }
    }

    let id = UUID()

    var instance: Instance!
    var audioAsset: AVURLAsset!
    var videoAsset: AVURLAsset!
    var hlsURL: URL!
    var localURL: URL!

    var resolution: Resolution!
    var kind: Kind!
    var format: Format!

    var encoding: String?
    var videoFormat: String?
    var bitrate: Int?
    var requestRange: String?

    init(
        instance: Instance? = nil,
        audioAsset: AVURLAsset? = nil,
        videoAsset: AVURLAsset? = nil,
        hlsURL: URL? = nil,
        localURL: URL? = nil,
        resolution: Resolution? = nil,
        kind: Kind = .hls,
        encoding: String? = nil,
        videoFormat: String? = nil,
        bitrate: Int? = nil,
        requestRange: String? = nil
    ) {
        self.instance = instance
        self.audioAsset = audioAsset
        self.videoAsset = videoAsset
        self.hlsURL = hlsURL
        self.localURL = localURL
        self.resolution = resolution
        self.kind = kind
        self.encoding = encoding
        format = .from(videoFormat ?? "")
        self.bitrate = bitrate
        self.requestRange = requestRange
    }

    var isLocal: Bool {
        localURL != nil
    }

    var isHLS: Bool {
        hlsURL != nil
    }

    var quality: String {
        guard localURL.isNil else { return "Opened File" }

        if kind == .hls {
            return "adaptive (HLS)"
        }

        return resolution.name
    }

    var shortQuality: String {
        guard localURL.isNil else { return "File" }

        if kind == .hls {
            return "adaptive (HLS)"
        }

        if kind == .stream {
            return resolution.name
        }
        return resolutionAndFormat
    }

    var description: String {
        guard localURL.isNil else { return resolutionAndFormat }
        let instanceString = instance.isNil ? "" : " - (\(instance!.description))"
        return format != .hls ? "\(resolutionAndFormat)\(instanceString)" : "adaptive (HLS)\(instanceString)"
    }

    var resolutionAndFormat: String {
        let formatString = format == .unknown ? "" : " (\(format.description))"
        return "\(quality)\(formatString)"
    }

    var assets: [AVURLAsset] {
        [audioAsset, videoAsset]
    }

    var videoAssetContainsAudio: Bool {
        assets.dropFirst().allSatisfy { $0.url == assets.first!.url }
    }

    var singleAssetURL: URL? {
        guard localURL.isNil else {
            return URLBookmarkModel.shared.loadBookmark(localURL) ?? localURL
        }

        if kind == .hls {
            return hlsURL
        }
        if videoAssetContainsAudio {
            return videoAsset.url
        }

        return nil
    }

    static func == (lhs: Stream, rhs: Stream) -> Bool {
        lhs.id == rhs.id
    }

    func hash(into hasher: inout Hasher) {
        if let url = videoAsset?.url {
            hasher.combine(url)
        }
        if let url = audioAsset?.url {
            hasher.combine(url)
        }
        if let url = hlsURL {
            hasher.combine(url)
        }
    }
}

extension Stream.Resolution.PredefinedResolution {
    var height: Int {
        switch self {
        // 8K UHD (16:9) Resolutions
        case .hd4320p60, .hd4320p30:
            return 4320

        // 4K UHD (16:9) Resolutions
        case .hd2160p60, .hd2160p30:
            return 2160

        // 1440p (16:9) Resolutions
        case .hd1440p60, .hd1440p30:
            return 1440

        // 1080p (Full HD, 16:9) Resolutions
        case .hd1080p60, .hd1080p30:
            return 1080

        // 720p (HD, 16:9) Resolutions
        case .hd720p60, .hd720p30:
            return 720

        // Standard Definition (SD) Resolutions
        case .sd480p30:
            return 480

        case .sd360p30:
            return 360

        case .sd240p30:
            return 240

        case .sd144p30:
            return 144
        }
    }

    var refreshRate: Int {
        switch self {
        // 60 fps Resolutions
        case .hd4320p60, .hd2160p60, .hd1440p60, .hd1080p60, .hd720p60:
            return 60

        // 30 fps Resolutions
        case .hd4320p30, .hd2160p30, .hd1440p30, .hd1080p30, .hd720p30,
             .sd480p30, .sd360p30, .sd240p30, .sd144p30:
            return 30
        }
    }

    // These values are an approximation.
    // https://support.google.com/youtube/answer/1722171?hl=en#zippy=%2Cbitrate

    var bitrate: Int {
        switch self {
        // 8K UHD (16:9) Resolutions
        case .hd4320p60:
            return 180_000_000 // Midpoint between 120 Mbps and 240 Mbps
        case .hd4320p30:
            return 120_000_000 // Midpoint between 80 Mbps and 160 Mbps
        // 4K UHD (16:9) Resolutions
        case .hd2160p60:
            return 60_500_000 // Midpoint between 53 Mbps and 68 Mbps
        case .hd2160p30:
            return 40_000_000 // Midpoint between 35 Mbps and 45 Mbps
        // 1440p (2K) Resolutions
        case .hd1440p60:
            return 24_000_000 // 24 Mbps
        case .hd1440p30:
            return 16_000_000 // 16 Mbps
        // 1080p (Full HD, 16:9) Resolutions
        case .hd1080p60:
            return 12_000_000 // 12 Mbps
        case .hd1080p30:
            return 8_000_000 // 8 Mbps
        // 720p (HD, 16:9) Resolutions
        case .hd720p60:
            return 7_500_000 // 7.5 Mbps
        case .hd720p30:
            return 5_000_000 // 5 Mbps
        // Standard Definition (SD) Resolutions
        case .sd480p30:
            return 2_500_000 // 2.5 Mbps
        case .sd360p30:
            return 1_000_000 // 1 Mbps
        case .sd240p30:
            return 1_000_000 // 1 Mbps
        case .sd144p30:
            return 600_000 // 0.6 Mbps
        }
    }
}