import Foundation

enum DownloadStatus: String, Codable, CaseIterable, Sendable {
    case queued = "Queued"
    case downloading = "Downloading"
    case paused = "Paused"
    case completed = "Completed"
    case failed = "Failed"
    case canceled = "Canceled"
}

enum DownloadCategory: String, Codable, CaseIterable, Sendable {
    case musics = "Musics"
    case movies = "Movies"
    case compressed = "Compressed"
    case pictures = "Pictures"
    case documents = "Documents"
    case other = "Other"

    var symbolName: String {
        switch self {
        case .musics: "music.note"
        case .movies: "film"
        case .compressed: "archivebox"
        case .pictures: "photo"
        case .documents: "doc.text"
        case .other: "folder"
        }
    }
}

struct DownloadQueue: Identifiable, Codable, Equatable, Sendable {
    static let mainQueueID = UUID(uuidString: "00000000-0000-0000-0000-000000000001")!

    var id = UUID()
    var name: String

    var isMain: Bool {
        id == Self.mainQueueID
    }

    static var main: DownloadQueue {
        DownloadQueue(id: mainQueueID, name: "Main queue")
    }
}

struct DownloadCredentials: Codable, Equatable, Sendable {
    var username: String
    var password: String

    var isEmpty: Bool {
        username.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty &&
            password.isEmpty
    }
}

enum ChecksumAlgorithm: String, Codable, CaseIterable, Identifiable, Sendable {
    case md5
    case sha1 = "sha-1"
    case sha256 = "sha-256"
    case sha512 = "sha-512"

    var id: String { rawValue }

    var title: String {
        switch self {
        case .md5: "MD5"
        case .sha1: "SHA-1"
        case .sha256: "SHA-256"
        case .sha512: "SHA-512"
        }
    }
}

struct DownloadChecksum: Codable, Equatable, Sendable {
    var algorithm: ChecksumAlgorithm
    var value: String

    var normalized: DownloadChecksum {
        DownloadChecksum(
            algorithm: algorithm,
            value: value.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
        )
    }

    var isEmpty: Bool {
        value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
    }
}

struct DownloadRequestHeader: Codable, Equatable, Sendable {
    var name: String
    var value: String

    var normalized: DownloadRequestHeader {
        let cleanName = name.trimmingCharacters(in: .whitespacesAndNewlines).replacingOccurrences(of: "\r", with: "").replacingOccurrences(of: "\n", with: "")
        let cleanValue = value.trimmingCharacters(in: .whitespacesAndNewlines).replacingOccurrences(of: "\r", with: "").replacingOccurrences(of: "\n", with: "")
        return DownloadRequestHeader(name: cleanName, value: cleanValue)
    }

    var isEmpty: Bool {
        let clean = normalized
        return clean.name.isEmpty && clean.value.isEmpty
    }

    var headerLine: String {
        let clean = normalized
        return "\(clean.name): \(clean.value)"
    }
}

struct DownloadTransferOptions: Equatable, Sendable {
    var checksum: DownloadChecksum?
    var requestHeaders: [DownloadRequestHeader] = []
    var cookieHeader: String?
    var mirrorURLs: [URL] = []
}

enum DownloadTransferOptionParser {
    static func parseHeaders(_ text: String) -> [DownloadRequestHeader] {
        headerLines(text).compactMap { line in
            guard let colonIndex = line.firstIndex(of: ":") else { return nil }
            let name = String(line[..<colonIndex]).trimmingCharacters(in: .whitespacesAndNewlines)
            let value = String(line[line.index(after: colonIndex)...]).trimmingCharacters(in: .whitespacesAndNewlines)
            let header = DownloadRequestHeader(name: name, value: value).normalized
            return header.isEmpty || header.name.isEmpty ? nil : header
        }
    }

    static func invalidHeaderLines(_ text: String) -> [String] {
        headerLines(text).filter { line in
            guard let colonIndex = line.firstIndex(of: ":") else { return true }
            let name = String(line[..<colonIndex]).trimmingCharacters(in: .whitespacesAndNewlines)
            return name.isEmpty
        }
    }

    static func cleanCookieHeader(_ text: String) -> String? {
        var value = text.trimmingCharacters(in: .whitespacesAndNewlines)
        if value.lowercased().hasPrefix("cookie:") {
            value = String(value.dropFirst("cookie:".count)).trimmingCharacters(in: .whitespacesAndNewlines)
        }
        return value.isEmpty ? nil : value
    }

