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

import GRDB
public import LibSignalClient

public enum BackupValidationError: Error {
    case unknownFields([String])
    case validationFailed(message: String, unknownFields: [String])
    case ioError(String)
    case unknownError
}

public enum BackupImportError: Error {
    case unsupportedVersion
}

public class BackupArchiveManagerImpl: BackupArchiveManager {
    public enum Constants {
        fileprivate static let keyValueStoreCollectionName = "MessageBackupManager"
        fileprivate static let keyValueStoreRestoreStateKey = "keyValueStoreRestoreStateKey"
        fileprivate static let keyValueStoreNeedForwardSecrecyTokenFetchKey = "keyValueStoreNeedForwardSecrecyTokenFetchKey"

        public static let supportedBackupVersion: UInt64 = 1

        /// The ratio of frames processed for which to sample memory.
        fileprivate static let memorySamplerFrameRatio: Float = BuildFlags.Backups.detailedBenchLogging ? 0.001 : 0
    }

    private class NotImplementedError: Error {}
    private class BackupError: Error {}
    private typealias LoggableErrorAndProto = BackupArchive.LoggableErrorAndProto

    private let accountDataArchiver: BackupArchiveAccountDataArchiver
    private let adHocCallArchiver: BackupArchiveAdHocCallArchiver
    private let appVersion: AppVersion
    private let attachmentDownloadManager: AttachmentDownloadManager
    private let attachmentUploadManager: AttachmentUploadManager
    private let avatarFetcher: BackupArchiveAvatarFetcher
    private let backupArchiveErrorPresenter: BackupArchiveErrorPresenter
    private let backupAttachmentCoordinator: BackupAttachmentCoordinator
    private let backupAttachmentUploadEraStore: BackupAttachmentUploadEraStore
    private let backupNonceMetadataStore: BackupNonceMetadataStore
    private let backupRequestManager: BackupRequestManager
    private let backupSettingsStore: BackupSettingsStore
    private let backupStickerPackDownloadStore: BackupStickerPackDownloadStore
    private let callLinkRecipientArchiver: BackupArchiveCallLinkRecipientArchiver
    private let chatArchiver: BackupArchiveChatArchiver
    private let chatItemArchiver: BackupArchiveChatItemArchiver
    private let contactRecipientArchiver: BackupArchiveContactRecipientArchiver
    private let databaseChangeObserver: DatabaseChangeObserver
    private let dateProvider: DateProvider
    private let dateProviderMonotonic: DateProviderMonotonic
    private let db: any DB
    private let disappearingMessagesExpirationJob: DisappearingMessagesExpirationJob
    private let distributionListRecipientArchiver: BackupArchiveDistributionListRecipientArchiver
    private let encryptedStreamProvider: BackupArchiveEncryptedProtoStreamProvider
    private let fullTextSearchIndexer: BackupArchiveFullTextSearchIndexer
    private let groupRecipientArchiver: BackupArchiveGroupRecipientArchiver
    private let kvStore: KeyValueStore
    private let libsignalNet: LibSignalClient.Net
    private let localStorage: AccountKeyStore
    private let localRecipientArchiver: BackupArchiveLocalRecipientArchiver
    private let logger: PrefixedLogger
    private let messagePipelineSupervisor: MessagePipelineSupervisor
    private let oversizeTextArchiver: BackupArchiveInlinedOversizeTextArchiver
    private let plaintextStreamProvider: BackupArchivePlaintextProtoStreamProvider
    private let postFrameRestoreActionManager: BackupArchivePostFrameRestoreActionManager
    private let releaseNotesRecipientArchiver: BackupArchiveReleaseNotesRecipientArchiver
    private let remoteConfigManager: RemoteConfigManager
    private let stickerPackArchiver: BackupArchiveStickerPackArchiver
    private let tsAccountManager: TSAccountManager

    init(
        accountDataArchiver: BackupArchiveAccountDataArchiver,
        adHocCallArchiver: BackupArchiveAdHocCallArchiver,
        appVersion: AppVersion,
        attachmentDownloadManager: AttachmentDownloadManager,
        attachmentUploadManager: AttachmentUploadManager,
        avatarFetcher: BackupArchiveAvatarFetcher,
        backupArchiveErrorPresenter: BackupArchiveErrorPresenter,
        backupAttachmentCoordinator: BackupAttachmentCoordinator,
        backupAttachmentUploadEraStore: BackupAttachmentUploadEraStore,
        backupNonceMetadataStore: BackupNonceMetadataStore,
        backupRequestManager: BackupRequestManager,
        backupSettingsStore: BackupSettingsStore,
        backupStickerPackDownloadStore: BackupStickerPackDownloadStore,
        callLinkRecipientArchiver: BackupArchiveCallLinkRecipientArchiver,
        chatArchiver: BackupArchiveChatArchiver,
        chatItemArchiver: BackupArchiveChatItemArchiver,
        contactRecipientArchiver: BackupArchiveContactRecipientArchiver,
        databaseChangeObserver: DatabaseChangeObserver,
        dateProvider: @escaping DateProvider,
        dateProviderMonotonic: @escaping DateProviderMonotonic,
        db: any DB,
        disappearingMessagesExpirationJob: DisappearingMessagesExpirationJob,
        distributionListRecipientArchiver: BackupArchiveDistributionListRecipientArchiver,
        encryptedStreamProvider: BackupArchiveEncryptedProtoStreamProvider,
        fullTextSearchIndexer: BackupArchiveFullTextSearchIndexer,
        groupRecipientArchiver: BackupArchiveGroupRecipientArchiver,
        libsignalNet: LibSignalClient.Net,
        localStorage: AccountKeyStore,
        localRecipientArchiver: BackupArchiveLocalRecipientArchiver,
        messagePipelineSupervisor: MessagePipelineSupervisor,
        oversizeTextArchiver: BackupArchiveInlinedOversizeTextArchiver,
        plaintextStreamProvider: BackupArchivePlaintextProtoStreamProvider,
        postFrameRestoreActionManager: BackupArchivePostFrameRestoreActionManager,
        releaseNotesRecipientArchiver: BackupArchiveReleaseNotesRecipientArchiver,
        remoteConfigManager: RemoteConfigManager,
        stickerPackArchiver: BackupArchiveStickerPackArchiver,
        tsAccountManager: TSAccountManager,
    ) {
        self.accountDataArchiver = accountDataArchiver
        self.appVersion = appVersion
        self.attachmentDownloadManager = attachmentDownloadManager
        self.attachmentUploadManager = attachmentUploadManager
        self.avatarFetcher = avatarFetcher
        self.backupArchiveErrorPresenter = backupArchiveErrorPresenter
        self.backupAttachmentCoordinator = backupAttachmentCoordinator
        self.backupAttachmentUploadEraStore = backupAttachmentUploadEraStore
        self.backupNonceMetadataStore = backupNonceMetadataStore
        self.backupRequestManager = backupRequestManager
        self.backupSettingsStore = backupSettingsStore
        self.backupStickerPackDownloadStore = backupStickerPackDownloadStore
        self.callLinkRecipientArchiver = callLinkRecipientArchiver
        self.chatArchiver = chatArchiver
        self.chatItemArchiver = chatItemArchiver
        self.contactRecipientArchiver = contactRecipientArchiver
        self.databaseChangeObserver = databaseChangeObserver
        self.dateProvider = dateProvider
        self.dateProviderMonotonic = dateProviderMonotonic
        self.db = db
        self.disappearingMessagesExpirationJob = disappearingMessagesExpirationJob
        self.distributionListRecipientArchiver = distributionListRecipientArchiver
        self.encryptedStreamProvider = encryptedStreamProvider
        self.fullTextSearchIndexer = fullTextSearchIndexer
        self.groupRecipientArchiver = groupRecipientArchiver
        self.kvStore = KeyValueStore(collection: Constants.keyValueStoreCollectionName)
        self.libsignalNet = libsignalNet
        self.localStorage = localStorage
        self.localRecipientArchiver = localRecipientArchiver
        self.logger = PrefixedLogger(prefix: "[Backups]")
        self.messagePipelineSupervisor = messagePipelineSupervisor
        self.oversizeTextArchiver = oversizeTextArchiver
        self.plaintextStreamProvider = plaintextStreamProvider
        self.postFrameRestoreActionManager = postFrameRestoreActionManager
        self.releaseNotesRecipientArchiver = releaseNotesRecipientArchiver
        self.remoteConfigManager = remoteConfigManager
        self.stickerPackArchiver = stickerPackArchiver
        self.adHocCallArchiver = adHocCallArchiver
        self.tsAccountManager = tsAccountManager
    }

