//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import Foundation

public enum Upload {
    public typealias ProgressBlock = (Progress) -> Void

    public static let uploadQueue: OperationQueue = {
        let queue = OperationQueue()
        queue.name = "OWSUpload"
        queue.maxConcurrentOperationCount = CurrentAppContext().isNSE ? 2 : 8
        return queue
    }()

    public enum Constants {
        public static let attachmentUploadProgressNotification = NSNotification.Name("AttachmentUploadProgressNotification")
        public static let uploadProgressKey = "UploadProgressKey"
        public static let uploadAttachmentIDKey = "UploadAttachmentIDKey"

        /// If within this window, we can reause existing attachment transit tier uploads for resending.
        public static let uploadReuseWindow: TimeInterval = 60 * 60 * 24 * 3 // 3 days
    }

    public enum FormSource {
        case remote
        case local(Upload.Form)
    }

    public struct Form: Codable {
        private enum CodingKeys: String, CodingKey {
            case headers = "headers"
            case signedUploadLocation = "signedUploadLocation"
            case cdnKey = "key"
            case cdnNumber = "cdn"
        }

        let headers: [String: String]
        let signedUploadLocation: String
        let cdnKey: String
        let cdnNumber: UInt32
    }

    // MARK: -

    public enum FailureMode {
        public enum RetryMode {
            case immediately
            case afterDelay(TimeInterval)
        }

        // The overall upload has hit the max number of retries.
        case noMoreRetries

        // Attempt to resume the current upload from the last known good state.
        case resume(RetryMode)

        // Restart the upload by discarding any current upload progres and
        // fetching a new upload form.
        case restart(RetryMode)
    }

    public enum ResumeProgress {
        // There was an issue with the resume data, discard the current upload
        // form and restart the upload with a new form
        case restart

        // The endpoint reported a complete upload.
        case complete

        // Contains the number of bytes the upload endpoint has received.  This
        // can be 0 bytes, which is effectively a new upload, but can use the
        // existing upload form.
        case uploaded(Int)
    }

    public enum Error: Swift.Error, IsRetryableProvider, LocalizedError {
        case invalidUploadURL
        case uploadFailure(recovery: FailureMode)
        case unsupportedEndpoint
        case unknown

        public var isRetryableProvider: Bool {
            switch self {
            case .invalidUploadURL, .uploadFailure, .unsupportedEndpoint, .unknown:
                return false
            }
        }

        public var errorDescription: String? {
            localizedDescription
        }

        public var localizedDescription: String {
            return OWSLocalizedString(
                "ERROR_MESSAGE_ATTACHMENT_UPLOAD_FAILED",
                comment: "Error message indicating that attachment upload(s) failed."
            )
        }
    }

    public struct EncryptedBackupUploadMetadata: UploadMetadata {
        /// File URL of the data consisting of "iv  + encrypted data + hmac"
        public let fileUrl: URL

        /// The digest of the encrypted file.  The encrypted file consist of "iv + encrypted data + hmac"
        public let digest: Data

        /// The length of the encrypted data, consiting of "iv  + encrypted data + hmac"
        public let encryptedDataLength: UInt32

        /// The length of the unencrypted data
        public let plaintextDataLength: UInt32
    }

    public struct LocalUploadMetadata: UploadMetadata {
        /// File URL of the data consisting of "iv  + encrypted data + hmac"
        public let fileUrl: URL

        /// encryption key + hmac
        public let key: Data

        /// The digest of the encrypted file.  The encrypted file consist of "iv + encrypted data + hmac"
        public let digest: Data

        /// The length of the encrypted data, consiting of "iv  + encrypted data + hmac"
        public let encryptedDataLength: UInt32

        /// The length of the unencrypted data
        public let plaintextDataLength: UInt32
    }

    public struct Result<Metadata: UploadMetadata> {
        let cdnKey: String
        let cdnNumber: UInt32
        let localUploadMetadata: Metadata

        // Timestamp the upload attempt began
        let beginTimestamp: UInt64

        // Timestamp the upload attempt completed
        let finishTimestamp: UInt64
    }

    public struct Attempt {
        let cdnKey: String
        let cdnNumber: UInt32
        let localMetadata: UploadMetadata
        let beginTimestamp: UInt64
        let endpoint: UploadEndpoint
        let uploadLocation: URL
        let logger: PrefixedLogger
    }
}

extension Upload.LocalUploadMetadata {

    static func validateAndBuild(
        fileUrl: URL,
        metadata: EncryptionMetadata
    ) throws -> Upload.LocalUploadMetadata {
        guard let lengthRaw = metadata.length, let plaintextLengthRaw = metadata.plaintextLength else {
            throw OWSAssertionError("Missing length.")
        }

        guard
            lengthRaw > 0,
            lengthRaw <= UInt32.max,
            plaintextLengthRaw > 0,
            plaintextLengthRaw <= UInt32.max
        else {
            throw OWSAssertionError("Invalid length.")
        }

        let length = UInt32(lengthRaw)
        let plaintextLength = UInt32(plaintextLengthRaw)

        guard
            plaintextLength <= OWSMediaUtils.kMaxFileSizeGeneric,
            length <= OWSMediaUtils.kMaxAttachmentUploadSizeBytes
        else {
            throw OWSAssertionError("Data is too large: \(length).")
        }

        guard let digest = metadata.digest else {
            throw OWSAssertionError("Digest missing for attachment.")
        }

        return Upload.LocalUploadMetadata(
            fileUrl: fileUrl,
            key: metadata.key,
            digest: digest,
            encryptedDataLength: length,
            plaintextDataLength: plaintextLength
        )
    }
}