    static func parseMirrorURLs(_ text: String) -> [URL] {
        DownloadURLParser.parse(text)
    }

    static func invalidMirrorLines(_ text: String) -> [String] {
        text.split(whereSeparator: { $0 == "\n" || $0 == "\r" })
            .map { String($0).trimmingCharacters(in: .whitespacesAndNewlines) }
            .filter { !$0.isEmpty && DownloadURLParser.parse($0).isEmpty }
    }

    private static func headerLines(_ text: String) -> [String] {
        text.split(whereSeparator: { $0 == "\n" || $0 == "\r" })
            .map { String($0).trimmingCharacters(in: .whitespacesAndNewlines) }
            .filter { !$0.isEmpty }
    }
}

struct DownloadItem: Identifiable, Codable, Equatable, Sendable {
    var id = UUID()
    var url: URL
    var fileName: String
    var category: DownloadCategory
    var destinationDirectory: URL
    var connectionsPerServer: Int
    var credentials: DownloadCredentials?
    var checksum: DownloadChecksum?
    var requestHeaders: [DownloadRequestHeader]?
    var cookieHeader: String?
    var mirrorURLs: [URL]?
    var speedLimitKiBPerSecond: Int?
    var status: DownloadStatus = .queued
    var progress: Double = 0
    var speedText: String = "-"
    var etaText: String = "-"
    var connectionCount: Int = 0
    var sizeBytes: Int64?
    var bytesText: String = "-"
    var message: String = ""
    var createdAt = Date()
    var lastTryAt: Date?
    var autoResumeOnLaunch: Bool?
    var queueID: UUID?
    var rpcPort: Int?
    var rpcSecret: String?
    var mediaFormatSelector: String?
    var isAudioOnlyMedia: Bool?

    private enum CodingKeys: String, CodingKey {
        case id, url, fileName, category, destinationDirectory, connectionsPerServer
        case credentials, checksum, requestHeaders, cookieHeader, mirrorURLs, speedLimitKiBPerSecond
        case status, progress, speedText, etaText, connectionCount, sizeBytes, bytesText, message
        case createdAt, lastTryAt, autoResumeOnLaunch, queueID
        case mediaFormatSelector, isAudioOnlyMedia
    }

    var displaySpeedText: String {
        if status == .completed || status == .paused || status == .queued || status == .canceled { return "-" }
        return speedText
    }

    var displayETAText: String {
        if status == .completed || status == .queued || status == .canceled { return "-" }
        return etaText
    }

    var destinationPath: String {
        destinationDirectory.appendingPathComponent(fileName).path
    }

    var sortableSize: Int64 {
        sizeBytes ?? 0
    }

    var transferOptions: DownloadTransferOptions {
        DownloadTransferOptions(
            checksum: checksum,
            requestHeaders: requestHeaders ?? [],
            cookieHeader: cookieHeader,
            mirrorURLs: mirrorURLs ?? []
        )
    }

    var speedLimitText: String {
        guard let speedLimitKiBPerSecond, speedLimitKiBPerSecond > 0 else {
            return "No limit"
        }
        return "\(speedLimitKiBPerSecond) KiB/s"
    }

    var redactedForPersistence: DownloadItem {
        var item = self
        if item.credentials != nil {
            item.credentials?.password = ""
        }
        item.cookieHeader = nil
        item.requestHeaders = item.requestHeaders?.filter { !$0.containsSensitiveValue }
        return item
    }
}

private extension DownloadRequestHeader {
    var containsSensitiveValue: Bool {
        switch normalized.name.lowercased() {
        case "authorization", "cookie", "set-cookie", "x-api-key", "x-auth-token":
            true
        default:
            false
        }
    }
}

struct DownloadProgress: Equatable, Sendable {
    var fraction: Double
    var bytesText: String
    var speedText: String
    var etaText: String
    var connectionCount: Int
}

struct PendingDownload: Identifiable, Equatable, Sendable {
    enum MetadataState: Equatable, Sendable {
        case pending
        case loading
        case loaded
        case failed(String)
    }

    var id = UUID()
    var url: URL
    var fileName: String
    var category: DownloadCategory
    var defaultDirectory: URL
    var sizeBytes: Int64?
    var mimeType: String?
    var state: MetadataState = .pending
    
    var isMedia: Bool = false
    var mediaOptions: [CleanFormatOption] = []
    var selectedMediaOption: CleanFormatOption?
    var mediaMetadata: MediaMetadata?

    var destinationPath: String {
        defaultDirectory.appendingPathComponent(fileName).path
    }
}