    // MARK: - Remote backups

    public func downloadEncryptedBackup(
        backupKey: MessageRootBackupKey,
        backupAuth: BackupServiceAuth,
        progress: OWSProgressSink?,
        logger: PrefixedLogger,
    ) async throws -> URL {
        let metadata = try await backupRequestManager.fetchBackupRequestMetadata(auth: backupAuth, logger: logger)
        let tmpFileUrl = try await attachmentDownloadManager.downloadBackup(
            metadata: metadata,
            progress: progress,
        )

        // Once protos calm down, this can be enabled to warn/error on failed validation
        // try await validateBackup(localIdentifiers: localIdentifiers, fileUrl: tmpFileUrl)

        return tmpFileUrl
    }

    public func backupCdnInfo(
        backupKey: MessageRootBackupKey,
        backupAuth: BackupServiceAuth,
        logger: PrefixedLogger,
    ) async throws -> BackupCdnInfo {
        let metadata = try await backupRequestManager.fetchBackupRequestMetadata(auth: backupAuth, logger: logger)
        return try await attachmentDownloadManager.backupCdnInfo(metadata: metadata)
    }

    public func uploadEncryptedBackup(
        backupKey: MessageRootBackupKey,
        metadata: Upload.EncryptedBackupUploadMetadata,
        auth: ChatServiceAuth,
        progress: OWSProgressSink?,
        logger: PrefixedLogger,
    ) async throws -> Upload.Result<Upload.EncryptedBackupUploadMetadata> {
        try Task.checkCancellation()
        guard db.read(block: { tsAccountManager.registrationState(tx: $0).isPrimaryDevice }) == true else {
            throw OWSAssertionError("Backing up not on a registered primary!")
        }

        let backupAuth = try await backupRequestManager.fetchBackupServiceAuth(
            for: backupKey,
            localAci: backupKey.aci,
            auth: auth,
            logger: logger,
        )
        let form = try await backupRequestManager.fetchBackupUploadForm(
            backupByteLength: metadata.encryptedDataLength,
            auth: backupAuth,
            logger: logger,
        )
        let result = try await attachmentUploadManager.uploadBackup(
            localUploadMetadata: metadata,
            form: form,
            progress: progress,
        )

        await db.awaitableWrite { tx in
            let backupFileSizeBytes: UInt64
            let backupMediaSizeBytes: UInt64
            switch backupSettingsStore.backupPlan(tx: tx) {
            case .paid, .paidExpiringSoon, .paidAsTester:
                backupFileSizeBytes = UInt64(safeCast: metadata.encryptedDataLength)
                backupMediaSizeBytes = metadata.attachmentByteSize
            case .free:
                backupFileSizeBytes = UInt64(safeCast: metadata.encryptedDataLength)
                backupMediaSizeBytes = 0
            case .disabled, .disabling:
                owsFailDebug("Shouldn't generate backup when backups is disabled", logger: logger)
                backupFileSizeBytes = 0
                backupMediaSizeBytes = 0
            }

            backupSettingsStore.setLastBackupDetails(
                date: metadata.exportStartDate,
                backupFileSizeBytes: backupFileSizeBytes,
                backupMediaSizeBytes: backupMediaSizeBytes,
                tx: tx,
            )

            if let nonceMetadata = metadata.nonceMetadata {
                backupNonceMetadataStore.setLastForwardSecrecyToken(
                    nonceMetadata.forwardSecrecyToken,
                    for: backupKey,
                    tx: tx,
                )
                backupNonceMetadataStore.setNextSecretMetadata(
                    nonceMetadata.nextSecretMetadata,
                    for: backupKey,
                    tx: tx,
                )
            }
        }

        return result
    }

    // MARK: - Export

    public func exportEncryptedBackup(
        localIdentifiers: LocalIdentifiers,
        backupPurpose: BackupExportPurpose,
        progress progressSink: OWSProgressSink?,
        logger: PrefixedLogger,
    ) async throws -> Upload.EncryptedBackupUploadMetadata {
        let attachmentByteCounter = BackupArchiveAttachmentByteCounter()
        let startDate = dateProvider()

        // Filter included content according to the purpose of this backup.
        let includedContentFilter = BackupArchive.IncludedContentFilter(
            backupPurpose: backupPurpose.libsignalPurpose,
        )

        switch backupPurpose {
        case .remoteExport(let key, let chatAuth):
            // If an SVRB restore has been scheduled, do this restore before continuing
            // with the remote backup.  This ensures the local and remote state are
            // consistent and avoids the possibility of a backup being created that
            // can't be recovered using the material in SVRB.
            if db.read(block: { needsRestoreFromSVRBBeforeRemoteExport(tx: $0) }) {
                do {
                    try await fetchRemoteSVRBForwardSecrecyToken(key: key, auth: chatAuth, logger: logger)
                } catch SVRBError.unrecoverable {
                    // Not found, so consider a success and fallthrough
                    logger.info("SVRB not found, skipping restore.")
                } catch {
                    logger.warn("Encountered error restoring SVRB: \(error)")
                    throw error
                }

                await db.awaitableWrite {
                    kvStore.setBool(
                        false,
                        key: Constants.keyValueStoreNeedForwardSecrecyTokenFetchKey,
                        transaction: $0,
                    )
                }
            }
        case .linkNsync:
            break
        }

        let encryptionMetadata = try await backupPurpose.deriveEncryptionMetadataWithSVRBIfNeeded(
            backupRequestManager: backupRequestManager,
            db: db,
            libsignalNet: libsignalNet,
            nonceStore: backupNonceMetadataStore,
        )

        let metadata = try await _exportBackup(
            localIdentifiers: localIdentifiers,
            backupPurpose: backupPurpose.libsignalPurpose,
            startDate: startDate,
            includedContentFilter: includedContentFilter,
            progressSink: progressSink,
            attachmentByteCounter: attachmentByteCounter,
            benchTitle: "Export encrypted Backup",
            openOutputStreamBlock: { exportProgress, tx in
                return encryptedStreamProvider.openEncryptedOutputFileStream(
                    startDate: startDate,
                    encryptionMetadata: encryptionMetadata,
                    exportProgress: exportProgress,
                    attachmentByteCounter: attachmentByteCounter,
                    tx: tx,
                )
            },
        )

        try await self.validateEncryptedBackup(
            fileUrl: metadata.fileUrl,
            backupEncryptionKey: encryptionMetadata.encryptionKey,
            backupPurpose: backupPurpose.libsignalPurpose,
        )

        return metadata
    }

#if TESTABLE_BUILD
    public func exportPlaintextBackupForTests(
        localIdentifiers: LocalIdentifiers,
    ) async throws -> URL {
        let attachmentByteCounter = BackupArchiveAttachmentByteCounter()
        let startDate = dateProvider()

        // For the integration tests, don't filter out any content. The premise
        // of the tests is to verify that round-tripping a Backup file is
        // idempotent. The device transfer purpose includes everything.
        let includedContentFilter = BackupArchive.IncludedContentFilter(
            backupPurpose: .deviceTransfer,
        )

        return try await _exportBackup(
            localIdentifiers: localIdentifiers,
            backupPurpose: .remoteBackup,
            startDate: startDate,
            includedContentFilter: includedContentFilter,
            progressSink: nil,
            attachmentByteCounter: attachmentByteCounter,
            benchTitle: "Export plaintext Backup",
            openOutputStreamBlock: { exportProgress, tx in
                return plaintextStreamProvider.openPlaintextOutputFileStream(
                    exportProgress: exportProgress,
                )
            },
        )
    }
#endif

    private func _exportBackup<OutputStreamMetadata>(
        localIdentifiers: LocalIdentifiers,
        backupPurpose: MessageBackupPurpose,
        startDate: Date,
        includedContentFilter: BackupArchive.IncludedContentFilter,
        progressSink: OWSProgressSink?,
        attachmentByteCounter: BackupArchiveAttachmentByteCounter,
        benchTitle: String,
        openOutputStreamBlock: (
            BackupArchiveExportProgress?,
            DBReadTransaction,
        ) -> BackupArchive.ProtoStream.OpenOutputStreamResult<OutputStreamMetadata>,
    ) async throws -> OutputStreamMetadata {
        let prepareOversizeTextAttachmentsProgressSink: OWSProgressSink?
        let exportProgress: BackupArchiveExportProgress?
        if let progressSink {
            prepareOversizeTextAttachmentsProgressSink = await progressSink.addChild(
                withLabel: "Export Backup: Oversize Text Attachments",
                unitCount: 5,
            )
            exportProgress = await .prepare(
                sink: await progressSink.addChild(
                    withLabel: "Export Backup: Export Frames",
                    unitCount: 95,
                ),
                db: db,
            )
        } else {
            prepareOversizeTextAttachmentsProgressSink = nil
            exportProgress = nil
        }

        await oversizeTextArchiver.populateTableIncrementally(progress: prepareOversizeTextAttachmentsProgressSink)

        // Before we export, we need to make sure we have an MRBK – the export
        // will refetch this, and throw if it's missing.
        _ = await db.awaitableWrite { tx in
            localStorage.getOrGenerateMediaRootBackupKey(tx: tx)
        }

        return try db.read { tx in
            let outputStreamMetadata = try BenchMemory(
                title: benchTitle,
                memorySamplerRatio: Constants.memorySamplerFrameRatio,
                logInProduction: true,
            ) { memorySampler -> OutputStreamMetadata in
                let outputStream: BackupArchiveProtoOutputStream
                let outputStreamMetadataProvider: () throws -> OutputStreamMetadata
                switch openOutputStreamBlock(exportProgress, tx) {
                case .success(let _outputStream, let _outputStreamMetadataProvider):
                    outputStream = _outputStream
                    outputStreamMetadataProvider = _outputStreamMetadataProvider
                case .unableToOpenFileStream:
                    throw OWSAssertionError("Unable to open output file stream!")
                }

                try self._exportBackup(
                    outputStream: outputStream,
                    localIdentifiers: localIdentifiers,
                    backupPurpose: backupPurpose,
                    startDate: startDate,
                    attachmentByteCounter: attachmentByteCounter,
                    includedContentFilter: includedContentFilter,
                    currentAppVersion: appVersion.currentAppVersion,
                    firstAppVersion: appVersion.firstBackupAppVersion ?? appVersion.firstAppVersion,
                    memorySampler: memorySampler,
                    tx: tx,
                )

                return try outputStreamMetadataProvider()
            }

            return outputStreamMetadata
        }
    }

    private func _exportBackup(
        outputStream stream: BackupArchiveProtoOutputStream,
        localIdentifiers: LocalIdentifiers,
        backupPurpose: MessageBackupPurpose,
        startDate: Date,
        attachmentByteCounter: BackupArchiveAttachmentByteCounter,
        includedContentFilter: BackupArchive.IncludedContentFilter,
        currentAppVersion: String,
        firstAppVersion: String,
        memorySampler: MemorySampler,
        tx: DBReadTransaction,
    ) throws {
        let bencher = BackupArchive.ArchiveBencher(
            dateProviderMonotonic: dateProviderMonotonic,
            memorySampler: memorySampler,
        )
        let remoteConfig = remoteConfigManager.currentConfig()
        let currentUploadEra = backupAttachmentUploadEraStore.currentUploadEra(tx: tx)
        let backupVersion = Constants.supportedBackupVersion
        let purposeString: String = switch backupPurpose {
        case .deviceTransfer: "LinkNSync"
        case .remoteBackup: "RemoteBackup"
        }

        // We already have a passed-in MRBK, but that came from outside this read tx so
        // refetch it to make sure. If it changed to a new value, use the new value, thats fine
        // (though unexpected). If it changed to _nil_ (should never happen on primaries), exit.
        guard let mediaRootBackupKey = localStorage.getMediaRootBackupKey(tx: tx) else {
            throw OWSAssertionError("MRBK unset as backup being created!")
        }

        var errors = [LoggableErrorAndProto]()
        let result = Result<Void, Error>(catching: {
            logger.info("Exporting for \(purposeString) with version \(backupVersion), timestamp \(startDate.ows_millisecondsSince1970)")

            try autoreleasepool {
                try writeHeader(
                    stream: stream,
                    backupVersion: backupVersion,
                    startDate: startDate,
                    currentAppVersion: currentAppVersion,
                    firstAppVersion: firstAppVersion,
                    mediaRootBackupKey: mediaRootBackupKey,
                    tx: tx,
                )
            }
            try Task.checkCancellation()

            let customChatColorContext = BackupArchive.CustomChatColorArchivingContext(
                localIdentifiers: localIdentifiers,
                startDate: startDate,
                remoteConfig: remoteConfig,
                currentUploadEra: currentUploadEra,
                bencher: bencher,
                attachmentByteCounter: attachmentByteCounter,
                includedContentFilter: includedContentFilter,
                tx: tx,
            )
            try autoreleasepool {
                let accountDataResult = accountDataArchiver.archiveAccountData(
                    stream: stream,
                    context: customChatColorContext,
                )
                switch accountDataResult {
                case .success:
                    break
                case .failure(let error):
                    errors.append(LoggableErrorAndProto(error: error, wasFrameDropped: true))
                    throw OWSAssertionError("Failed to archive account data")
                }
            }
            try Task.checkCancellation()

            let localRecipientResult = localRecipientArchiver.archiveLocalRecipient(
                stream: stream,
                bencher: bencher,
                localIdentifiers: localIdentifiers,
                tx: tx,
            )

            try Task.checkCancellation()

            let localRecipientId: BackupArchive.RecipientId
            switch localRecipientResult {
            case .success(let success):
                localRecipientId = success
            case .failure(let error):
                errors.append(LoggableErrorAndProto(error: error, wasFrameDropped: true))
                throw OWSAssertionError("Failed to archive local recipient!")
            }

            guard
                let localSignalRecipientRowId = localRecipientArchiver.fetchLocalRecipientRowId(
                    localIdentifiers: localIdentifiers,
                    tx: tx,
                )
            else {
                throw OWSAssertionError("Failed to fetch local recipient row ID!")
            }

            let recipientArchivingContext = BackupArchive.RecipientArchivingContext(
                localRecipientId: localRecipientId,
                localSignalRecipientRowId: localSignalRecipientRowId,
                localIdentifiers: localIdentifiers,
                startDate: startDate,
                remoteConfig: remoteConfig,
                currentUploadEra: currentUploadEra,
                bencher: bencher,
                attachmentByteCounter: attachmentByteCounter,
                includedContentFilter: includedContentFilter,
                tx: tx,
            )

            try autoreleasepool {
                switch releaseNotesRecipientArchiver.archiveReleaseNotesRecipient(
                    stream: stream,
                    context: recipientArchivingContext,
                ) {
                case .success:
                    break
                case .failure(let error):
                    errors.append(LoggableErrorAndProto(error: error, wasFrameDropped: true))
                    throw OWSAssertionError("Failed to archive release notes channel!")
                }
            }
            try Task.checkCancellation()

            switch try contactRecipientArchiver.archiveAllContactRecipients(
                stream: stream,
                context: recipientArchivingContext,
            ) {
            case .success:
                break
            case .partialSuccess(let partialFailures):
                errors.append(contentsOf: partialFailures.map { LoggableErrorAndProto(error: $0, wasFrameDropped: false) })
            case .completeFailure(let error):
                errors.append(LoggableErrorAndProto(error: error, wasFrameDropped: true))
                throw BackupError()
            }

            switch try groupRecipientArchiver.archiveAllGroupRecipients(
                stream: stream,
                context: recipientArchivingContext,
            ) {
            case .success:
                break
            case .partialSuccess(let partialFailures):
                errors.append(contentsOf: partialFailures.map { LoggableErrorAndProto(error: $0, wasFrameDropped: false) })
            case .completeFailure(let error):
                errors.append(LoggableErrorAndProto(error: error, wasFrameDropped: true))
                throw BackupError()
            }

            switch try distributionListRecipientArchiver.archiveAllDistributionListRecipients(
                stream: stream,
                context: recipientArchivingContext,
            ) {
            case .success:
                break
            case .partialSuccess(let partialFailures):
                errors.append(contentsOf: partialFailures.map { LoggableErrorAndProto(error: $0, wasFrameDropped: false) })
            case .completeFailure(let error):
                errors.append(LoggableErrorAndProto(error: error, wasFrameDropped: true))
                throw BackupError()
            }

            switch try callLinkRecipientArchiver.archiveAllCallLinkRecipients(
                stream: stream,
                context: recipientArchivingContext,
            ) {
            case .success:
                break
            case .partialSuccess(let partialFailures):
                errors.append(contentsOf: partialFailures.map { LoggableErrorAndProto(error: $0, wasFrameDropped: false) })
            case .completeFailure(let error):
                errors.append(LoggableErrorAndProto(error: error, wasFrameDropped: true))
                throw BackupError()
            }

            let chatArchivingContext = BackupArchive.ChatArchivingContext(
                customChatColorContext: customChatColorContext,
                recipientContext: recipientArchivingContext,
                localIdentifiers: localIdentifiers,
                startDate: startDate,
                remoteConfig: remoteConfig,
                currentUploadEra: currentUploadEra,
                bencher: bencher,
                attachmentByteCounter: attachmentByteCounter,
                includedContentFilter: includedContentFilter,
                tx: tx,
            )
            let chatArchiveResult = try chatArchiver.archiveChats(
                stream: stream,
                context: chatArchivingContext,
            )
            switch chatArchiveResult {
            case .success:
                break
            case .partialSuccess(let partialFailures):
                errors.append(contentsOf: partialFailures.map { LoggableErrorAndProto(error: $0, wasFrameDropped: false) })
            case .completeFailure(let error):
                errors.append(LoggableErrorAndProto(error: error, wasFrameDropped: true))
                throw BackupError()
            }

            let chatItemArchiveResult = try chatItemArchiver.archiveInteractions(
                stream: stream,
                context: chatArchivingContext,
            )
            switch chatItemArchiveResult {
            case .success:
                break
            case .partialSuccess(let partialFailures):
                errors.append(contentsOf: partialFailures.map { LoggableErrorAndProto(error: $0, wasFrameDropped: false) })
            case .completeFailure(let error):
                errors.append(LoggableErrorAndProto(error: error, wasFrameDropped: true))
                throw BackupError()
            }

            let archivingContext = BackupArchive.ArchivingContext(
                localIdentifiers: localIdentifiers,
                startDate: startDate,
                remoteConfig: remoteConfig,
                currentUploadEra: currentUploadEra,
                bencher: bencher,
                attachmentByteCounter: attachmentByteCounter,
                includedContentFilter: includedContentFilter,
                tx: tx,
            )
            let stickerPackArchiveResult = try stickerPackArchiver.archiveStickerPacks(
                stream: stream,
                context: archivingContext,
            )
            switch stickerPackArchiveResult {
            case .success:
                break
            case .partialSuccess(let partialFailures):
                errors.append(contentsOf: partialFailures.map { LoggableErrorAndProto(error: $0, wasFrameDropped: false) })
            case .completeFailure(let error):
                errors.append(LoggableErrorAndProto(error: error, wasFrameDropped: true))
                throw BackupError()
            }

            let adHocCallArchiveResult = try adHocCallArchiver.archiveAdHocCalls(
                stream: stream,
                context: chatArchivingContext,
            )
            switch adHocCallArchiveResult {
            case .success:
                break
            case .partialSuccess(let partialFailures):
                errors.append(contentsOf: partialFailures.map { LoggableErrorAndProto(error: $0, wasFrameDropped: false) })
            case .completeFailure(let error):
                errors.append(LoggableErrorAndProto(error: error, wasFrameDropped: true))
                throw BackupError()
            }

            try stream.closeFileStream()

            logger.info("Finished exporting backup")
            bencher.logResults()
        })
        processErrors(errors: errors, didFail: result.isSuccess.negated)
        return try result.get()
    }

    private func writeHeader(
        stream: BackupArchiveProtoOutputStream,
        backupVersion: UInt64,
        startDate: Date,
        currentAppVersion: String,
        firstAppVersion: String,
        mediaRootBackupKey: MediaRootBackupKey,
        tx: DBReadTransaction,
    ) throws {
        var backupInfo = BackupProto_BackupInfo()
        backupInfo.version = backupVersion
        backupInfo.backupTimeMs = startDate.ows_millisecondsSince1970
        backupInfo.currentAppVersion = currentAppVersion
        backupInfo.firstAppVersion = firstAppVersion

        backupInfo.mediaRootBackupKey = mediaRootBackupKey.serialize()

        switch stream.writeHeader(backupInfo) {
        case .success:
            break
        case .fileIOError(let error), .protoSerializationError(let error):
            throw error
        }
    }

    // MARK: - Import

    public func backupRestoreState(tx: DBReadTransaction) -> BackupRestoreState {
        let raw = kvStore.getInt(
            Constants.keyValueStoreRestoreStateKey,
            defaultValue: 0,
            transaction: tx,
        )
        guard let value = BackupRestoreState(rawValue: raw) else {
            owsFailDebug("Unrecognized state!")
            return .none
        }
        return value
    }

    public func importEncryptedBackup(
        fileUrl: URL,
        localIdentifiers: LocalIdentifiers,
        isPrimaryDevice: Bool,
        source: BackupImportSource,
        progress progressSink: OWSProgressSink?,
        logger: PrefixedLogger,
    ) async throws {

        let backupEncryptionKey = try await source.deriveBackupEncryptionKeyWithSVRBIfNeeded(
            backupRequestManager: backupRequestManager,
            db: db,
            libsignalNet: libsignalNet,
            nonceStore: backupNonceMetadataStore,
            logger: logger,
        )

        try await _importBackup(
            fileUrl: fileUrl,
            localIdentifiers: localIdentifiers,
            isPrimaryDevice: isPrimaryDevice,
            progressSink: progressSink,
            benchTitle: "Import encrypted Backup",
            backupPurpose: source.libsignalPurpose,
            openInputStreamBlock: { fileUrl, frameRestoreProgress, tx in
                return encryptedStreamProvider.openEncryptedInputFileStream(
                    fileUrl: fileUrl,
                    source: source,
                    backupEncryptionKey: backupEncryptionKey,
                    frameRestoreProgress: frameRestoreProgress,
                    tx: tx,
                )
            },
        )
    }

#if TESTABLE_BUILD
    public func importPlaintextBackupForTests(
        fileUrl: URL,
        localIdentifiers: LocalIdentifiers,
    ) async throws {
        try await _importBackup(
            fileUrl: fileUrl,
            localIdentifiers: localIdentifiers,
            isPrimaryDevice: true,
            progressSink: nil,
            benchTitle: "Import plaintext Backup",
            backupPurpose: .remoteBackup,
            openInputStreamBlock: { fileUrl, frameRestoreProgress, _ in
                return plaintextStreamProvider.openPlaintextInputFileStream(
                    fileUrl: fileUrl,
                    frameRestoreProgress: frameRestoreProgress,
                )
            },
        )
    }
#endif

    /// Everything in this method MUST be idempotent, as partial progress can be made
    /// before app termination, which will result in this getting called again.
    public func finalizeBackupImport(progress: OWSProgressSink?) async throws {
        let oversizedTextProgress: OWSProgressSink?
        if let progress {
            oversizedTextProgress = await progress.addChild(
                withLabel: "Import Backup: Process Oversized Text Attachments",
                unitCount: 5,
            )
        } else {
            oversizedTextProgress = nil
        }

        try await oversizeTextArchiver.finishRestoringOversizedTextAttachments(
            progress: oversizedTextProgress,
        )

        await db.awaitableWrite { tx in
            kvStore.setInt(
                BackupRestoreState.finalized.rawValue,
                key: Constants.keyValueStoreRestoreStateKey,
                transaction: tx,
            )
        }
    }

    private func _importBackup(
        fileUrl: URL,
        localIdentifiers: LocalIdentifiers,
        isPrimaryDevice: Bool,
        progressSink: OWSProgressSink?,
        benchTitle: String,
        backupPurpose: MessageBackupPurpose,
        openInputStreamBlock: (
            URL,
            BackupArchiveImportFramesProgress?,
            DBReadTransaction,
        ) -> BackupArchive.ProtoStream.OpenInputStreamResult,
    ) async throws {
        let frameRestoreProgress: BackupArchiveImportFramesProgress?
        let recreateIndexesProgress: BackupArchiveImportRecreateIndexesProgress?
        let finalizeProgress: OWSProgressSink?
        if let progressSink {
            frameRestoreProgress = try await .prepare(
                sink: await progressSink.addChild(
                    withLabel: "Import Backup: Import Frames",
                    unitCount: 83,
                ),
                fileUrl: fileUrl,
            )
            recreateIndexesProgress = await .prepare(
                sink: await progressSink.addChild(
                    withLabel: "Import Backup: Recreate Indexes",
                    unitCount: 12,
                ),
            )
            finalizeProgress = await progressSink.addChild(
                withLabel: "Import Backup: Finalize",
                unitCount: 5,
            )
        } else {
            frameRestoreProgress = nil
            recreateIndexesProgress = nil
            finalizeProgress = nil
        }

        let backupInfo = try await db.awaitableWriteWithRollbackIfThrows { tx in
            return try BenchMemory(
                title: benchTitle,
                memorySamplerRatio: Constants.memorySamplerFrameRatio,
                logInProduction: true,
            ) { memorySampler -> BackupProto_BackupInfo in
                return try self.databaseChangeObserver.disable(tx: tx) { tx in
                    let inputStream: BackupArchiveProtoInputStream
                    switch openInputStreamBlock(fileUrl, frameRestoreProgress, tx) {
                    case .success(let protoStream, _):
                        inputStream = protoStream
                    case .fileNotFound:
                        throw OWSAssertionError("File not found!")
                    case .unableToOpenFileStream:
                        throw OWSAssertionError("Unable to open input stream!")
                    case .hmacValidationFailedOnEncryptedFile:
                        throw OWSAssertionError("HMAC validation failed!")
                    }

                    let inputFileSize = try OWSFileSystem.fileSize(of: fileUrl)

                    return try self._importBackup(
                        inputStream: inputStream,
                        inputFileSize: inputFileSize,
                        localIdentifiers: localIdentifiers,
                        isPrimaryDevice: isPrimaryDevice,
                        backupPurpose: backupPurpose,
                        recreateIndexesProgress: recreateIndexesProgress,
                        memorySampler: memorySampler,
                        tx: tx,
                    )
                }
            }
        }

        appVersion.didRestoreFromBackup(
            backupCurrentAppVersion: backupInfo.currentAppVersion.nilIfEmpty,
            backupFirstAppVersion: backupInfo.firstAppVersion.nilIfEmpty,
        )

        try await self.finalizeBackupImport(progress: finalizeProgress)
    }

    private func _importBackup(
        inputStream stream: BackupArchiveProtoInputStream,
        inputFileSize: UInt64,
        localIdentifiers: LocalIdentifiers,
        isPrimaryDevice: Bool,
        backupPurpose: MessageBackupPurpose,
        recreateIndexesProgress: BackupArchiveImportRecreateIndexesProgress?,
        memorySampler: MemorySampler,
        tx: DBWriteTransaction,
    ) throws -> BackupProto_BackupInfo {
        let bencher = BackupArchive.RestoreBencher(
            dateProviderMonotonic: dateProviderMonotonic,
            memorySampler: memorySampler,
        )

        switch backupRestoreState(tx: tx) {
        case .none:
            break
        case .unfinalized, .finalized:
            throw OWSAssertionError("Restoring from backup twice!")
        }

        let startDate = dateProvider()
        let remoteConfig = remoteConfigManager.currentConfig()
        let attachmentByteCounter = BackupArchiveAttachmentByteCounter()

        // Drops all indexes on the `TSInteraction` table before doing the
        // import, which dramatically speeds up the import. We'll then recreate
        // all these indexes in bulk afterwards.
        let interactionIndexes = try bencher.benchPreFrameRestoreAction(.DropInteractionIndexes) {
            try dropAllIndexes(
                forTable: InteractionRecord.databaseTableName,
                tx: tx,
            )
        }

        var frameErrors = [LoggableErrorAndProto]()
        let result = Result<BackupProto_BackupInfo, Error>(catching: {
            var hasMoreFrames = false
            var framesRestored: UInt64 = 0

            let backupInfo: BackupProto_BackupInfo
            switch stream.readHeader() {
            case .success(let header, let moreBytesAvailable):
                backupInfo = header
                hasMoreFrames = moreBytesAvailable
                framesRestored += 1
            case .invalidByteLengthDelimiter:
                throw OWSAssertionError("invalid byte length delimiter on header")
            case .emptyFinalFrame:
                throw OWSAssertionError("invalid empty header frame")
            case .protoDeserializationError(let error):
                // Fail if we fail to deserialize the header.
                frameErrors.append(LoggableErrorAndProto(
                    error: BackupArchive.RestoreFrameError.restoreFrameError(
                        .invalidProtoData(.missingBackupInfoHeader),
                    ),
                    wasFrameDropped: true,
                ))
                throw error
            }

            logger.info("Importing with version \(backupInfo.version), timestamp \(backupInfo.backupTimeMs)")

            guard backupInfo.version == Constants.supportedBackupVersion else {
                frameErrors.append(LoggableErrorAndProto(
                    error: BackupArchive.RestoreFrameError.restoreFrameError(
                        .invalidProtoData(.unsupportedBackupInfoVersion),
                    ),
                    wasFrameDropped: true,
                    protoFrame: backupInfo,
                ))
                throw BackupImportError.unsupportedVersion
            }
            do {
                let mrbk = try BackupKey(contents: backupInfo.mediaRootBackupKey)
                localStorage.setMediaRootBackupKey(MediaRootBackupKey(backupKey: mrbk), tx: tx)
            } catch {
                frameErrors.append(LoggableErrorAndProto(
                    error: BackupArchive.RestoreFrameError.restoreFrameError(
                        .invalidProtoData(.invalidMediaRootBackupKey),
                    ),
                    wasFrameDropped: true,
                    protoFrame: backupInfo,
                ))
                throw error
            }

            /// Wraps all the various "contexts" we pass to downstream archivers.
            struct Contexts {
                let accountData: BackupArchive.AccountDataRestoringContext
                let chat: BackupArchive.ChatRestoringContext
                var chatItem: BackupArchive.ChatItemRestoringContext
                let customChatColor: BackupArchive.CustomChatColorRestoringContext
                let recipient: BackupArchive.RecipientRestoringContext
                let stickerPack: BackupArchive.RestoringContext

                init(
                    localIdentifiers: LocalIdentifiers,
                    backupPurpose: MessageBackupPurpose,
                    startDate: Date,
                    remoteConfig: RemoteConfig,
                    attachmentByteCounter: BackupArchiveAttachmentByteCounter,
                    isPrimaryDevice: Bool,
                    tx: DBWriteTransaction,
                ) {
                    accountData = BackupArchive.AccountDataRestoringContext(
                        backupPurpose: backupPurpose,
                        localIdentifiers: localIdentifiers,
                        startDate: startDate,
                        remoteConfig: remoteConfig,
                        attachmentByteCounter: attachmentByteCounter,
                        isPrimaryDevice: isPrimaryDevice,
                        tx: tx,
                    )
                    customChatColor = BackupArchive.CustomChatColorRestoringContext(
                        accountDataContext: accountData,
                        localIdentifiers: localIdentifiers,
                        startDate: startDate,
                        remoteConfig: remoteConfig,
                        attachmentByteCounter: attachmentByteCounter,
                        isPrimaryDevice: isPrimaryDevice,
                        tx: tx,
                    )
                    recipient = BackupArchive.RecipientRestoringContext(
                        localIdentifiers: localIdentifiers,
                        startDate: startDate,
                        remoteConfig: remoteConfig,
                        attachmentByteCounter: attachmentByteCounter,
                        isPrimaryDevice: isPrimaryDevice,
                        tx: tx,
                    )
                    chat = BackupArchive.ChatRestoringContext(
                        customChatColorContext: customChatColor,
                        recipientContext: recipient,
                        localIdentifiers: localIdentifiers,
                        startDate: startDate,
                        remoteConfig: remoteConfig,
                        attachmentByteCounter: attachmentByteCounter,
                        isPrimaryDevice: isPrimaryDevice,
                        tx: tx,
                    )
                    chatItem = BackupArchive.ChatItemRestoringContext(
                        accountDataContext: accountData,
                        chatContext: chat,
                        recipientContext: recipient,
                        localIdentifiers: localIdentifiers,
                        startDate: startDate,
                        remoteConfig: remoteConfig,
                        attachmentByteCounter: attachmentByteCounter,
                        isPrimaryDevice: isPrimaryDevice,
                        tx: tx,
                    )
                    stickerPack = BackupArchive.RestoringContext(
                        localIdentifiers: localIdentifiers,
                        startDate: startDate,
                        remoteConfig: remoteConfig,
                        attachmentByteCounter: attachmentByteCounter,
                        isPrimaryDevice: isPrimaryDevice,
                        tx: tx,
                    )
                }
            }
            let contexts = Contexts(
                localIdentifiers: localIdentifiers,
                backupPurpose: backupPurpose,
                startDate: startDate,
                remoteConfig: remoteConfig,
                attachmentByteCounter: attachmentByteCounter,
                isPrimaryDevice: isPrimaryDevice,
                tx: tx,
            )

            while hasMoreFrames {
                try Task.checkCancellation()
                try autoreleasepool {
                    let frame: BackupProto_Frame?
                    switch stream.readFrame() {
                    case let .success(_frame, moreBytesAvailable):
                        frame = _frame
                        hasMoreFrames = moreBytesAvailable
                        framesRestored += 1
                    case .invalidByteLengthDelimiter:
                        throw OWSAssertionError("invalid byte length delimiter on header")
                    case .emptyFinalFrame:
                        frame = nil
                        hasMoreFrames = false
                    case .protoDeserializationError(let error):
                        // fail the whole thing if we fail to deserialize one frame
                        owsFailDebug("Failed to deserialize proto frame!")
                        if BuildFlags.Backups.restoreFailOnAnyError {
                            throw error
                        } else {
                            return
                        }
                    }

                    guard
                        let frame,
                        let frameItem = frame.item
                    else {
                        if hasMoreFrames {
                            frameErrors.append(LoggableErrorAndProto(
                                error: BackupArchive.UnrecognizedEnumError(
                                    enumType: BackupProto_Frame.OneOf_Item.self,
                                ),
                                wasFrameDropped: true,
                            ))
                        }
                        return
                    }

                    try bencher.processFrame { frameBencher in
                        defer {
                            frameBencher.didProcessFrame(frame)
                        }

                        switch frameItem {
                        case .recipient(let recipient):
                            let recipientResult: BackupArchive.RestoreFrameResult
                            switch recipient.destination {
                            case nil:
                                recipientResult = .unrecognizedEnum(BackupArchive.UnrecognizedEnumError(
                                    enumType: BackupProto_Recipient.OneOf_Destination.self,
                                ))
                            case .self_p(let selfRecipientProto):
                                recipientResult = localRecipientArchiver.restoreSelfRecipient(
                                    selfRecipientProto,
                                    recipient: recipient,
                                    context: contexts.recipient,
                                )
                            case .contact(let contactRecipientProto):
                                recipientResult = contactRecipientArchiver.restoreContactRecipientProto(
                                    contactRecipientProto,
                                    recipient: recipient,
                                    context: contexts.recipient,
                                )
                            case .group(let groupRecipientProto):
                                recipientResult = groupRecipientArchiver.restoreGroupRecipientProto(
                                    groupRecipientProto,
                                    recipient: recipient,
                                    context: contexts.recipient,
                                )
                            case .distributionList(let distributionListRecipientProto):
                                recipientResult = distributionListRecipientArchiver.restoreDistributionListRecipientProto(
                                    distributionListRecipientProto,
                                    recipient: recipient,
                                    context: contexts.recipient,
                                )
                            case .releaseNotes(let releaseNotesRecipientProto):
                                recipientResult = releaseNotesRecipientArchiver.restoreReleaseNotesRecipientProto(
                                    releaseNotesRecipientProto,
                                    recipient: recipient,
                                    context: contexts.recipient,
                                )
                            case .callLink(let callLinkRecipientProto):
                                recipientResult = callLinkRecipientArchiver.restoreCallLinkRecipientProto(
                                    callLinkRecipientProto,
                                    recipient: recipient,
                                    context: contexts.recipient,
                                )
                            }

                            switch recipientResult {
                            case .success:
                                return
                            case .unrecognizedEnum(let error):
                                frameErrors.append(LoggableErrorAndProto(error: error, wasFrameDropped: true, protoFrame: recipient))
                                return
                            case .partialRestore(let errors):
                                frameErrors.append(contentsOf: errors.map { LoggableErrorAndProto(error: $0, wasFrameDropped: false, protoFrame: recipient) })
                            case .failure(let errors):
                                frameErrors.append(contentsOf: errors.map { LoggableErrorAndProto(error: $0, wasFrameDropped: true, protoFrame: recipient) })
                                if BuildFlags.Backups.restoreFailOnAnyError {
                                    throw BackupError()
                                }
                            }
                        case .chat(let chat):
                            let chatResult = chatArchiver.restore(
                                chat,
                                context: contexts.chat,
                            )
                            switch chatResult {
                            case .success:
                                return
                            case .unrecognizedEnum(let error):
                                frameErrors.append(LoggableErrorAndProto(error: error, wasFrameDropped: true, protoFrame: chat))
                                return
                            case .partialRestore(let errors):
                                frameErrors.append(contentsOf: errors.map { LoggableErrorAndProto(error: $0, wasFrameDropped: false, protoFrame: chat) })
                            case .failure(let errors):
                                frameErrors.append(contentsOf: errors.map { LoggableErrorAndProto(error: $0, wasFrameDropped: true, protoFrame: chat) })
                                if BuildFlags.Backups.restoreFailOnAnyError {
                                    throw BackupError()
                                }
                            }
                        case .chatItem(let chatItem):
                            let chatItemResult = chatItemArchiver.restore(
                                chatItem,
                                context: contexts.chatItem,
                            )
                            switch chatItemResult {
                            case .success:
                                return
                            case .unrecognizedEnum(let error):
                                frameErrors.append(LoggableErrorAndProto(error: error, wasFrameDropped: true, protoFrame: chatItem))
                                return
                            case .partialRestore(let errors):
                                frameErrors.append(contentsOf: errors.map { LoggableErrorAndProto(error: $0, wasFrameDropped: false, protoFrame: chatItem) })
                            case .failure(let errors):
                                frameErrors.append(contentsOf: errors.map { LoggableErrorAndProto(error: $0, wasFrameDropped: true, protoFrame: chatItem) })
                                if BuildFlags.Backups.restoreFailOnAnyError {
                                    throw BackupError()
                                }
                            }
                        case .account(let backupProtoAccountData):
                            let accountDataResult = accountDataArchiver.restore(
                                backupProtoAccountData,
                                context: contexts.accountData,
                                chatColorsContext: contexts.customChatColor,
                                chatItemContext: contexts.chatItem,
                            )
                            switch accountDataResult {
                            case .success:
                                return
                            case .unrecognizedEnum(let error):
                                frameErrors.append(LoggableErrorAndProto(error: error, wasFrameDropped: true, protoFrame: backupProtoAccountData))
                                return
                            case .partialRestore(let errors):
                                frameErrors.append(contentsOf: errors.map { LoggableErrorAndProto(error: $0, wasFrameDropped: false, protoFrame: backupProtoAccountData) })
                            case .failure(let errors):
                                frameErrors.append(contentsOf: errors.map { LoggableErrorAndProto(error: $0, wasFrameDropped: true, protoFrame: backupProtoAccountData) })
                                // We always fail if we fail to import account data, even in prod.
                                throw BackupError()
                            }
                        case .stickerPack(let backupProtoStickerPack):
                            let stickerPackResult = stickerPackArchiver.restore(
                                backupProtoStickerPack,
                                context: contexts.stickerPack,
                            )
                            switch stickerPackResult {
                            case .success:
                                return
                            case .unrecognizedEnum(let error):
                                frameErrors.append(LoggableErrorAndProto(error: error, wasFrameDropped: true, protoFrame: backupProtoStickerPack))
                                return
                            case .partialRestore(let errors):
                                frameErrors.append(contentsOf: errors.map { LoggableErrorAndProto(error: $0, wasFrameDropped: false, protoFrame: backupProtoStickerPack) })
                            case .failure(let errors):
                                frameErrors.append(contentsOf: errors.map { LoggableErrorAndProto(error: $0, wasFrameDropped: true, protoFrame: backupProtoStickerPack) })
                                if BuildFlags.Backups.restoreFailOnAnyError {
                                    throw BackupError()
                                }
                            }
                        case .adHocCall(let backupProtoAdHocCall):
                            let adHocCallResult = adHocCallArchiver.restore(
                                backupProtoAdHocCall,
                                context: contexts.chatItem,
                            )
                            switch adHocCallResult {
                            case .success:
                                return
                            case .unrecognizedEnum(let error):
                                frameErrors.append(LoggableErrorAndProto(error: error, wasFrameDropped: true, protoFrame: backupProtoAdHocCall))
                                return
                            case .partialRestore(let errors):
                                frameErrors.append(contentsOf: errors.map { LoggableErrorAndProto(error: $0, wasFrameDropped: false, protoFrame: backupProtoAdHocCall) })
                            case .failure(let errors):
                                frameErrors.append(contentsOf: errors.map { LoggableErrorAndProto(error: $0, wasFrameDropped: true, protoFrame: backupProtoAdHocCall) })
                                if BuildFlags.Backups.restoreFailOnAnyError {
                                    throw BackupError()
                                }
                            }
                        case .notificationProfile:
                            // Notification profiles are unsupported on iOS and
                            // we do not even round trip them per spec.
                            break
                        case .chatFolder:
                            // Chat folders are unsupported on iOS and
                            // we do not even round trip them per spec.
                            break
                        }
                    }
                }
            }

            stream.closeFileStream()

            // Now that we've imported successfully, we want to recreate the
            // the indexes we temporarily dropped.
            recreateIndexesProgress?.willStartIndexRecreation(totalFramesRestored: framesRestored)
            try bencher.benchPostFrameRestoreAction(.RecreateInteractionIndexes) {
                try createIndexes(
                    interactionIndexes,
                    onTable: InteractionRecord.databaseTableName,
                    tx: tx,
                )
            }
            recreateIndexesProgress?.didFinishIndexRecreation()

            // Take any necessary post-frame-restore actions.
            try postFrameRestoreActionManager.performPostFrameRestoreActions(
                recipientActions: contexts.recipient.postFrameRestoreActions,
                chatActions: contexts.chat.postFrameRestoreActions,
                bencher: bencher,
                chatItemContext: contexts.chatItem,
            )

            // Index threads synchronously, since that should be fast.
            bencher.benchPostFrameRestoreAction(.IndexThreads) {
                fullTextSearchIndexer.indexThreads(tx: tx)
            }

            // Schedule background message indexing, since that'll be slow.
            try fullTextSearchIndexer.scheduleMessagesJob(tx: tx)

            // Record that we've restored a Backup!
            kvStore.setInt(
                BackupRestoreState.unfinalized.rawValue,
                key: Constants.keyValueStoreRestoreStateKey,
                transaction: tx,
            )

            // Populate "last Backup" details, since otherwise they'll be blank
            // and imply the user has no Backup.
            backupSettingsStore.setLastBackupDetails(
                date: Date(millisecondsSince1970: backupInfo.backupTimeMs),
                backupFileSizeBytes: inputFileSize,
                backupMediaSizeBytes: attachmentByteCounter.attachmentByteSize(),
                tx: tx,
            )

            tx.addSyncCompletion { [self] in
                Task {
                    // Kick off avatar fetches enqueued during restore.
                    try await avatarFetcher.runIfNeeded()
                }

                Task {
                    // Kick off attachment downloads enqueued during restore.
                    try await backupAttachmentCoordinator.restoreAttachmentsIfNeeded()
                }

                // We may have inserted disappearing messages, so we need to let
                // the expiration job know.
                disappearingMessagesExpirationJob.restart()
            }

            logger.info("Imported with version \(backupInfo.version), timestamp \(backupInfo.backupTimeMs)")
            logger.info("Backup app version: \(backupInfo.currentAppVersion.nilIfEmpty ?? "Missing!")")
            logger.info("Backup first app version: \(backupInfo.firstAppVersion.nilIfEmpty ?? "Missing!")")
            bencher.logResults()

            return backupInfo
        })

        processErrors(errors: frameErrors, didFail: result.isSuccess.negated)
        return try result.get()
    }

    // MARK: -

    private struct SQLiteIndexInfo {
        let tableName: String
        let sqlThatCreatedIndex: String
    }

    private func dropAllIndexes(
        forTable tableName: String,
        tx: DBWriteTransaction,
    ) throws -> [SQLiteIndexInfo] {
        let allIndexesOnTable: [GRDB.IndexInfo] = try tx.database.indexes(on: tableName)

        var sqliteIndexInfos = [SQLiteIndexInfo]()

        for index in allIndexesOnTable {
            if index.name.contains("autoindex") {
                // Skip indexes automatically created by SQLite, such as on
                // primary keys.
                continue
            }

            guard
                let sqlThatCreatedIndex = try String.fetchOne(
                    tx.database,
                    sql: """
                        SELECT sql FROM sqlite_master
                        WHERE type = 'index'
                        AND name = '\(index.name)'
                    """,
                )
            else {
                throw OWSAssertionError("Failed to get SQL for creating index \(index.name)!")
            }

            sqliteIndexInfos.append(SQLiteIndexInfo(
                tableName: tableName,
                sqlThatCreatedIndex: sqlThatCreatedIndex,
            ))

            try tx.database.drop(index: index.name)
        }

        return sqliteIndexInfos
    }

    private func createIndexes(
        _ indexInfos: [SQLiteIndexInfo],
        onTable tableName: String,
        tx: DBWriteTransaction,
    ) throws {
        owsPrecondition(indexInfos.allSatisfy { $0.tableName == tableName })

        for indexInfo in indexInfos {
            try tx.database.execute(sql: indexInfo.sqlThatCreatedIndex)
        }
    }

    // MARK: -

    private func processErrors(
        errors: [LoggableErrorAndProto],
        didFail: Bool,
    ) {
        let collapsedErrors = BackupArchive.collapse(errors)
        var maxLogLevel = -1
        var wasFrameDropped = false
        collapsedErrors.forEach { collapsedError in
            collapsedError.log()
            maxLogLevel = max(maxLogLevel, collapsedError.logLevel.rawValue)
            if collapsedError.wasFrameDropped {
                wasFrameDropped = true
            }
        }
        if wasFrameDropped {
            // Log this specifically so we can do a naive exact text search in debug logs.
            logger.error("Dropped frame(s) on backup export or import!!!")
        }
        // Only present errors if some error rises above warning.
        // (But if one does, present _all_ errors).
        if maxLogLevel > BackupArchive.LogLevel.warning.rawValue {
            Task {
                await db.awaitableWrite { tx in
                    backupArchiveErrorPresenter.persistErrors(collapsedErrors, didFail: didFail, tx: tx)
                }
            }

        }
    }

    private func validateEncryptedBackup(
        fileUrl: URL,
        backupEncryptionKey: MessageBackupKey,
        backupPurpose: MessageBackupPurpose,
    ) async throws {
        let fileSize = (try? OWSFileSystem.fileSize(ofPath: fileUrl.path)) ?? 0

        do {
            let result = try validateMessageBackup(key: backupEncryptionKey, purpose: backupPurpose, length: fileSize) {
                return try FileHandle(forReadingFrom: fileUrl)
            }
            if result.fields.count > 0 {
                throw BackupValidationError.unknownFields(result.fields)
            }
        } catch {
            switch error {
            case let validationError as MessageBackupValidationError:
                await backupArchiveErrorPresenter.persistValidationError(validationError)
                logger.error("Backup validation failed \(validationError.errorMessage)")
                throw BackupValidationError.validationFailed(
                    message: validationError.errorMessage,
                    unknownFields: validationError.unknownFields.fields,
                )
            case SignalError.ioError(let description):
                logger.error("Backup validation i/o error: \(description)")
                throw BackupValidationError.ioError(description)
            default:
                logger.error("Backup validation unknown error: \(error)")
                throw BackupValidationError.unknownError
            }
        }
    }

    // MARK: -

    public func scheduleRestoreFromSVRBBeforeNextExport(tx: DBWriteTransaction) {
        kvStore.setBool(
            true,
            key: Constants.keyValueStoreNeedForwardSecrecyTokenFetchKey,
            transaction: tx,
        )
    }

    private func needsRestoreFromSVRBBeforeRemoteExport(tx: DBReadTransaction) -> Bool {
        kvStore.getBool(
            Constants.keyValueStoreNeedForwardSecrecyTokenFetchKey,
            defaultValue: false,
            transaction: tx,
        )
    }

    private func fetchRemoteSVRBForwardSecrecyToken(
        key: MessageRootBackupKey,
        auth: ChatServiceAuth,
        logger: PrefixedLogger,
    ) async throws {
        let backupServiceAuth = try await backupRequestManager.fetchBackupServiceAuthForRegistration(
            key: key,
            localAci: key.aci,
            chatServiceAuth: auth,
            logger: logger,
        )

        let metadataHeader: BackupNonce.MetadataHeader
        do {
            metadataHeader = try await backupCdnInfo(
                backupKey: key,
                backupAuth: backupServiceAuth,
                logger: logger,
            ).metadataHeader
        } catch let error as OWSHTTPError where error.responseStatusCode == 404 {
            // If no backup is found, treat this as unrecoverable
            throw SVRBError.unrecoverable
        }

        let nonceSource = BackupImportSource.NonceMetadataSource.svrB(header: metadataHeader, auth: auth)
        let source = BackupImportSource.remote(key: key, nonceSource: nonceSource)
        _ = try await source.deriveBackupEncryptionKeyWithSVRBIfNeeded(
            backupRequestManager: backupRequestManager,
            db: db,
            libsignalNet: libsignalNet,
            nonceStore: backupNonceMetadataStore,
            logger: logger,
        )
    }
}
